【Java 8】Lambda表达式

本文参考书籍《Java 8实战》,陆明刚、劳佳  译,如有侵权,请联系删除!


我们了解了利用行为参数化来传递代码有助于应对不断变化的需求,它允许我们定义一个代码块来表示一个行为,然后传递它。但我们也看到,使用匿名类来表示不同的行为并不令人满意:代码十分啰嗦。在本章中,我们会学习Java 8中解决这个问题的新工具——Lambda表达式。它可以很简洁地表示一个行为或传递代码。现在我们可以把Lambda表达式看作匿名功能,它基本上就是没有声明名称的方法,但和匿名类一样,它也可以作为参数传递给一个方法。

什么是Lambda表达式

可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。Lambda表达式鼓励我们采用上一章中提到的行为参数化风格,最终结果就是代码变得更清晰、更灵活。比如,利用Lambda表达式,可以更为简洁地自定义一个Comparator对象:

    Comparator<Apple> byWeight =
        (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

而在Java 8以前,我们是这样做的:

    Comparator<Apple> byWeight = new Comparator<Apple>() {
        public int compare(Apple a1, Apple a2) {
            return a1.getWeight().compareTo(a2.getWeight());
        }
    };

不得不承认,代码看起来更清晰了!

从上面的代码可以看出,Lambda表达式有三个部分:

1、参数列表——这里它采用了Comparator中compare方法的参数,两个Apple。

2、箭头——箭头->把参数列表与Lambda主体分隔开。

3、Lambda主体——比较两个Apple的重量。表达式就是Lambda的返回值了。

一般的,Lambda的基本语法是:

    (parameters) -> expression

或(请注意语句的花括号)

    (parameters) -> { statements; }

在哪里使用 Lambda

可以在函数式接口上使用Lambda表达式。所谓函数式接口,就是只定义一个抽象方法的接口。

我们再看一下上一章的例子:

    List<Apple> greenApples =
        filter(inventory, (Apple a) -> "green".equals(a.getColor()));

我们把 Lambda 表 达 式 作 为 第 二 个 参 数 传 给 filter 方 法 , 因 为 它 这 里 需 要Predicate<T>,Predicate只定义了一个抽象方法test,它一个函数式接口。Java API中我们已经接触到了一些函数式接口,比如Comparator和Runnable。

后面的章节我们还会看到,Java 8中的接口还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就是一个函数式接口。

Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现, 并把整个表达式作为函数式接口的实例。用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。下面的代码是有效的, 因为Runnable是一个只定义了一个抽象方法run的函数式接口:

    // 使用Lambda
    Runnable r = () -> System.out.println("Hello!");

    // 使用匿名类
    Runnable r2 = new Runnable() {
        public void run() {
            System.out.println("Hello World 2");
        }
    };

函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符。例如, Runnable接口可以看作一个什么也不接收什么也不返回(void)的函数的签名,因为它只有一个叫做run的抽象方法,这个方法什么也不接收,什么也不返回(void)。

我们使用了一个特殊表示法来描述Lambda和函数式接口的签名。 () -> void 代表了参数列表为空,且返回void的函数。 这正是Runnable接口所代表的。 举另一个例子, (Apple, Apple) -> int 代表接受两个Apple作为参数且返回int的函数。

如果你去看看新的Java API,会发现函数式接口带有@FunctionalInterface的标注。这个标注用于表示该接口会设计成一个函数式接口。如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。例如,错误消息可能是“Multiple non-overriding abstract methods found in interface Foo”,表明存在多个抽象方法。请注意,@FunctionalInterface不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。 它就像是@Override标注表示方法被重写了。

使用Lambda

为了应用不同的Lambda表达式,我们需要一套能够描述常见函数描述符的函数式接口。Java API中已经有了几个函数式接口,比如Comparator、Runnable,以及Callable。Java 8在java.util.function包中引入了几个新的函数式接口。

Predicate

java.util.function.Predicate<T>接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。这和我们先前创建的一样,现在就可以直接使用而无需自己去创建了。如果我们需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口。比如,定义一个接受String对象的Lambda表达式,如下所示:

    public static <T> List<T> filter(List<T> list, Predicate<T> p) {
        List<T> results = new ArrayList<>();
        for(T s: list) {
            if(p.test(s)) {
                results.add(s);
            }
        }
        return results;
    }

    Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
    List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);

Consumer

