Java 8 Lambda表达式和Stream API详细教程和使用实例

Lambda表达式介绍

Lambda表达式是Java 8中新增的新功能之一,使用lambda表达式可以替代只有一个抽象函数的函数式接口的实现,告别匿名内部类并使代码简单易懂。同时配合Stream API,可以提升对集合的迭代、遍历过滤等操作的并行性和便捷性。Lambda表达式的官方文档可见:https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html#use-case。下面首先看两个常见的lambda表达式使用示例,多线程创建:

    // Lambda使用示例1
    @Test
    public void demo1() {
        // no lambda
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread run...");
            }
        }).start();

        // lambda
        new Thread(() -> {System.out.println("thread run...");}).start();
    }

集合的处理(排序):

    // lambda使用示例2
    @Test
    public void demo2() {
        List strs = Arrays.asList("websphere", "nginx", "weblogic", "tomcat");
        // no lambda
        Collections.sort(strs, new Comparator() {
            @Override
            public int compare(String o1, String o2) {
                return o1.length() - o2.length();
            }
        });
        System.out.println(strs);

        // lambda
        Collections.sort(strs, (a, b) -> a.length() - b.length());
        System.out.println(strs);
    }

 因此lambda表达式具有一些特点:

  • 体现了函数式编程。
  • 实现了参数类型的自动推断。
  • 代码更加简洁。
  • 易于多核CPU下的并行处理等。

那么对于学号Lambda表达式需要注意哪些呢?需要学习和了解Java泛型的使用,并注意多练习,否则很容易忘掉(实际上这篇文章就是因为之前的很多东西都忘了,才进行补充的)

函数式接口

上面开始便提到了lambda表达式是针对函数式接口进行的操作过程,因此任何有函数式接口的地方就可以使用lambda表达式。而什么是函数式接口呢?函数式接口是只有一个抽象方法接口。既然明确了是抽象方法,那么其他的一些不属于抽象方法的方法在接口中存在则不影响函数式接口的定义。例如如下:

@FunctionalInterface // 帮助我们注解一个函数式接口
public interface FunctionInterfaceDemo {

    int func1();

    // Object类中的方法也不算
    public int hashCode();

    // 默认方法
    default int func2() {
        return 1;
    }

    // 静态方法
    static int func3() {
        return 1;
    }
}

该接口中仅有func1方法为真正的抽象方法,hashCode方法虽然是抽象方法,但其实际上是Object类中的方法,因此它在这里并不算抽象方法。而另外的default方法和static方法也不是抽象方法,因此该接口符合函数式接口的定义。此外使用@FunctionalInterface注解可以帮助我们定义函数式接口,加入该注解后,如果接口中不满足函数式接口,则会报错。

常用的一些函数式接口

在jdk1.8之前其实也有一些常见的函数式接口,例如如下:

  • Runnable
  • Callable
  • Comparator

jdk1.8则提供了一些有用的函数式接口(rt.jar包中7个重要的函数式接口):

  1. Supplier 代表一个输出 
  2. Consumer 代表一个输入
  3. BiConsumer 代表两个输入
  4. Function 代表一个输入,一个输出(一般输入和输出时不同类型的)
  5. UnaryFunction 代表一个输入,一个输出(一般输入和输出是相同类型的)
  6. BiFunction 代表为两个输入,一个输出(一般输入和输出时不同类型的)
  7. BinaryOperator 代表两个输入,一个输出(一般输入和输出是相同类型的)

Lambda表达式详解

Lambda表达式实际上是对象是一个函数式接口的实例,而不是方法

lambda表达式语法

lambda表达式中主要包含两个部分,一个是函数式接口中抽象方法的参数(args),另一个就是该抽象方法的执行体(body)其中包含该方法的返回值。也就是如下形式:

(函数式接口的args) -> {函数式接口中抽象方法的实现逻辑}

使用图示如下: 

Java 8 Lambda表达式和Stream API详细教程和使用实例_第1张图片

