进入Java8,从Lambda开始

Java语言从诞生开始,除了Java5引入的注解、泛型等特性,它的变化其实一直都不算太大。相信从你最初接触Java起,就会有人告诉你,Java是一门纯面向对象的语言,所有的属性和方法都需要封装在对象中,不管你要做什么功能,都必须定义一个Class类,然后new一个对象,接着才能通过这个对象完成自己想要做的事。然而,正当你每天面对着这些重复乏味的工作感觉到厌烦时,Java8出现了,它带来的Lambda表达式,让你第一次体会到,Java的世界突然变的跟以往完全不同。现在,就让我们来一起看看这个改变世界的东西,到底长什么样吧。

第一个Lambda表达式

开发过GUI编程的同学一定都知道,当我们在界面中加入一个按钮,然后当这个按钮被人点击时,我们需要对这个点击事件做出相应的响应。Java8之前我们通常会像下面这样操作:

1// 定义一个按钮
2Button button = new Button();
3// 使用匿名内部类,增加事件监听
4button.addActionListener(new ActionListener() {
5    @Override
6    public void actionPerformed(ActionEvent e) {
7        System.out.println("你点击了这个按钮");
8    }
9});

就像我最开始说的那样,在以往的Java世界中,一切事物都离不开对象。当我们需要对button增加事件监听时,我们必须通过匿名内部类的形式new出一个ActionListener对象,将它传入button的监听方法中。然而,我们现在仅仅只是想在button被点击时,根据这个点击事件,打印一句话,告诉对方“你点击了这个按钮”,就是这样一个简单的需求,我们就需要写如此多无用的样板代码,难道就不能简单点,我想做什么就只写什么吗???

欢迎来到Java8的世界,接下来,就是见证奇迹的时刻:

1// 使用Lambda表达式,增加事件监听
2button.addActionListener(e -> System.out.println("你点击了这个按钮"));

是的,你没有看错,在Java8的世界中,仅仅只需要一行代码,就实现了和上面4行代码同样的功能。不需要new无用的对象,仅仅去写你想做的东西,例如:你想打印一句话,那就只打印它就够了,顿时,整个世界都变得如此清爽。接下来,就让我们去真正的理解什么是Lambda表达式吧。

什么是Lambda表达式

简单点来说,Lambda表达式就是一种可传递的匿名函数。

  • 匿名:它不像普通方法那样有一个方法名。
  • 函数:Lambda函数不像普通方法那样属于某个特定的类,但和方法一样,Lambda有参数列表、函数主题、返回类型,还可能有可以抛出的异常列表。
  • 传递:Lambda表达式可以作为参数传递给方法或存储在变量中。

Lambda表达式有三个部分组成,参数列表、箭头、Lambda主体。
它的基本语法是:
(parameters) -> expression
或(多了花括号)
(parameters) -> { statements;}

接下来,我们列举几个简单的Lambda表达式来进行说明:

1() -> {} //这是一个有效的Lambda表达式,它没有参数,并返回void。它类似于主体为空的方法:public void run(){}。
2() -> "Hello Java 8" //这是一个有效的Lambda表达式,它没有参数,并返回String。
3() -> {return "Hello Java 8";} //这是一个有效的Lambda表达式,它没有参数,并返回String(利用显式返回语句)。
4(String name) -> return "Hello" + name; // 这是一个无效的Lambda表达式,return是一个控制流语句,需要使用花括号,如:(String name) -> {return "Hello" + name;}
5(String name) -> {"Hello World";} //这是一个无效的Lambda表达式,“Hello World”是一个表达式,不是一个语句。要让该Lambda有效,可以去掉花括号,或者像上一个例子一样,使用return显式的返回。

如何使用Lambda表达式

在Java中,所有的变量和方法参数都有固定的类型,前面我们说到,Lambda表达式也可以赋值给一个变量,或是作为参数传递给方法,那么,Lambda表达式应该是什么类型呢?

答案就是,函数式接口。简单点来说,函数式接口就是只定义了一个抽象方法的接口,用作Lambda表达式的类型。

注意:从Java8开始,接口中还可以定义默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,但只要接口中只定义了一个抽象方法,它就仍然是一个函数式接口。

例如,原本的Java API中的一些接口就是函数式接口:

 1// java.util.Comparator
2public interface Comparator<T{
3    int compare(T o1, T o2);
4}
5
6// java.lang.Runnable
7public interface Runnable{
8    void run();
9}
10
11// java.awt.event.ActionListener
12public interface ActionListener extends EventListener{
13    void actionPerformed(ActionEvent e);
14}
15
16// java.util.concurrent.Callable
17public interface Callable{
18    call();
19}

既然知道了Lambda表达式的类型就是函数式接口,那接下来我们就可以在用到函数式接口的地方去使用Lambda表达式了。

 1public static void process(Runnable r) {
2    r.run();
3}
4
5public static void main(String[] args) {
6    // 使用Lambda
7    Runnable r1 = () -> System.out.println("Hello Java 8");
8    // 当然也可以使用匿名内部类来实现
9    Runnable r2 = new Runnable() {
10        @Override
11        public void run() {
12            System.out.println("Hello world");
13        }
14    };
15
16    process(r1); // 打印 Hello Java 8
17    process(r2); // 打印 Hello world
18    process(() -> System.out.println("Hello Lambda")); // 直接传递Lambda表达式作为参数,打印 Hello Lambda
19}

