Java8 Lambda表达式教程

发布时间:2023-04-27 09:15:33

1. 什么是λ表达式

λ表达式本质上是一种匿名的方法。让我们来看看下面的例子:

public int add(int x, int y) { return x + y; }

转成λ表达式后是这样的: (int x, int y) -> x + y;

也可以省略参数类型,Java编译器将根据上下文推断:

(x, y) -> x + y; //回到两数之和

(x, y) -> { return x + y; } //显式指示返回值

可见λ表达式由参数列表、箭头三部分组成:(->),还有一个表达式或句块。

在下面的例子中λ表达式没有参数,也没有返回值(相当于一种方法接受0个参数,返回void实际上是Runnable中Run方法的实现):

() -> { System.out.println("Hello Lambda!"); }

如果只有一个参数可以被Java推断,则可以省略参数列表的括号:

c -> { return c.size(); }

2. λ表达式类型(是Object吗?)

λ表达式可视为Object(注意措辞)。λ表达式类型称为“目标类型”(target type)”。λ表达式的目标类型是“函数接口”(functional interface)",这是Java8新引入的概念。它的定义是:如果一个界面只有一个显式声明的抽象方法,那么它就是一个函数界面。一般用@Functionalinterface标注(也可以不标)。举例如下:

@FunctionalInterface public interface Runnable { void run(); } public interface Callable<V> { V call() throws Exception; } public interface ActionListener { void actionPerformed(ActionEvent e); } public interface Comparator<T> { int compare(T o1, T o2); boolean equals(Object obj); }

请注意最后一个Comparator接口。它声明了两种方法,它们似乎不符合函数接口的定义,但它们确实是函数接口。这是因为equals方法是object,所有接口都声明object的public方法——尽管它们大多是隐藏的。因此,comparator显式声明equals仍然是一个函数接口,不影响equals。

你可以用一个λ表达式是函数接口赋值: Runnable r1 = () -> {System.out.println("Hello Lambda!");};然后赋值Objectt:

Object obj = r1;但是你不能这样做:

Object obj = () -> {System.out.println("Hello Lambda!");}; // ERROR! Object is not a functional interface!

必须将显式转换为函数接口:

Object o = (Runnable) () -> { System.out.println("hi"); }; // 一个corectλ表达式只有在转换为函数接口后才能作为Object使用。因此,不能编译以下句子:

System.out.println( () -> {} ); //错误! 不明目标类型必须先转型:

System.out.println( (Runnable)() -> {} ); // 正确

假设你自己写了一个函数接口,长得和Runnable一模一样:

@FunctionalInterface public interface MyRunnable { public void run(); }那么

Runnable r1 = () -> {System.out.println("Hello Lambda!");}; MyRunnable2 r2 = () -> {System.out.println("Hello Lambda!");};

都是正确的写作方法。这说明一个λ只要函数匹配成功,表达式可以有多种目标类型(函数接口)。但是要注意一个λ表达式必须至少有一种目标类型。

JDK预定了许多函数接口,以避免用户重复定义。最典型的是Function:

@FunctionalInterface public interface Function<T, R> { R apply(T t); }

该接口代表函数,接受T型参数,并返回R型返回值。

另一个预定义函数接口叫Consumer,与Function唯一的区别是它没有返回值。

@FunctionalInterface public interface Consumer<T> { void accept(T t); }

还有一个Predicate,用来判断某个条件是否满足。常用于筛滤操作: @FunctionalInterface public interface Predicate<T> { boolean test(T t); }综上所述,一个λ表达式实际上是定义匿名方法,但该方法必须符合至少一个函数接口。3. λ使用表达式

3.1 λ在哪里使用表达式?

λ表达式主要用于替换以前广泛使用的内部匿名类别、各种回调,如事件响应器、传入Thread类别的Runnable等。请参见以下示例:

Thread oldSchool = new Thread( new Runnable () { @Override public void run() { System.out.println("This is from an anonymous class."); } } ); Thread gaoDuanDaQiShangDangCi = new Thread( () -> { System.out.println("This is from an anonymous method (lambda exp)."); } );

注意第二个线程λ表达式,你不需要把它变成Runnable,因为Java可以根据上下文自动推断:Thread的构造函数接受Runnnable参数并传入λ表达式正好符合其run()函数,所以Java编译器推断它是runnable。