注意:其中()里面的参数个数由函数式接口中抽象方法的参数个数来决定,其中的参数类型会自动推断,当然也可以指明。而当只有一个参数时,()可以被省略。当lambdabody中的实现代码只有一行时,{}和return可以省略。例如如下所示的lambda表达式示例:

() -> {}                         // 无参,无返回值
() -> {System.out.println(1);} 
() -> System.out.println();      // 省略{}
() -> {return 100;}              // 无参,有返回值
() -> 100;                       // 无参,有返回值,省略{}和return
() -> null;                      // 无参,有返回值(返回null)
(int x) -> {return x + 1;}       // 有参,有返回值
(x) -> x + 1;                    // 有参,有返回值,省略参数类型和{}
x -> x + 1;                      // 有参,有返回值,上面的简写

在对lambda表达式的使用过程中,有如下的几点需要注意:

  • lambda表达式会类型自动推断,但不能部分省略参数类型,所有参数要么全部指定类型,要么全不指定。例如下面的方式是不允许的。
(x, int y) -> {x + y};
  • 参数中不能使用final。例如下面的方式是不允许的。
(x, final y) -> {x + y};
  • lambda表达式可以强转为一个函数式接口。例如下面的方式将lambda表达式强转为Supplier接口是可以实现的。
Object obj = (Supplier) () -> "hello";
  • 不能将lambda表达式赋值给一个非函数式接口。例如下面的方式将lambda表达式赋值给Object对象是不允许的。
Object o = () -> "hello";
  • 不需要也不允许使用throws语句来声明lambda表达式可能会抛出的异常

lambda表达式实例

无参无返回值实例:

    // 无参,无返回值实例
    @Test
    public void demo1() {
        // 不使用lambda
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("run");
            }
        };
        new Thread(r1).start();

        // 使用lambda
        Runnable r2 = () -> {System.out.println("run");};
        // 可以直接省略大括号
        Runnable r3 = () -> System.out.println("run");
    }

无参有返回值实例:

    // 无参,有返回值实例
    @Test
    public void demo2() throws Exception {
        Callable c1 = new Callable() {
            @Override
            public Object call() throws Exception {
                return "Hello";
            }
        };
        c1.call();

        Callable c2 = () -> {return "Hello";};
        c2.call();
        // 逻辑很简单时可以直接省略return
        Callable c3 = () -> "Hello";
        c3.call();
    }

有参无返回值实例,返回自定义的函数式接口:

    // 有参,有返回值(使用自定义的FunctionInterface)
    @Test
    public void demo3() {
        UserMapper u1 = new UserMapper() {
            @Override
            public void insert(User user) {
                System.out.println("insert user: " + user);
            }
        };
        // 声明参数类型
        UserMapper u2 = (User user) -> {System.out.println("insert user: " + user);};
        // 省略参数类型
        UserMapper u3 = user -> {System.out.println("insert user: " + user);};

        u1.insert(new User());
        u2.insert(new User());
        u3.insert(new User());
    }

// 函数式接口
interface UserMapper {
    // 抽象方法
    void insert(User user);
}

有参有返回值实例,返回自定义的函数式接口:

    // 有参有返回值(使用自定义的FunctionInterface)
    @Test
    public void demo4() {
        OrderMapper o1 = new OrderMapper() {
            @Override
            public int insert(Order order) {
                System.out.println("insert order: " + order);
                return 1;
            }
        };

        OrderMapper o2 = (Order order) -> {return 1;};
        OrderMapper o3 = (order) -> {return 1;};
        OrderMapper o4 = (Order order) -> 1;
        OrderMapper o5 = order -> 1;

        o1.insert(new Order());
    }

interface OrderMapper {
    int insert(Order order);
}

