前言
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表达式使用,这些接口一看类名就大概知道是用来做什么的,如下是其中一部分:
这些接口都提供一个抽象方法供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一个线程去执行一个任务我们会怎么做:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("do some thing ...");
}
}).start();
在jdk1.8之后使用lambda表达式我们会这么做:
new Thread(()->System.out.println("do some thing ...")).start();
一看上面的代码能清晰的感觉到lambda表达式的简洁性,在1.8之前,有时候我们明明只需要传入一个函数,但java语言不允许我们这么做,非要我们new一个匿名内部类,lambda完美的解决了这个问题,要让Thread执行任务其实我们只用传入run方法,并在方法中写逻辑就行了。lambda表达式的用法如下:
(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类
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>
来代替
@FunctionalInterface
public interface Movable {
boolean moveTo(String address);
}
再新建一个测试类Test使用lambda表达式完成这个场景
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;
}, "上海");
}
}
结果:
老王带着行李从北京的老家出门了
老王坐地铁去了机场...
老王坐上了飞机准备飞往上海
飞机发生事故,老王...
老王没有到达目的地!
虽然这个例子不够合理但主要是用来练习使用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
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表达式性能比使用匿名内部类高很多