java.util.function.Consumer<T>定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。比如,在下面的代码中,使用forEach方法,配合Lambda来打印列表中的所有元素:

    public static <T> void forEach(List<T> list, Consumer<T> c) {
        for(T i: list){
            c.accept(i);
        }
    }

    forEach(Arrays.asList(1,2,3,4,5), (Integer i) -> System.out.println(i));

Function

java.util.function.Function<T, R>接口定义了一个叫作apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度)。在下面的代码中,展示了如何利用它来创建一个map方法,以将一个String列表映射到包含每个String长度的Integer列表:

    public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
        List<R> result = new ArrayList<>();
        for(T s: list) {
            result.add(f.apply(s));
        }
        return result;
    }
    
    List<Integer> l = map(Arrays.asList("lambdas","in","action"), (String s) -> s.length());

我们介绍了三个泛型函数式接口: Predicate<T>、 Consumer<T>和Function<T,R>。还有些函数式接口专为某些类型而设计。

回顾一下: Java类型要么是引用类型(比如Byte、 Integer、 Object、 List),要么是原始类型(比如int、 double、 byte、 char)。但是泛型(比如Consumer<T>中的T)只能绑定到引用类型。这是由泛型内部的实现方式造成的。 因此,在Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫作装箱(boxing)。相反的操作,也就是将引用类型转换为对应的原始类型,叫作拆箱(unboxing)。 Java还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作是自动完成的。比如,这就是为什么下面的代码是有效的(一个int被装箱成为Integer):

    List<Integer> list = new ArrayList<>();
        for (int i = 300; i < 400; i++) {
        list.add(i);
    }

但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存。

Java 8为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。比如,针对Predicate<T>,有专门应用于int型的IntPredicate,看下面的代码:

    IntPredicate evenNumbers = (int i) -> i % 2 == 0;
    evenNumbers.test(1000); // true,无装箱

    Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1;
    oddNumbers.test(1000); // false,装箱

一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如DoublePredicate、 IntConsumer、 LongBinaryOperator、 IntFunction等。 Function接口还有针对输出参数类型的变种: ToIntFunction<T>、 IntToDoubleFunction等。

类型检查、类型推断以及限制

我们说过,Lambda表达式可以为函数式接口生成一个实例。然而,Lambda表达式本身并不包含它在实现哪个函数式接口的信息。为了全面了解Lambda表达式,你应该知道Lambda的实际类型是什么。

类型检查

Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。让我们看看使用下面的Lambda表达式时背后发生了什么:

    List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);

首先,你要找出filter方法的声明。

第二,要求它是Predicate<Apple>(目标类型)对象的第二个正式参数。

第三, Predicate<Apple>是一个函数式接口,定义了一个叫作test的抽象方法。

第四, test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。

最后, filter的任何实际参数都必须匹配这个要求。

这段代码是有效的,因为我们所传递的Lambda表达式也同样接受Apple为参数,并返回一个boolean。请注意,如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必须与之匹配。

同样的 Lambda,不同的函数式接口

有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。比如,Callable和PrivilegedAction(它的抽象方法是T run(),具体参看源码),这两个接口都代表着什么也不接受且返回一个泛型T的函数。 因此,下面两个赋值是有效的:

    Callable<Integer> c = () -> 42;
    PrivilegedAction<Integer> p = () -> 42;

这 里 , 第 一 个 赋 值 的 目 标 类 型 是 Callable<Integer> , 第 二 个 赋 值 的 目 标 类 型 是PrivilegedAction<Integer>。

类型推断

Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。换句话说, Java编译器会像下面这样推断Lambda的参数类型: 

    List<Apple> greenApples = filter(inventory, a -> "green".equals(a.getColor())); // 参数a并没有指定类型

Lambda表达式有多个参数,代码可读性的好处就更为明显。例如,你可以这样来创建一个Comparator对象:

    // 没有类型推断
    Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
    // 有类型推断
    Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

使用局部变量

我们迄今为止所介绍的所有Lambda表达式都只用到了其主体里面的参数。但Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。 它们被称作捕获Lambda。例如,下面的Lambda捕获了portNumber变量:

    int portNumber = 1337;
    Runnable r = () -> System.out.println(portNumber);

Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final,或事实上是final。换句话说, Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量this。) 例如,下面的代码无法编译,因为portNumber变量被赋值两次:

    int portNumber = 1337;
    Runnable r = () -> System.out.println(portNumber); // 错误: Lambda表达式引用的局部变量必须是最终的( final)或事实上最终的
    portNumber = 31337;

为什么局部变量有这些限制呢?第一,实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线
程将这个变量收回之后,去访问该变量。因此, Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式。

方法引用

