Java 8的最大变化是引入了Lambda表达式——一种紧凑的、传递行为的方式。它也是本书后续章节所述内容赖以依存的基础,因此,接下来就了解一下什么是Lambda表达式。

2.1 第一个Lambda表达式

Swing是一个与平台无关的Java类库,用来编写图形用户界面(GUI)。该类库有一个常见用法:为了响应用户操作,需要注册一个事件监听器。一旦用户输入,监听器会执行一些操作。(见例2-1)。

例2-1 使用匿名内部类将行为和按钮单击进行关联

button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        System.out.println("button clicked");
    }
});

在这个例子中,我们创建了一个新对象,该对象实现了ActionListener接口。这个接口只有一个方法:actionPerformed,当用户点击屏幕上的按钮时,该方法就会被button调用。匿名内部类实现了该方法。在例2-1中该方法所执行的只是打印出一条信息,表明按钮已被点击。

这实际上是一个代码即数据的例子——我们给按钮传递了一个代表某种行为的对象。

设计匿名内部类的目的,就是为了方便Java程序员将代码作为数据传递。不过,匿名内部类还是不够简便。为了调用一行重要的逻辑代码,不得不加上四行冗繁的样板代码。若把样板代码用其他颜色区分开来,就可一目了然:

button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        System.out.println("button clicked");
    }
});

尽管如此,冗余的样板代码并不是唯一的问题:这些代码还相当难读,因为它没有清楚地表达程序员的意图。我们不想传入对象,只想传入行为。在Java 8中,上述代码可以写成一个Lambda表达式,如例2-2所示。

例2-2 使用Lambda表达式将行为和按钮单击进行关联

button.addActionListener(event -> System.out.println("button clicked"));

和传入一个实现某接口的对象不同,我们传入了一段代码块——一个没有名字的函数。event是参数名,和上面匿名内部类示例中的是同一个参数。->将参数和Lambda表达式的主体分开,而主体是用户点击按钮时会运行的一些代码。

和使用匿名内部类的另一处不同在于声明event参数的方式。使用匿名内部类时需要显式地声明参数类型ActionEvent event,而在Lambda表达式中无需指定类型,程序依然可以编译。这是因为javac根据程序的上下文(addActionListener方法的签名)在后台推断出了参数event的类型。这意味着如果参数类型不言而明,则无需显式指定。稍后会介绍类型推断的更多细节,现在先来看看编写Lambda表达式的各种方式。

尽管与之前相比,Lambda表达式中的参数需要很少的样板代码,但是Java 8仍然是一种静态类型语言。为了增加可读性并迁就我们的习惯,声明参数时也可以包括类型信息,而且有时编译器不一定能根据上下文推断出参数的类型!

2.2 如何辨别Lambda表达式

Lambda表达式除了基本的形式之外,还有几种变体,如例2-3所示。

例2-3 编写Lambda表达式的不同形式

Runnable noArguments = () -> System.out.println("Hello World");➊

ActionListener oneArgument = event -> System.out.println("button clicked");➋

Runnable multiStatement = () -> {➌
    System.out.print("Hello");
    System.out.println(" World");
};

BinaryOperator<Long> add = (x, y) -> x + y;➍ 

BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;➎

➊中所示的Lambda表达式不包含参数,使用空括号()表示没有参数。该Lambda表达式实现了Runnable接口,该接口也只有一个run方法,没有参数,且返回类型为void

➋中所示的Lambda表达式包含且只包含一个参数,可省略参数的括号,这和例2-2中的形式一样。

Lambda表达式的主体不仅可以是一个表达式,也可以是一段代码块,使用大括号({})将代码块括起来,如➌所示。该代码块和普通方法遵循的规则别无二致,可以用返回或抛出异常来退出。只有一行代码的Lambda表达式也可使用大括号,用以明确Lambda表达式从何处开始、到哪里结束。

Lambda表达式也可以表示包含多个参数的方法,如➍所示。这时就有必要思考怎样去阅读该Lambda表达式。这行代码并不是将两个数字相加,而是创建了一个函数,用来计算两个数字相加的结果。变量add的类型是BinaryOperator<Long>,它不是两个数字的和,而是将两个数字相加的那行代码。

到目前为止,所有Lambda表达式中的参数类型都是由编译器推断得出的。这当然不错,但有时最好也可以显式声明参数类型,此时就需要使用小括号将参数括起来,多个参数的情况也是如此。如➎所示。

目标类型是指Lambda表达式所在上下文环境的类型。比如,将Lambda表达式赋值给一个局部变量,或传递给一个方法作为参数,局部变量或方法参数的类型就是Lambda表达式的目标类型。

