Dabai的个人博客

Java 8 新特性:lambda表达式

lambda表达式是Java8带给我们的几个重量级特性之一,借用lambda表达式,可以让我们的Java程序设计更加简洁和高效。要深入理解lambda表达式,需要理解函数式接口,lambda表达式的表示形式,以及方法引用。

1. 函数式接口

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

函数式接口可以用@FunctionalInterface注解,可以把它放在注解的前面,但它是非必须的,使用注解只是为了方便编译器作语法检查。只要接口只包含一个抽象方法,虚拟机会自动判断。在接口中添加了 @FunctionalInterface 的注解后,该接口就只允许有一个抽象方法,否则编译器也会报错。如下,java.lang.Runnable 就是一个函数式接口:

1
2
3
4
@FunctionalInterface
public interface Runnable {
public abstract void run();
}

函数式接口的重要属性是:我们能够使用 lambda 实例化它们,lambda 表达式让你能够将函数作为方法参数,或者将代码作为数据对待。

2. lambda表达式

lambda表达式是一种紧凑的传递行为的方式。lambda表达式由三部分组成:第一部分为一个括号内用逗号分隔的形式参数,参数是函数式接口里面方法的参数;第二部分为一个箭头符号:->;第三部分为方法体,可以是表达式和代码块。语法如下:

  • 方法体为表达式,该表达式的值作为返回值返回

    1
    (parameters) -> expression

    例如:

    1
    Supplier<String> i  = ()-> "Supplier test";

    这里,”Supplier test”就是lambda表达式i的get方法返回值。

  • 方法体为代码块,必须用 {} 来包裹起来,且需要一个 return 返回值,但若函数式接口里面方法返回值是 void,则无需返回值。

    1
    (parameters) -> { statements; }

    例如上面的例子的等价形式为:

    1
    Supplier<String> i  = ()-> {return  "Supplier test";};

    下面是匿名内部类的代码:

    1
    2
    3
    4
    5
    6
    button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
    System.out.print("Hello lambda in actionPerformed");
    }
    });

    下面是使用lambda表达式后的代码:

    1
    2
    3
    4
    button.addActionListener(
    //actionPerformed 有一个参数 e 传入,所以用 (ActionEvent e)
    (ActionEvent e)-> System.out.print("Hello lambda in actionPerformed")
    );

    其实,lambda表达式可以自己根据上下文推断参数类型,无需显示指定:

    1
    button.addActionListener( e -> System.out.print("Hello lambda in actionPerformed"));
  • lambda表达式的几种变体
    将lambda表达式赋值给一个一个变量,lambda表达式没有参数:

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

    将lambda表达式赋值给一个一个变量,lambda表达式的参数类型由编译器推导出来:

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

    将lambda表达式赋值给一个一个变量,lambda表达式有多个参数类型,参数类型由编译器推导出来:

    1
    BinaryOperator<Long> add = (x, y) -> x + y; //(3)

    将lambda表达式赋值给一个一个变量,lambda表达式有多个参数类型,显示声明lambda表达式类型:

    1
    BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y; //(4)

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

    注意:

    目标类型是指lambda表达式所在上下文环境的类型。比如,将lambda表达式赋值给一个局部变量,或传递给一个方法作为参数,局部变量或方法参数的类型就是lambda表达式的目标类型。lambda表达式的目标类型类型依赖于上下文环境,是由编译器推断出来的。如(1)和(2)的lambda表达式的目标类型分别是Runnable和ActionListener类型,可以把lambda表达式看作实现了该接口的内部类的实例,noArguments和oneArgument分表表示指向实例的引用。

  • lambda表达式能引用表达式之外定义的既成事实的final变量。虽然无需将变量声明为final,但在lambda表达式中,也无法用作非终态变量。下面的可以编译通过:

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

    但下面不能编译通过:

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

    也就是说,lambda只能引用表达式之外不会改变的变量,之所以有这样的限制,是因为若变量可以改变,并发执行多个lambda表达式时就会不安全。

3. 方法引用

可以用已经定义好的类中的方法来表示一个lambda表达式。例如:

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

可以使用System.out::println的方法引用表示成如下形式:

1
button.addActionListener(System.out::println);

总的来说,方法引用有如下四种使用情况:

  1. 对象::实例方法
  2. 类::静态方法
  3. 类::实例方法
  4. 类::new

在前两种情况,方法引用等同于提供方法参数的lambda表达式。如System.out::println等同于x -> System.out.println(x)。类似的,Math::pow等同于(x, y) -> Math.pow(x, y)。

第三种情况,第一个参数为执行方法的对象,即当lambda表达式的的第一个参数是要执行的方法体所属的对象时,可以使用类::实例方法的方法引用代替。例如:String::compareToIgnoreCase等同于(x, y) -> x.compareToIgnoreCase(y),这里x就是compareToIgnoreCase方法所属对象。其实这也是合理的,因为Java中实例方法拥有者为一个具体的对象,只有一个对象才能调用实例方法。

下面是一个具体的引用类::实例方法的例子,其中personList.stream().forEach(x -> x.getName())和personList.stream().forEach(Person::getName)是等价的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.ArrayList;
import java.util.List;

public class Main {
public static void main(String[] args) {
String[] stringArray = { "Barbara", "James", "Mary", "John",
"Patricia", "Robert", "Michael", "Linda" };
//Arrays.sort(stringArray, String::compareToIgnoreCase);
List<Person> personList = new ArrayList<>();
for( String str : stringArray )
personList.add(new Person(str));
//personList.stream().forEach(x -> x.getName());
personList.stream().forEach(Person::getName);
}
}

class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
System.out.println(name);
return name;
}
}

第四种情况则是构造器的引用,对于拥有多个构造器的类,选择使用哪个引用取决于上下文。需要注意的是,虽然方法引用使用的是一个方法,但不需要在后面加括号,因为这里并不调用该方法。使用该方法只是提供了一种和lambda表达式等价的一种结构,在需要时才会调用。凡是使用lambda表达式的地方,就可以使用方法引用。

方法引用还可以使用this或super参数,表示引用本类或父类中的方法,例如:this::equals就等同于x -> this.equals(x)。

1
2
3
4
5
6
7
8
9
10
11
12
public class ConcurrentGreeter extends Greeter {
public void greet () {
Thread t = new Thread(super::greet);
t.start();
}
}

class Greeter {
public void greet() {
System.out.println("Hello, world");
}
}

该例子中,相当于引用父类的方法,实现了一个这样的lambda表达式,返回类型为Runnable:

1
Thread t = new Thread(() -> System.out.println("Hello, world"));

4. 什么时候使用Lambda表达式

函数式接口是lambda表达式的类型,因此,若函数的形参传递的是一个函数式接口类型的引用,则可以直接给该形参传递lambda表达式。当然,也可以给该形参传递实现该接口的匿名类对象或实例。下面的三种方法是等效的,当然,使用lambda表达式是最简洁的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.Arrays;
import java.util.function.Consumer;

public class Test {
public static void main(String[] args) {
List<String> list = Arrays.asList("bay", "max", "huang");
list.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
}); // 1: object of an anonymous class
list.forEach(new InternalConsumer()); // 2: normal object
list.forEach(System.out::println); // 3: lambda
}

private static class InternalConsumer implements Consumer<String> {
@Override
public void accept(String s) {
System.out.println(s);
}
}
}

参考:

Method References

Java Lambda Introduction

Java 8函数式编程