java8新特性之lambda表达式

Java9已经在今年9月正式发布了,作为一个java开发人员如果对Java8的新特性还不了解,可能需要跟进一下了,虽然从我大学开始学Java装的第一个jdk版本就是jdk1.8,但我却一直没用过java8的新特性,但是当我学会Java8的一些新特性之后,我感觉我可能离不开它们了,因为lambda表达式能给我提供很大的方便使得开发变得更加简洁高效。


正文

抛开题外话,网上有太多的java8新特性介绍及使用,本文主要总结一下Java8最主要的新特性lambda表达式和方法引用的使用,关于lambda表达式先总结为一下几点

  • lambda表达式可以实现函数式编程
  • lambda表达式将完全取代函数式接口的匿名内部类
  • lambda表达式并非匿名内部类的语法糖
  • lambda表达式可以使代码更简洁、高效

什么是函数式接口

函数式接口指的是一个接口中只有一个抽象方法,在1.8之前已经有很多函数式接口了,比如:Runable接口只有一个run()方法,Callable接口只有一个call()方法,Comparable接口只有一个compareTo(T o)方法,这些都是函数式接口,在jdk1.8中为了更好的支持lambda表达式还引入了一个java.util.function包,在这个包中提供了非常全面的一些函数式接口来供lambda表达式使用,这些接口一看类名就大概知道是用来做什么的,如下是其中一部分:
jdk1.8函数式接口
这些接口都提供一个抽象方法供lambda表达式使用,比如DoubleBinaryOperator一看名字就知道提供一个方法支持两个double类型的数据的运算,那么返回的也应该是一个double,所以该类中一定只有一个类似于double 方法名(double a, double b)这样的抽象方法,点开该接口:
这里写图片描述
其他的接口类似,如Consumer接口,Consumer表示消费者,其消费一个对象并且返回值为void,应该有一个void 方法名(T t) 这样的抽象方法,Function代表方法应该是有一个包含方法全部特征的方法包括返回值和参数,所有应该有一个R 方法名(T t) 这样的方法,至于方法名到底是什么,完全不用去关心,所以我们完全不用去记有哪些函数式接口,用到的时候查一下就行,这里面的接口已经能对付绝大多数情况的使用,如果实在遇到比较特殊的情况可以自己写一个接口。
关于自己写函数式接口需要注意的几点:

  • 函数式接口允许定义默认方法(前面一直强调抽象方法就是这个原因,关于默认方法我之前有一篇博客已经讲到 关于java8接口中默认方法的使用
  • 函数式接口允许定义静态方法
  • 函数式接口允许定义java.lang.Object中的public方法
  • 关于@FunctionalInterface注解:在jdk1.8提供的函数式接口中都使用了该注解,该注解的作用是确保注解的接口是规范的函数式接口,如果该接口不是规范的函数式接口(如接口中定义了两个抽象方法),则会报错

其实讲这么多只用记住一点:函数式接口就是只有一个抽象方法的就口

第一个lambda表达式

既然lambda表达式可以用来替换匿名内部类,那么来看看在jdk1.8之前如果要new一个线程去执行一个任务我们会怎么做:

1
2
3
4
5
6
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("do some thing ...");
}
}).start();

在jdk1.8之后使用lambda表达式我们会这么做:

1
new Thread(()->System.out.println("do some thing ...")).start();

一看上面的代码能清晰的感觉到lambda表达式的简洁性,在1.8之前,有时候我们明明只需要传入一个函数,但java语言不允许我们这么做,非要我们new一个匿名内部类,lambda完美的解决了这个问题,要让Thread执行任务其实我们只用传入run方法,并在方法中写逻辑就行了。lambda表达式的用法如下:

1
(int a, int b)->{do some thing...; return some thing ...}

  • 前面的括号里面表示方法的参数,没有参数的时候可以不写,如run方法没有参数,所以直接使用()->,当上下文能明确知道参数类型时,参数前面的类型可以省略,当只有一个参数时,可以省略括号。
  • 后面大括号里面表示方法体,当只有一条语时可以省略大括号,比如前面只有一条输出语句,就没有写大括号,只有一条语句时如果该方法有返回值不写return 直接写返回值,如:如果是call()方法,其有一个返回值,可以直接这样new Thread(()-> 1).start(); 表示call方法返回的是一个int型的1。