上述例子还隐含了另外一层意思:Lambda表达式的类型依赖于上下文环境,是由编译器推断出来的。目标类型也不是一个全新的概念。如例2-4所示,Java中初始化数组时,数组的类型就是根据上下文推断出来的。另一个常见的例子是null,只有将null赋值给一个变量,才能知道它的类型。

例2-4 等号右边的代码并没有声明类型,系统根据上下文推断出类型信息

final String[] array = { "hello", "world" };

2.3 引用值,而不是变量

如果你曾使用过匿名内部类,也许遇到过需要引用它所在方法里的变量的情况。这个时候,需要将变量声明为final,如例2-5所示。将变量声明为final,意味着不能为其重复赋值。同时也意味着在使用final变量时,实际上是在使用赋给该变量的一个特定的值。

例2-5 匿名内部类中使用final局部变量

final String name = getUserName();
button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        System.out.println("hi " + name);
    }
});

Java 8虽然放松了这一限制,可以引用非final变量,但是该变量在既成事实上必须是final。虽然无需将变量声明为final,但在Lambda表达式中,也无法用作非终态变量。如果坚持用作非终态变量,编译器就会报错。

既成事实上的final是指只能给该变量赋值一次。换句话说,Lambda表达式引用的是,而不是变量。在例2-6中,name就是一个既成事实上的final变量。

例2-6 Lambda表达式中引用既成事实上的final变量

String name = getUserName();
button.addActionListener(event -> System.out.println("hi " + name));

final就像代码中的线路噪声,省去之后代码更易读。当然,有些情况下,显式地使用final代码更易懂。是否使用这种既成事实上的final变量,完全取决于个人喜好。

如果你试图给该变量多次赋值,然后在Lambda表达式中引用它,编译器就会报错。比如,例2-7无法通过编译,并显示出错信息:local variables referenced from a Lambda expression must be final or effectively final{![Lambda表达式中引用的局部变量必须是final或既成事实上的final变量。——译者注]}。

例2-7 未使用既成事实上的final变量,导致无法通过编译

String name = getUserName();
name = formatUserName(name);
button.addActionListener(event -> System.out.println("hi " + name));

这种行为也解释了为什么Lambda表达式也被称为闭包。未赋值的变量被周边环境封闭起来,进而被绑定到一个特定的值。在众说纷纭的计算机编程语言圈子里,Java是否拥有真正的闭包一直备受争议,因为在Java中只能引用既成事实上的final变量。名字虽异,功能相同,就好比把菠萝叫做凤梨,其实都是同一种水果。为了避免无意义的争论,全书将使用“Lambda表达式”一词。无论名字如何,如前文所述,Lambda表达式都是静态类型的。因此,接下来就分析一下Lambda表达式本身的类型:函数接口

2.4 函数接口

函数接口是只有一个抽象方法的接口,用作Lambda表达式的类型。

在Java里,所有方法参数都有固定的类型。假设将数字3作为参数传给一个方法,则参数的类型是int。那么,Lambda表达式的类型又是什么呢?

使用单一方法的接口来表示某特定方法并反复使用,是很早就有的习惯。使用Swing编写过用户界面的人对这种方式都不陌生,例2-2中的用法也是如此。这里无需再标新立异,Lambda表达式也使用同样的技巧,并将这种接口称为函数接口。例2-8展示了前面例子中所用的函数接口。

例2-8 ActionListener接口:接受ActionEvent类型的参数,返回空

public interface ActionListener extends EventListener {
    public void actionPerformed(ActionEvent event);
}

ActionListener只有一个抽象方法:actionPerformed,被用来表示行为:接受一个参数,返回空。记住,由于actionPerformed定义在一个接口里,因此abstract关键字不是必需的。该接口也继承自一个不具有任何方法的父接口:EventListener

这就是函数接口,接口中单一方法的命名并不重要,只要方法签名和Lambda表达式的类型匹配即可。可在函数接口中为参数起一个有意义的名字,增加代码易读性,便于更透彻地理解参数的用途。

这里的函数接口接受一个ActionEvent类型的参数,返回空(void),但函数接口还可有其他形式。例如,函数接口可以接受两个参数,并返回一个值, 还可以使用泛型,这完全取决于你要干什么。

使用Java编程,总会遇到很多函数接口,但Java开发工具包(JDK)提供的一组核心函数接口会频繁出现。 前面已讲过函数接口接收的类型,也讲过javac可以根据上下文自动推断出参数的类型,且用户也可以手动声明参数类型,但何时需要手动声明呢?下面将对类型推断作出详尽说明。