具有较复杂方法体的函数式接口实例,这里用到了上述提到的Function接口,其表示输入一个值并输出一个值。

    // 方法体中逻辑较为复杂的情况
    @Test
    public void demo5() {
        // 输入int,返回int
        Function f1 = a -> {
            int sum = 0;
            for (int i = 0; i <= a; i++) {
                sum += i;
            }
            return sum;
        };
        System.out.println(f1.apply(10));
    }

lambda表达式直接调用其他方法实现抽象方法实例:

当抽象方法无返回值(void)时,在lambda表达式中调用其他方法时(无论是否有返回值),可以直接调用而不用考虑被调用的方法是否有返回值。但如果抽象方法具有返回值(例如int),那么在lambda表达式中实现方法体时,需要其返回值与抽象方法本身的返回值(int)相一致。

    @Test
    public void demo6() {
        // lambda表达式的实现,执行后面指定的方法
        Runnable r1 = () -> get();
        Runnable r2 = () -> exec();
        Runnable r4 = () -> find();
        // Runnable r3 = () -> 100;

        Foo f1 = () -> get();     // Foo函数式接口中的get方法有返回值,所以调用具有相同类型返回值的get方法可以
        // Foo f2 = () -> exec(); // exec方法没有返回值,而Foo接口中的get方法有返回值,出现异常
        // Foo f3 = () -> find(); // find方法返回值类型与Foo接口中的方法返回值类型不一致
        Foo f4  = () -> 100;
    }

    public int get() { return 1;}
    public void exec() {}
    public String find() { return " ";}

interface Foo {
    int get();
}

使用BiFunction函数式接口实例。方法体的实现为简单的计算传入的两个String类型变量的长度和并返回。

    @Test
    public void demo7() {
        // 两个输入(String,String),一个返回值(Integer)
        BiFunction bf = (a, b) -> {
            return a.length() + b.length();
        };
        System.out.println(bf.apply("java", "se"));
    }

Lambda表达式中方法的引用

什么是lambda表达式中的方法的引用?下面先看一下如下的两个lambda表达式:

    @Test
    void demo5() {
        // 输入一个字符串,将其转换为大写字母
        Function fn = (str) -> str.toUpperCase();
        System.out.println(fn.apply("admin"));

        // Consumer表示一个输入没有输出,下面实现输出输入的参数
        Consumer consumer = arg -> {System.out.println(arg);};
        consumer.accept("hello");
    }

上面两个lambda表达式有如下的特点:

  • 第一个lambda表达式,是直接使用了String对象的实例方法。
  • 第二个lambda表达式,则是调用了另外的方法来实现抽象接口的方法。

因此在Lambda表达式中,方法引用直接使用类或实例已经存在的方法(静态方法、实例方法或构造方法)来实现抽象方法的一种方式。在这种方式中,函数式接口中的抽象方法需要恰好可以使用其他已存在的方法来进行调用实现,而不需要重新实现抽象方法,此时就可以(可能)使用方法引用的方式重新构造lambda表达式。方法应用主要具有如下四种类型:

方法引用类型 语法格式 对应的lambda表达式
静态方法引用 类名::staticMethod (args) -> 类名.staticMethod(args)
实例方法引用 对象实例::instanceMethod

(args) -> new 对象实例.instanceMethod(args)

对象方法引用 类名::instanceMethod (instance, args) -> new 对象实例.instanceMethod(args)
构造方法应用 类名::new (args) -> new 类名(args)

静态方法引用

静态方法引用:如果函数式接口的实现恰好可以通过调用一个静态方法来实现,那么就可以使用静态方法引用。

  • 语法:类名::staticMethod

可以参考下面的静态方法引用实例:

public class ReferenceDemo {
    @Test
    void demo6() {
        Supplier s1 = () -> ReferenceDemo.get();
        // 等价于上面
        Supplier s2 = ReferenceDemo::get;
        Supplier s3 = Fun::get;
        System.out.println(s1.get());
        System.out.println(s2.get());
        System.out.println(s3.get());

        // 有输入参数
        Consumer c1 = (size) -> ReferenceDemo.consumer(size);
        Consumer c2 = ReferenceDemo::consumer; // 直接换成方法的引用
        c1.accept(100);
        c2.accept(100);

        // 有输入,也有输出 (一个输入,一个输出)
        Function f1 = str -> str.toUpperCase();  // 原始的lambda表达式
        Function f2 = String::toUpperCase;       // 调用String类中的静态方法来进行实现
        Function f3 = ReferenceDemo::toUpperCase;// 调用自定义的静态方法来实现
        System.out.println(f1.apply("hello"));
        System.out.println(f2.apply("hello"));
        System.out.println(f3.apply("hello"));

        // 两个输入,一个输出
        BiFunction bf1 = (str1, str2) -> str1.length() + str2.length();
        BiFunction bf2 = ReferenceDemo::length;
        System.out.println(bf1.apply("hello", "world"));
        System.out.println(bf2.apply("hello", "world"));
    }

    static String get() { return "hello"; }
    static void consumer(Integer size) { System.out.println("size: " + size);}
    static String toUpperCase(String str) {return str.toUpperCase();}
    static Integer length(String str1, String str2) { return str1.length() + str2.length();}

    static class Fun {
        public static String get() {
            return "hello";
        }
    }
}

实例方法的引用

实例方法引用:如果函数式接口的实现恰好可以通过调用一个实例的实例方法来实现,那么就可以使用实例方法引用。

  • 语法:instance::instanceMethod
    @Test
    void demo7() {
        Supplier s1 = () -> new ReferenceDemo().put();
        Supplier s2 = () -> {return new ReferenceDemo().put();};
        Supplier s3 = new ReferenceDemo()::put;
        System.out.println(s1.get());
        System.out.println(s2.get());
        System.out.println(s3.get());

        // 没有输入,有输出
        Consumer c1 = (size) -> new ReferenceDemo().con(size);
        Consumer c2 = (size) -> {new ReferenceDemo().con(size);};
        Consumer c3 = new ReferenceDemo()::con;
        c1.accept(100);
        c2.accept(100);
        c3.accept(100);

        // 其他的类似
        Function f1 = (str) -> this.toUpper(str); // this表示当前实例
        Function f2 = this::toUpper;
        f1.apply("pika_pika");
        f2.apply("hello_hello");
    }

    public String put() {return "hello";}
    public void con(Integer size) {System.out.println("size:" + size);}
    public Integer toUpper(String str) {return str.length();}

对象方法引用

相较于其他方法引用,对象方法引用是比较复杂的一个。对象方法引用:抽象方法的第一个参数类型刚好是实例方法的类型,抽象方法剩余的参数恰好可以当做实例方法的参数。如果函数式接口的实现能由上述实例方法调用来实现,那么就可以使用对象方法引用。

根据该表述,使用对象方法引用时具有如下3个使用条件:

  • 抽象方法必需有参数。
  • 抽象方法的第一个参数类型和实例方法的类型相同。
  • 抽象方法的剩余参数恰好可以当做实例方法的参数。
  • 注意:抽象方法的第一个参数类型最好是自定义的。

语法:类名::instanceMethod(实际上调用的是指定对象的实例方法)。

对象方法引用的语法示意图如下:

Java 8 Lambda表达式和Stream API详细教程和使用实例_第2张图片

如果上面的形式可以被满足,那么该lambda表达式就可以使用如下的对象方法引用来表示:

函数式接口 接口实例 = InstanceClass::method;

根据上述的形式,第一个参数需要参与类型的匹配过程,因此必须存在。所以对于没有参数的抽象方法无法使用对象方法引用,例如下面的示例:

    @Test
    void unusable() {
        // 如下函数式接口的抽象方法中没有输入参数,因此不能使用对象方法引用
        Runnable r = () -> {};
        Closeable c = () -> {};
        Supplier s = () -> "";
    }