方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。在一些情况下,比起使用Lambda表达式,它们似乎更易读,感觉也更自然。下面就是用方法引用写的一个排序的例子:

    // 使用Lambda
    inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

    // 使用方法引用,请确保已经静态导入了Comparator中的comparing方法
    inventory.sort(comparing(Apple::getWeight))

如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。但是,显式地指明方法的名称,代码的可读性会更好。它是如何工作的呢?当 你 需 要使用 方 法 引用时 , 目标引用 放 在 分隔符::前 ,方法的名称放在后面 。例如 ,Apple::getWeight就是引用了Apple类中定义的方法getWeight。请记住,不需要括号,因为你没有实际调用这个方法。方法引用就是Lambda表达式(Apple a) -> a.getWeight()的快捷写法。下表给出了Java 8中方法引用的其他一些例子。

Lambda等效的方法引用
(Apple a) -> a.getWeight()Apple::getWeight
() -> Thread.currentThread().dumpStack()Thread.currentThread()::dumpStack
(str, i) -> str.substring(i)String::substring
(String s) -> System.out.println(s)System.out::println

可以把方法引用看作针对仅仅涉及单一方法的Lambda的语法糖,因为表达同样的事情时要写的代码更少了。

如何构建方法引用

方法引用主要有三类:

1、指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)。

2、指 向 任 意 类 型 实 例 方 法 的 方 法 引 用( 例 如 String 的 length 方 法 , 写作String::length)。

3、指向现有对象的实例方法的方法引用(假设有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensiveTransaction::getValue)。

第二种和第三种的区别是:类似于String::length的第二种方法引用的思想就是你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。例如, Lambda表达式(String s) -> s.toUppeCase()可以写作String::toUpperCase。但第三种方法引用指 的 是 , 你 在 Lambda 中 调 用 一 个 已 经 存 在 的 外 部 对 象 中 的 方 法 。 例 如 , Lambda 表 达 式()->expensiveTransaction.getValue()可以写作expensiveTransaction::getValue。一个是Lambda的参数,一个是外部的对象。

构造函数引用

对于一个现有构造函数,可以利用它的名称和关键字new来创建它的一个引用:ClassName::new。它的功能与指向静态方法的引用类似。例如,假设有一个构造函数没有参数,它适合Supplier的签名() -> Apple。你可以这样做:

    // 构造函数引用指向默认的Apple()构造函数
    Supplier<Apple> c1 = Apple::new;
    // 调用Supplier的get方法将产生一个新的Apple
    Apple a1 = c1.get();

这就等价于:

    // 利用默认构造函数创建Apple的Lambda表达式
    Supplier<Apple> c1 = () -> new Apple();
    // 调用Supplier的get方法将产生一个新的Apple
    Apple a1 = c1.get();

如果构造函数的签名是Apple(Integer weight),那么它就适合Function接口的签名,于是可以这样写:

    // 指向Apple(Integer weight)的构造函数引用
    Function<Integer, Apple> c2 = Apple::new;
    // 调用该Function函数的apply方法,并给出要求的重量,将产生一个Apple
    Apple a2 = c2.apply(110);

这就等价于:

    // 用要求的重量创建一个Apple的Lambda表达式
    Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
    // 调用该Function函数的apply方法,并给出要求的重量,将产生一个新的Apple对象
    Apple a2 = c2.apply(110);

在下面的代码中,一个由Integer构成的List中的每个元素都通过map方法传递给了Apple的构造函数,得到了一个具有不同重量苹果的List:

    List<Integer> weights = Arrays.asList(7, 3, 4, 10);
    List<Apple> apples = map(weights, Apple::new);
    
    public static List<Apple> map(List<Integer> list, Function<Integer, Apple> f) {
        List<Apple> result = new ArrayList<>();
        for(Integer e: list) {
            result.add(f.apply(e));
        }
        return result;
    }

如果有一个具有两个参数的构造函数Apple(String color, Integer weight),那么它就适合BiFunction接口的签名,可以这样写:

    BiFunction<String, Integer, Apple> c3 = Apple::new;
    Apple c3 = c3.apply("green", 110);

这就等价于:

    BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight);
    Apple c3 = c3.apply("green", 110);

Lambda 和方法引用实战

下面我们使用不同的方式对List<Apple>进行排序,并展示如何把一个原始粗暴的解决方案转变得更为简明,这会用到迄今讲到的所有概念和功能:行为参数化、匿名类、 Lambda表达式和方法引用。我们想要实现的最终解决方案是这样的:

    inventory.sort(comparing(Apple::getWeight));