函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符。既然定义一个函数式接口这么简单,当然我们也就可以根据自己的需要,定义自己的函数式接口,并将它用作方法参数,从而在调用方法时,使用Lambda表达式。然而,为了在不同的地方应用不同的Lambda表达式,Java8的库设计师,早就已经帮我们定义好了很多常用的函数式接口,它们都放在java.util.function包中(我也将会单独用一篇文章来说明这些常用的函数式接口),只有当里面的接口满足不了我们的需求时,我们才有必要定义自己的函数式接口。

注意:当你去看Java8中这些新的API时,会发现它们都使用了一个@FunctionalInterface注解。这个注解表示该接口会设计成一个函数式接口,如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。但@FunctionalInterface并不是必须的,它就像是@Override注解用于表示方法被重写了。

类型推断

Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。例如:

1Callable<Integer> c = () -> 42// Callable<Integer>就是该Lambda表达式的目标类型

而当同一个Lambda表达式可以兼容多个抽象方法签名时,虽然它代表的目标类型不同,但是它却可以用于多个不同的函数式接口。例如,下面的同一个Lambda表达式(String s1, String s2) -> s1.length() - s2.length()就可以用于以下三个函数式接口:

1Comparator<String> c1 = (String s1, String s2) -> s1.length() - s2.length(); // 目标类型是Comparator<String>
2
3ToIntBiFunction<String, String> c2 = (String s1, String s2) -> s1.length() - s2.length(); // 目标类型是ToIntBiFunction<String, String>
4
5BiFunction<String, String, Integer> c3 = (String s1, String s2) -> s1.length() - s2.length(); // 目标类型是BiFunction<String, String, Integer>

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

1Comparator<String> c4 = (String s1, String s2) -> s1.length() - s2.length(); // 没有类型推断
2
3Comparator<String> c5 = (s1, s2) -> s1.length() - s2.length(); // 有类型推断
4
5Predicate<String> c6 = s -> s.length() > 0;// 当只有一个参数时,参数两边的括号也可以省略

使用局部变量

如果你使用过匿名内部类,你就应该会知道,当它需要引用其所在方法的局部变量时,需要将该局部变量声明为final。

1final String name = getUserName(); //匿名内部类中引用该变量必须为final
2button.addActionListener(new ActionListener() {
3    @Override
4    public void actionPerformed(ActionEvent e) {
5        System.out.println("Hello " + name);
6    }
7});

在Lambda表达式中,虽然不需要强制将变量声明为final,但在既成事实上,该变量也必须是final,如果你试图将该变量多次赋值,然后在Lambda表达式中引用它,编译器就会报错。

既成事实上的final是指只能给该变量赋值一次。换句话说,Lambda表达式引用的是值,而不是变量。

1String name = getUserName(); //Lambda表达式中,不用强制声明,但其事实上也必须是final
2button.addActionListener(e -> System.out.println("Hello " + name));
3//name = "new name";// 放开该行注释,Lambda中的name将会编译报错

为什么会有这种限制?
第一,因为局部变量是保存在栈上,如果Lambda可以直接访问局部变量,而且Lambda是在一个异步线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。
第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(这种模式会阻碍很容易做到的并行处理)。

方法引用

方法引用是Java8中新加的另一个功能,可以把它们视为某些Lambda的快捷写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。很多时候,显示地指明方法的名称,你的代码的可读性会更好。方法引用的写法,目标引用::方法名称,,方法名后面不需要括号,因为你没有实际的调用这个方法。例如:方法引用String::length就是引用了String类定义的方法length,其就是Lambda表达式(String s) -> s.length()的快捷写法。

有三种方式可以构建方法引用:

  1. 指向静态方法的方法引用:Integer::parseInt
  2. 指向任意类型实例方法的方法引用:String::length
  3. 指向现有对象的实例方法的方法引用:假如你有一个局部变量person,在它的类中定义了一个实例方法getName,那么你就可以写person::getName

此外,还有针对构造函数、数组构造函数和父类调用的一些特殊形式的方法引用。例如,对于一个现有的构造函数,利用它的名称和关键字new来创建它的一个引用:ClassName::new

1Supplier<Person> c = Person::new//构造函数引用指向默认的Person()构造函数
2Person p c.get(); //调用Supplier的get方法将产生一个新的Person对象

上面的例子就等价于:

1Supplier<Person> c = new Person(); //利用默认构造函数创建Person的Lambda表达式
2Person p c.get(); //调用Supplier的get方法将产生一个新的Person对象

结束语

Lambda表达式的基本用法就暂时介绍到这里,在本篇文章中主要介绍了什么是Lambda表达式,可以在什么时候使用它,如何通过类型推断来简化Lambda表达式的写法,在Lambda中引用局部变量的注意点,以及一种Lambda的快捷写法方法引用。在下一篇文章中,我会介绍Java8 API中自带的几个常用的函数式接口,并且通过一个实例来说明如何去使用它们,当你学会了使用Lambda表达式,从此以后你将会进入一个全新的Java世界,Good Luck。

本文案例源码:

https://github.com/lanheixingkong/HelloJava8

参考资料

  • 《Java 8实战》
  • 《Java 8函数式编程》

ps:才刚刚开始写了第一遍文章,就让我感受到了读书十分钟,打字十小时的巨大差距。平时看书的时候,只要理解书中写的内容,工作中需要用到的时候再现查找来使用就行。可当现在需要将书中的内容,根据自己的理解重新归纳总结,再通过自己的语言重新描述一遍时,才发现这个过程真的很难,以至于文章中的很多内容,我都只能照搬书中的原话,由此更加钦佩能够写出这些书籍的大牛们,如果你的时间充足,也推荐你可以看一遍上面的两本书,这样可以帮助你更好的理解Lambda表达式。同时,也对我自己定的一年计划小小的捏了一把汗 -_-!!!,既然开始了,就只能硬着头皮刚把得了。


0条留言

留言