可以使用对象方法引用的实例如下:

    class Too {
        public void foo() {
            System.out.println("invoking foo function...");
        }
    }

    class Too2 {
        public void foo() {
            System.out.println("invoking foo function...");
        }
        public void fo(String str) {
        }
    }

    class Producer {
        public Integer fun(String s) {
            return 1;
        }
        public void run(String name, String size) {
        }
    }

    @Test
    void demo1() {
        // Consumer函数接口中抽象方法没有参数,而传入的参数Too类型正好为方法体中的new Too类型。
        // 同时抽象方法中剩余的参数(无参数)与Too.foo()方法剩余的参数(无参数)相同,因此可以使用对象方法引用。
        Consumer c1 = (Too too) -> new Too().foo();
        Consumer c2 = Too::foo;
        c1.accept(new Too());
        c2.accept(new Too());

        // 此时传入的第一个参数类型为Too,与后续使用new Too2类型不匹配,因此无法使用对象方法引用
        Consumer c3 = (Too too) -> new Too2().foo();
        // Consumer c4 = Too2::foo;
    }

    @Test
    void demo2() {
        BiConsumer c1 = (too2, str) -> new Too2().fo(str);
        BiConsumer c2 = Too2::fo;

        BiFunction bf1 = (p, s) -> new Producer().fun(s);
        BiFunction bf2 = Producer::fun;
    }

    // 不能使用对方方法引用,因为第一个参数不是自定义的
    interface Execute1 {
        public void run1(String name, String size);
    }

    interface Execute2 {
        public void run2(Producer p, String name, String size);
    }

    @Test
    void demo3() {
        // 如果第一个参数类型不是自定义的,那么只能使用已有类中的对象方法(从业务逻辑上考虑:这种情况有时无法满足自己的业务逻辑需要)
        Execute1 e1 = (name, size) -> name.equalsIgnoreCase(size);
        Execute1 e2 = String::equalsIgnoreCase; // 这种写法属于实例方法引用,而不是对象方法引用

        // 如果第一个参数类型是自定义的,那么就可以使用自己的对象方法实现
        Execute2 e3 = (p, name, size) -> new Producer().run(name, size);
        Execute2 e4 = Producer::run;
    }

构造方法引用

构造方法引用:如果函数式接口的实现恰好可以通过调用一个类的构造方法来实现,那么就可以使用构造方法引用。

  • 语法:类名::new

构造方法引用实例如下:

@Test
    void demo4() {
        Supplier s1 = () -> new Person();
        Supplier s2 = Person::new;
        s1.get();
        s2.get();

        // 已存在的类,只要有无参构造方法都可以使用
        Supplier listSupplier = ArrayList::new;
        Supplier threadSupplier = Thread::new;
        Supplier setSupplier = HashSet::new;
        Supplier stringSupplier = String::new;
    }

    /**
     * 构造方法有参数的情况
     */
    @Test
    void demo5() {
        Consumer c1 = (age) -> new Account(age);
        Consumer c2 = Account::new;
        c1.accept(100);
        c2.accept(100); // 会调用有参数的构造方法

        // 这个不是构造方法引用
        Function fu1 = (str) -> Integer.valueOf(str);
        Function fu2 = Integer::valueOf;

        Function fu3 = (str) -> new Account();
        Function fu4 = Account::new; // 这样的使用就需要Account类中有一个参数类型为String的构造方法,否则不能使用
        fu3.apply("test");
        fu4.apply("admin");
    }

    class Person {
        public Person() {
            System.out.println("new Person()");
        }
    }

    class Account {
        public Account() {
            System.out.println("Account()");
        }
        public Account(int age) {
            System.out.println("Account(age)");
        }
        public Account(String name) {
            System.out.println("Account(name)");
        }
    }

Stream API

由于篇幅原因,Stream API方面的内容另起一篇文章:https://blog.csdn.net/yitian_z/article/details/104680964。

 

 

 

你可能感兴趣的:(Java编程及开发)