了解了lambda表达式的基本语法后来分析一下为什么jvm能理解lambda表达式,首先Thread类需要接受一个实现了Runnable接口的对象,而Runnable接口是个标准的函数式接口,该接口内只有一个run方法,所以当我们传入()->System.out.println("do some thing ...") 的时候,jvm发现我们传入的是Runnable中的run方法,->后面的内容是该方法要做的事。如果理解困难可以把它理解成匿名内部类,任何lambda表达式都可以用匿名内部类来实现,但是需要注意的是,正如前面所说,lambda表达式并非匿名内部类的语法糖,jvm不会把lambda表达式翻译成匿名内部类来执行,lambda表达式的性能比匿名内部类高很多,这在之后会详细讲解

lambda表达式的应用

为了更熟练的使用lambda表达式,我们通过一个简单的场景来练习lambda表达式的使用:
有以下场景:老王需要从北京老家去上海出差。。
可以抽象出一个Person类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Person {

private String name;

public void go(Movable movable, String address){
System.out.println(name+"带着行李从北京的老家出门了");
if (movable.moveTo(address)) {
System.out.println(name + "到达了目的地!");
} else {
System.out.println(name + "没有到达目的地!");
}

}
...
}

该类有一个name属性表示老王的名字,一个go方法表示老王怎么去的上海,但是具体怎么去上海不在这里写死,是由Movable的moveTo方法来决定,address表示要去的地址
Movable接口只定义了一个moveTo方法来表示如何到达目的地,这是一个标准的函数式接口,接受一个String,返回boolean,这完全可以用java.util.function包中的Function<boolean, String> 来代替

1
2
3
4
@FunctionalInterface
public interface Movable {
boolean moveTo(String address);
}

再新建一个测试类Test使用lambda表达式完成这个场景

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
public static void main(String[] args){
Person person = new Person("老王");

person.go(address -> {
System.out.println("老王坐地铁去了机场...");
System.out.println("老王坐上了飞机准备飞往"+address);
System.out.println("飞机发生事故,老王...");
return false;
}, "上海");
}
}

结果:

1
2
3
4
5
老王带着行李从北京的老家出门了
老王坐地铁去了机场...
老王坐上了飞机准备飞往上海
飞机发生事故,老王...
老王没有到达目的地!

虽然这个例子不够合理但主要是用来练习使用lambda表达式,在要使用匿名内部类的时候考虑用lambda表达式来代替。
在使用list要遍历list的时候也可以使用lambda表达式,如List<String> list 比如要遍历这个list并将list中的字符串输出到控制台可以使用list.forEach(value->System.out.println(value)); 来代替传统的遍历,forEach是Iterable的方法,该方法接收一个Consumer 参数,前面提到Consumer是一个标准的函数式接口所以可以使用lambda表达式。

方法引用

java8允许传入一个方法引用,其和lambda表达式原理差不多,也是仅支持函数式接口,如前面提到的list.forEach(value->System.out.println(value)); 表达式可以使用方法引用代替list.forEach(System.out::println); 方法引用使用一对冒号( :: ),传入的方法必须函数式接口的方法有相同的参数和返回值类型,如forEach方法需要接受一个Consumer对象,而Consumer这个函数式接口的方法接收一个值返回void,而System.out.println()方法通用是接收一个对象,返回void,所以可以在此处使用System.out::println方法引用,jvm会自动将参数传给println方法。方法引用有4种用法

  • 引用静态方法 如System.out的println方法 使用Class:static_method
    • System.out::println
  • 引用特定对象的实例方法 如StringBuilder的append()方法,使用instance::method

    1
    2
    StringBuilder sb = new StringBuilder();
    sb::append
  • 引用特定类型的任意对象的实例方法 如String的toString()方法,使用Class:method

    • String::toString
  • 引用构造函,使用Class::new
    • String::new

方法应用是用来简化lambda表达式的,所以能使用lambda表达式的地方就能使用方法引用

lambda表达式与匿名内部类

关于lambda表达式与匿名内部类的比较以及lambda表达式的实现原理,网络上已经有一些很好的文章
http://blog.csdn.net/raintungli/article/details/54910152 这篇文章很好的分析了jvm如何翻译lambda表达式
http://www.infoq.com/cn/articles/Java-8-Lambdas-A-Peek-Under-the-Hood 这篇文章很好的分析了为什么使用lambda表达式性能比使用匿名内部类高很多