第 1 步:传递代码

Java 8的API已经提供了一个List可用的sort方法,不用自己去实现它:

    public class AppleComparator implements Comparator<Apple> {
        public int compare(Apple a1, Apple a2){
            return a1.getWeight().compareTo(a2.getWeight());
        }
    }
    inventory.sort(new AppleComparator());

第 2 步:使用匿名类

    inventory.sort(new Comparator<Apple>() {
        public int compare(Apple a1, Apple a2){
            return a1.getWeight().compareTo(a2.getWeight());
        }
    });

第 3 步:使用 Lambda 表达式

    inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

由于Java编译器可以根据Lambda出现的上下文来推断Lambda表达式参数的类型,于是可以将Lambda简化成这样:

    inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));

Comparator具有一个叫作comparing的静态辅助方法,它可以接受一个Function来提取Comparable键值,并生成一个Comparator对象。它可以像下面这样用(注意你现在传递的Lambda只有一个参数: Lambda说明了如何从苹果中提取需要比较的键值):

    Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());

现在可以把代码再改得紧凑一点了:

    inventory.sort(comparing((a) -> a.getWeight())); // 注意静态引用

第 4 步:使用方法引用

前面解释过,方法引用就是替代那些转发参数的Lambda表达式的语法糖。可以用方法引用让代码更简洁(假设你静态导入了java.util.Comparator.comparing):

    inventory.sort(comparing(Apple::getWeight));

复合 Lambda 表达式

可以把多个简单的Lambda复合成复杂的表达式。比如,可以让两个谓词之间做一个or操作,组合成一个更大的谓词;再比如,可以让一个函数的结果成为另一个函数的输入。

比较器复合

我们前面看到,可以使用静态方法Comparator.comparing,根据提取用于比较的键值的Function来返回一个Comparator,如下所示:

    Comparator<Apple> c = Comparator.comparing(Apple::getWeight);

如果你想要对苹果按重量递减排序怎么办?用不着去建立另一个Comparator的实例。接口有一个默认方法reversed可以使给定的比较器逆序。因此仍然用开始的那个比较器,只要修改一下前一个例子就可以对苹果按重量递减排序:

    inventory.sort(comparing(Apple::getWeight).reversed());

如果两个苹果一样重呢?这样的话我们可能需要再按照另一个属性(比如是否国产)再次进行排序,thenComparing方法就是做这个用的。它接受一个函数作为参数(就像comparing方法一样),如果两个对象用第一个Comparator比较之后是一样的,就提供第二个Comparator:

    inventory.sort(comparing(Apple::getWeight)
        .reversed()  // 按重量递减排序
        .thenComparing(Apple::getCountry)); // 两个苹果一样重时按照国家排序

谓词复合

谓词接口包括三个方法: negate、 and和or,让你可以重用已有的Predicate来创建更复杂的谓词。比如,你可以使用negate方法来返回一个Predicate的非,比如苹果不是红的:

    Predicate<Apple> notRedApple = redApple.negate(); // 产生现有Predicate对象redApple的非

可以把两个Lambda用and方法组合起来,比如一个苹果既是红色又比较重:

    // 链接两个谓词来生成另一个Predicate对象
    Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);

可以进一步组合谓词,表达要么是重(150克以上)的红苹果,要么是绿苹果:

    // 链接Predicate的方法来构造更复杂Predicate对象
    Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150).or(a -> "green".equals(a.getColor()));

注意, and和or方法是按照在表达式链中的位置,从左向右确定优先级的。因此, a.or(b).and(c)可以看作(a || b) && c。

函数复合

最后,我们还可以把Function接口所代表的Lambda表达式复合起来。 Function接口为此配了andThen和compose两个默认方法,它们都会返回Function的一个实例。andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。比如,假设有一个函数f给数字加1 (x -> x + 1),另一个函数g给数字乘2,你可以将它们组合成一个函数h,先给数字加1,再给结果乘2:

    Function<Integer, Integer> f = x -> x + 1;
    Function<Integer, Integer> g = x -> x * 2;
    Function<Integer, Integer> h = f.andThen(g);
    int result = h.apply(1); // 结果是4

类似地,也可以使用compose方法,先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。比如在上一个例子里用compose的话,它将意味着f(g(x)),而andThen则意味着g(f(x)):

    Function<Integer, Integer> f = x -> x + 1;
    Function<Integer, Integer> g = x -> x * 2;
    Function<Integer, Integer> h = f.compose(g);
    int result = h.apply(1); // 结果是3

 

  • 27
    点赞
  • 121
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值