从形式上看,λ表达式只是为你节省了几行代码。但将λJava的动机不仅仅是表达式引入。Java8有短期目标和长期目标。短期目标是:内部迭代并行处理与“集合批处理操作”相匹配(以下将讨论);长期目标是引导Java到函数编程语言的方向(不是完全成为函数编程语言,只是让它有更多的函数编程语言特征),正是因为这个原因,Oracle不仅仅是使用内部类来实现它λ表达式,相反,它使用了一种更动态、更灵活、更容易在未来扩展和改变的策略(invokedynamic)。

3.2 λ表达式和集合批处理操作(或块操作)

上面提到了集合类的批处理操作。这是Java8的另一个重要特征λJava8的主要特点是表达式的配合使用。API批处理操作的目的是实现集合类的“内部迭代”,并希望充分利用现代多核CPU进行并行计算。Java8之前的集合迭代(Iteration)它们都是外部的,即客户代码。内部迭代意味着Java类库而不是客户代码,而不是客户代码。例如:

for(Object o: list) { // 外部迭代 System.out.println(o); }

可以写成:

list.forEach(o -> {System.out.println(o);}); //foreach函数实现内部迭代

集合类(包括List)现在有一种foreach方法来迭代元素(遍历),所以我们不需要写for循环。foreach方法接受函数接口consumer作为参数,因此可以使用λ表达式。

这种内部迭代方法广泛存在于C++STL算法库等各种语言中Python、ruby、scala等。

Java8引入了集合类的另一个重要概念:流动(stream)。一个流通常以一个集合实例为数据源,然后在其上定义各种操作。流动API设计采用管道(pipelines)模式。一个对流操作将返回另一个流。就像IO的API或StringBufer的Append方法一样,可以在一个句子中串联多个不同的操作。看以下例子:

List<Shape> shapes = ... shapes.stream() .filter(s -> s.getColor() == BLUE) .forEach(s -> s.setColor(RED));

首先,调用stream方法,以集合对象shapes中的元素为数据源,生成一个流。然后将filter方法调用到这个流动中,挑出蓝色,返回另一个流动。最后,用foreach将这些蓝色物体喷成红色。(foreach方法不再回流,而是终端方法,类似于Stringbuffer调用多个append后的tostring)

filter方法的参数是predicate类型,foreach方法的参数是consumer类型,都是函数接口,可以使用λ表达式。

另一种方法叫paralelstream(),顾名思义,它和stream()一样,只是指为了充分利用现代CPU的多核特性,需要并行处理。

shapes.parallelStream(); // 或者shapes.stream().parallel()

让我们看看更多的例子。以下是典型的大数据处理方法,Filter-Map-Reduce:

///给出String类型的数组,找出所有不重复的素数 public void distinctPrimary(String... numbers) { List<String> l = Arrays.asList(numbers); List<Integer> r = l.stream() .map(e -> new Integer(e)) .filter(e -> Primes.isPrime(e)) .distinct() .collect(Collectors.toList()); System.out.println("distinctPrimary result is: " + r); }

第一步:将一系列String(假设都是合法数字)转换为List,然后调用Stream()生成流。

第二步:调用流的map方法,将每个元素从String转换为Integer,获得新的流。map方法接受Function类型的参数,上面介绍了Function是函数接口,所以在这里使用λ表达式。

第三步:调用流的filter方法,过滤不是素数的数字,获得新流。filter方法接受Predicate类型的参数。上面介绍了Predicate是函数接口,这里用λ表达式。

第四步:调用流的distinct方法,去掉重复,获得新流。这本质上是另一个filter操作。

第五步:用collect方法将最终结果收集到List中。collect方法接受collector类型的参数,指示如何收集最终结果。在这个例子中,结果简单地收集到一个List。也可以使用Collectors.toMap(e->e, e->e)将结果收集到一个Map中,这意味着将结果收集到一个Map中,并使用这些素数作为键和值。tomap方法接受两种Function类型的参数,分别用于生成键和值,Function是一个函数接口,所以在这里使用λ表达式。

你可能会认为在这个例子中,List l被迭代了很多次,map,filter,distinct是一个循环,效率会很差。事实并非如此。返回另一个Stream的方法是“懒惰”(lazy)“是的,collect最终返回最终结果的方法是“急”(eager)”的。lazy的方法在遇到eager方法之前不会执行。

遇到eager方法时,依次执行前面的lazy方法。而且是管道贯通式执行。这意味着每个元素依次通过这些管道。例如,有一个元素“3”;然后通过filter,发现是素数,保留下来;再通过distinct,如果已经有一个3,那就直接丢弃,如果没有,就保留。这样,三个操作实际上只经过一个循环。

除collect外,其他eager操作还包括foreach,toArray,reduce等。

让我们来看看最常用的收集器方法,groupingBy:

///给出String类型的数组,找出每个素数,并统计其出现次数    public void primaryOccurrence(String... numbers) {        List<String> l = Arrays.asList(numbers);        Map<Integer, Integer> r = l.stream()            .map(e -> new Integer(e))            .filter(e -> Primes.isPrime(e))            .collect( Collectors.groupingBy(p->p, Collectors.summingInt(p->1)) );        System.out.println("primaryOccurrence result is: " + r);    }

注意这一行:

Collectors.groupingBy(p->p, Collectors.summingInt(p->1))

它的意思是:将结果收集到Map中,以统计的每个素数本身为键,其出现次数为值。

以下是reduce的例子:

///给出String类型的数组,寻求所有不重复素数的和    public void distinctPrimarySum(String... numbers) {        List<String> l = Arrays.asList(numbers);        int sum = l.stream()            .map(e -> new Integer(e))            .filter(e -> Primes.isPrime(e))            .distinct()            .reduce(0, (x,y) -> x+y); // equivalent to .sum()        System.out.println("distinctPrimarySum result is: " + sum);    }

reduce方法用于产生单一的最终结果。有许多预定义的reduce操作,如sum(),max(),min()等。

以现实世界中的栗子为例:

// 统计年龄为25-35岁的男女人数、比例    public void boysAndGirls(List<Person> persons) {        Map<Integer, Integer> result = persons.parallelStream().filter(p -> p.getAge()>=25 && p.getAge()<=35).            collect(                Collectors.groupingBy(p->p.getSex(), Collectors.summingInt(p->1))        );        System.out.print("boysAndGirls result is " + result);        System.out.println(", ratio (male : female) is " + (float)result.get(Person.MALE)/result.get(Person.FEMALE));    }

3.3 λ更多的表达式用法

// 嵌套的λ表达式 Callable<Runnable> c1 = () -> () -> { System.out.println("Nested lambda"); }; c1.call().run();

// 用于条件表达式 Callable<Integer> c2 = true ? (() -> 42) : (() -> 24); System.out.println(c2.call());

// 要定义递归函数,请注意使用this限制 protected UnaryOperator<Integer> factorial = i -> i == 0 ? 1 : i * this.factorial.apply( i - 1 ); ... System.out.println(factorial.apply(3));

在Java中,随声明调用的方式是不可能的,比如下面这样,声明了一个λ表达式(x, y) -> x + y,同时试图通过传入实参(2), 3)调用它:

int five = ( (x, y) -> x + y ) (2, 3); // ERROR! try to call a lambda in-place

这在C++中是可以的,但在Java中不行。Javaλ表达式只能用作赋值、传参、返回值等。

4. 其它相关概念

4.1 捕获(Capture)

捕获的概念在于解决λ我们在表达式中可以使用哪些外部变量(即除了它自己的参数和内部定义的本地变量)。

答案是:和内部类很像,但是有区别。区别在于内部类总是持有其外部类对象的引用。而且λ至于表达式,除非其外部类(包围类)对象的方法或成员在其内部使用,否则不持有该对象的引用。

在Java8之前,如果要在内部类访问外部对象的本地变量,则该变量必须声明为final。在Java8中,这种限制被消除,取而代之的是一个新概念,“effectively final”。这意味着你可以声明final,也可以不声明final,但按照final使用,即赋值永不改变。换句话说,在添加final前缀后,确保它不会出错。

内部类和在Java8中λ表达式可以访问effectively final本地变量。λ表达式的例子如下:

int tmp1 = 1; ////包围类成员变量    static int tmp2 = 2; ////包围类静态成员变量    public void testCapture() {        int tmp3 = 3; ///没有声明是final,但是effectively final本地变量        final int tmp4 = 4; //声明为final本地变量        int tmp5 = 5; ///普通本地变量                Function<Integer, Integer> f1 = i -> i + tmp1;        Function<Integer, Integer> f2 = i -> i + tmp2;        Function<Integer, Integer> f3 = i -> i + tmp3;        Function<Integer, Integer> f4 = i -> i + tmp4;        Function<Integer, Integer> f5 = i -> {            tmp5  += i; // 编译错!对tmp5的赋值导致它不是effectively final            return tmp5;        };        ...        tmp5 = 9; // 编译错!对tmp5的赋值导致它不是effectively final            return tmp5;        };        ...        tmp5 = 9; // 编译错!对tmp5的赋值导致它不是effectively final    }    ...