2.5 类型推断

某些情况下,用户需要手动指明类型,建议大家根据自己或项目组的习惯,采用让代码最便于阅读的方法。有时候省略类型信息可以减少干扰,更易弄清状况;而有时却需要类型信息帮助理解代码。经验证发现,一开始类型信息是有用的,但随后可以只在真正需要时才加上类型信息。下面将介绍一些简单的规则,来帮助确认是否需要手动声明参数类型。

Lambda表达式中的类型推断,实际上是Java 7就引入的目标类型推断的扩展。读者可能已经知道Java 7中的菱形操作符,它可使javac推断出泛型参数的类型。参见例2-9。

例2-9 使用菱形操作符,根据变量类型做推断

Map<String, Integer> oldWordCounts = new HashMap<String, Integer>();➊
Map<String, Integer> diamondWordCounts = new HashMap<>();➋

我们为变量oldWordCounts➊明确指定了泛型的类型,而变量diamondWordCounts➋则使用了菱形操作符。不用明确声明泛型类型,编译器就可以自己推断出来,这就是它的神奇之处!

当然,这并不是什么魔法,根据变量diamondWordCounts➋的类型可以推断出HashMap的泛型类型,但用户仍需要声明变量的泛型类型。

如果将构造函数直接传递给一个方法,也可根据方法签名来推断类型。在例2-10中,我们传入了HashMap,根据方法签名已经可以推断出泛型的类型。

例2-10 使用菱形操作符,根据方法签名做推断

useHashmap(new HashMap<>());
...
private void useHashmap(Map<String, String> values);

Java 7中程序员可省略构造函数的泛型类型,Java 8更进一步,程序员可省略Lambda表达式中的所有参数类型。再强调一次,这并不神奇,javac根据Lambda表达式上下文信息就能推断出参数的正确类型。程序依然要经过类型检查来保证运行的安全性,但不用再明确声明类型罢了。这就是所谓的类型推断

Java 8中对类型推断系统的改善值得一提。上面的例子将new HashMap<>()传给useHashmap方法,即使编译器拥有足够的信息,也无法在Java 7中通过编译。

接下来将通过举例来详细分析类型推断。

例2-11和例2-12都将变量赋给一个函数接口,这样便于理解。第一个例子(例2-11)使用Lambda表达式检测一个Integer是否大于5。这实际上是一个Predicate——用来判断真假的函数接口。

例2-11 类型推断

Predicate<Integer> atLeast5 = x -> x > 5;

Predicate也是一个Lambda表达式,和前文中ActionListener不同的是,它还返回一个值。在例2-11中,表达式x > 5是Lambda表达式的主体。这样的情况下,返回值就是Lambda表达式主体的值。

例2-12 Predicate接口的源码,接受一个对象,返回一个布尔值

public interface Predicate<T> {
    boolean test(T t);
}

从例2-12中可以看出,Predicate只有一个泛型类型的参数,Integer用于其中。Lambda表达式实现了Predicate接口,因此它的单一参数被推断为Integer类型。javac还可检查Lambda表达式的返回值是不是boolean,这正是Predicate方法的返回类型。

例2-13是一个略显复杂的函数接口:BinaryOperator。该接口接受两个参数,返回一个值,参数和值的类型均相同。实例中所用的类型是Long

例2-13 略显复杂的的类型推断

BinaryOperator<Long> addLongs = (x, y) -> x + y;

类型推断系统相当智能,但若信息不够,类型推断系统也无能为力。类型系统不漫无边际地瞎猜,而会中止操作并报告编译错误,寻求帮助。比如,删掉例2-13中的某些类型信息,则如例2-14所示。

例2-14 没有泛型,代码则通不过编译

BinaryOperator add = (x, y) -> x + y;

编译器给出的报错信息如下:

Operator '& #x002B;' cannot be applied to java.lang.Object, java.lang.Object.

报错信息让人一头雾水,到底怎么回事?BinaryOperator毕竟是一个具有泛型参数的函数接口,该类型既是参数xy的类型,也是返回值的类型。上面的例子中并没有给出变量add的任何泛型信息,给出的正是原始类型的定义。因此,编译器认为参数和返回值都是java.lang.Object实例。

后文中讨论“重载解析”时还会讲到类型推断,但就目前来说,掌握以上类型推断的知识就已经足够了。

2.6 要点回顾

  • Lambda表达式是一个匿名方法,将行为像数据一样进行传递。
  • Lambda表达式的常见结构:BinaryOperator<Integer> add = (x, y) → x + y
  • 函数接口指仅具有单一抽象方法的接口,用来表示Lambda表达式的类型。