Java8特性之Lambda表达式

标签: Java

保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/13/ )。

1 概述

Java 8由Oracle公司于2014年3月18日发布,至今已过去数年之久。然而,直到今日仍有许多软件开发者对其相关特性不了解,这可能主要是Java教材更新缓慢的原因。为了使大家对与Java8的特性有全面系统的了解,本公众号将连续几篇文章介绍Java8中的各个特性。

Java8中新增的特性主要有:

本文介绍其中的Lambda表达式。其它特性会在后续文章中陆续介绍。

2 Lambda表达式

经常听到一个概念:闭包。闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

其实,就是Lambda表达式,Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。

2.1 出现原因

面向对象式编程就应该纯粹的面向对象,于是经常看到这样的写法: 如果你想写一个方法,那么就必须把它放到一个类里面,然后new出来对象,对象调用这个方法。最大的问题就在于其冗余的语法,有人戏称匿名类型导致了“高度问题”(height problem): 大多匿名内部类的多行代码中仅有一行在做实际工作。因此JAVA8中就提供了这种“函数式编程”的方法 —— lambda表达式,供我们来更加简明扼要的实现内部匿名类的功能。

也就是说,你在某处就真的只需要一个能做一件事情的函数而已,连它叫什么名字都无关紧要。 Lambda 表达式就可以用来做这件事。

2.2 语法介绍

2.2.1 函数式接口

函数式接口(Functional Interface):定义的一个接口,接口里面必须 有且只有一个抽象方法(可以有默认方法和静态方法) ,这样的接口就成为函数式接口。在可以使用lambda表达式的地方,方法声明时必须包含一个函数式的接口。 

如果我们提供的这个接口包含一个以上的Abstract Method,那么使用lambda表达式则会报错。 因为这不是函数式接口。

例如:

@FunctionalInterface
interface MathOperation {
    int operation(int a, int b);
}

Java 8为函数式接口引入了一个新注解@FunctionalInterface,主要用于编译级错误检查,加上该注解,当你写的接口不符合函数式接口定义的时候,编译器会报错。你不加的话,就不做检查。

注意点: * 函数式接口中可以额外定义多个Object的public方法一样抽象方法:接口最终有确定的类实现, 而类的最终父类是Object。 因此函数式接口可以定义Object的public方法。 * 函数式接口的抽象方法可以声明 可检查异常(checked exception)。 在调用目标对象的这个方法时必须catch这个异常。 * 函数式接口中除了那个抽象方法外还可以包含静态方法、默认方法。

任何函数式接口都可以使用lambda表达式替换。 例如系统已经有的:ActionListener、Comparator、Runnable。JDK 8之前已有的函数式接口:

java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.nio.file.PathMatcher
java.lang.reflect.InvocationHandler
java.beans.PropertyChangeListener
java.awt.event.ActionListener
javax.swing.event.ChangeListener

Java SE 8中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如:

Predicate<T>——接收T对象并返回boolean
Consumer<T>——接收T对象,不返回值
Function<T, R>——接收T对象,返回R对象
Supplier<T>——提供T对象(例如工厂),不接收值
UnaryOperator<T>——接收T对象,返回T对象
BinaryOperator<T>——接收两个T对象,返回T对象

另外,还有好多好多的接口,可以到具体的java.util.function包下去看一下。

那么在参数为这些接口的地方,我们就可以直接使用lambda表达式了!

2.2.2 Lambda表达式

(Type1 param1, Type2 param2, ..., TypeN paramN) -> {
  statment1;
  statment2;
  //.............
  return statmentM;
}

Lambda 表达式的简单例子:

// 1. 不需要参数,返回值为 5  
() -> 5  
  
// 2. 接收一个参数(数字类型),返回其2倍的值  
x -> 2 * x  
  
// 3. 接受2个参数(数字),并返回他们的差值  
(x, y) -> x – y  
  
// 4. 接收2个int型整数,返回他们的和  
(int x, int y) -> x + y  
  
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)  
(String s) -> System.out.print(s)

2.3 特性与注意点

2.3.1 lambda表达式中的this概念

在lambda中,this不是指向lambda表达式产生的那个SAM对象,而是声明它的外部对象。

不举例子了,是真的。

2.3.2 类型推导

先上个小例子举例:

public class TestBean {
    public static void main(String[] args) {
        MathOperation addition = (a, b) -> a + b;

        Integer ans = addition.operation(7,9);
        System.out.println(ans.toString());
    }

    interface MathOperation {
        int operation(int a, int b);
    }
}

编译器负责推导lambda表达式的类型。它利用lambda表达式所在上下文所期待的类型进行推导, 这个被期待的类型被称为目标类型。就是说我们传入的参数可以无需写类型了!

因此,在定义函数时:

MathOperation addition = (a, b) -> a + b;

我们并没有在入参里说明入参a,b的类型,因为如果没有类型推导,得这么写:

(int a, int b) -> a + b;

2.3.3 函数嵌套

我们知道,系统已经帮我们创建了几个函数时接口,例如:

Function<T, R>——接收T对象,返回R对象

它的代码如下:

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

它内置了两个写出了默认实现的方法,基于它们,可以实现函数的嵌套。

public class TestBean {
    public static void main(String[] args) {
        Function<String, String> fun1 = input -> input + ">";
        Function<String, String> fun2 = input -> input + "+";

        System.out.println(fun1.apply("a"));
        System.out.println(fun2.apply("a"));
        System.out.println(fun1.compose(fun2).apply("b"));
        System.out.println(fun1.andThen(fun2).apply("c"));
    }
}
---
a>
a+
b+>
c>+

首先,这是一个函数式接口,真正的抽象函数只有一个。

接下来我们看一下,compose() 和 andThen() 这两个默认实现为什么能够实现级联操作:首先我们定义了两个,分别是fun1和fun2。那,对于fun1.apply(“a”)操作,我们就是将“a”作为参数,传给函数fun1,然后fun1把它执行掉,故输出“a>”。

我们着重要分析的是fun1.compose(fun2).apply("b")。分析compose函数,before=fun2。因此,(V v) -> apply(before.apply(v))最终变为:(V v) -> apply(fun2.apply(v)),即为:fun1.apply(fun2.apply(v)),因此是v先给fun2处理,得到“b+”;然后“b+”传给fun1,得到“b+>”。因此,实现了两个函数的嵌套。

同理,fun1.andThen(fun2).apply("c")after=fun2,因此(T t) -> after.apply(apply(t));即为:fun2.apply(apply(t))apply(t)是针对于fun1的,因此是fun1处理完完后又fun2处理。

不过,这两个方法是系统内置的,会用就可以了。

3 总结

Java8 提供的Lambda表达式使得Java程序更为便捷、灵活,不过在使用Lambda表达式时也要注意相关指向问题,防止迷失。

关于Java8中Lambda表达式的介绍就到这里,在后续的文章中我们将介绍Java8中的其它特性。

本文首发于个人知乎:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。

作者书籍推荐