Java要求本地变量final或effectively final的原因是多线程并发问题。内部类,λ可以在不同的线程中执行表达式,允许多线程同时修改不符合Java设计理念的本地变量。

4.2 方法引用(Method reference)

任何一个λ表达式可以代表函数接口中唯一方法的匿名描述符。我们也可以使用特定类别的特定方法来代表这个描述符,称为方法参考。例如:

Integer::parseInt ///引用静态方法 System.out::print //引用实例方法 Person::new //引用构造器

以下是一组例子,教你如何引用替代方法λ表达式:p>

//c1 与 c2 是一样的(引用静态方法) Comparator<Integer> c2 = (x, y) -> Integer.compare(x, y); Comparator<Integer> c1 = Integer::compare; //下面两句是一样的(实例方法引用1) persons.forEach(e -> System.out.println(e)); persons.forEach(System.out::println); //下面两句是一样的(实例引用2) persons.forEach(person -> person.eat()); persons.forEach(Person::eat); ///下面两句是一样的(引用构造器) strList.stream().map(s -> new Integer(s)); strList.stream().map(Integer::new);引用使用方法,你的程序会变短。现在distinctprimarySum方法可以改写如下:

public void distinctPrimarySum(String... numbers) {        List<String> l = Arrays.asList(numbers);        int sum = l.stream().map(Integer::new).filter(Primes::isPrime).distinct().sum();        System.out.println("distinctPrimarySum result is: " + sum);    }

还有其他一些方法可以引用:

super::toString ////引用对象的父类方法 String[]::new ///引用数组结构器

4.3 默认方法(Default method)

在Java8中,有一种方法可以在界面声明中实现,称为默认方法。在此之前,界面中的所有方法都是抽象的。

public interface MyInterf { String m1(); default String m2() { return "Hello default method!"; } }这实际上混淆了接口和抽象类,但一个类仍然可以实现多个接口,只能继承一个抽象类。

由于Collection库需要为批处理操作添加新的方法,如foreach(),stream()等等,但现有的Collection接口无法修改——如果这样做,所有的实现类都需要修改,包括很多客户自制的实现类。所以我们必须使用这种妥协。

这样,我们就面临着一个类似于多继承的问题。如果Sub继承了两个接口,Base1和Base2,这两个接口有两种完全相同的默认方法,那么就会发生冲突。此时,Sub类必须通过重载来显示实现哪个接口(或提供自己的实现):

public class Sub implements Base1, Base2 {            public void hello() {            Base1.super.hello(); ///使用Base1的实现        }            }

Java8界面除了默认方法外,还可以实现静态方法:

public interface MyInterf {            String m1();                default String m2() {            return "Hello default method!";        }                static String m3() {            return "Hello static method in Interface!";        }            }

4.4 生成器函数(Generator function)

有时一流的数据源不一定是现有的集合对象,也可能是“生成器函数”。一个生成器函数产生一系列元素,并提供一个流。Stream.generate(Supplier<T> s)它是一个生成器函数。参数Supplier是一个函数接口,其中有唯一的抽象方法 <T> get()。

以下示例生成并打印5个随机数:

Stream.generate(Math::random).limit(5).forEach(System.out::println);

注意这个limit(5)。如果没有这个呼叫,这个句子将永远执行。也就是说,这个生成器是无限的。这个呼叫被称为终结操作或短路(short-circuiting)操作。

上一篇 Sqoop-1.4.6安装部署及详细使用介绍
下一篇 Oracle中UNION 与UNION ALL的学习

文章素材均来源于网络,如有侵权,请联系管理员删除。

标签: Java教程Java基础Java编程技巧面试题Java面试题