2018.12.02
文章目录
前言
虽然Java 8早在2014年就已经发布了,但得益于它所带来的新特性,使得Java重新焕发生机。
Java 8包含如下新特性1:
- Lambda表达式
- 方法引用
- 默认方法
- 新的
Streams
API Optional
- 新的
Date/Time
API - Narshorn,新的JS引擎
- 移除永久代(Permanent Generation)
- … …
Lambda表达式
定义
Lambda表达式,可以说就是为单个方法的匿名类所提供的语法糖。Lambda表达式的支持有助于简化Java代码。编译器会根据Lambda表达式的上下文来判断所使用的***函数式接口***和参数的类型。对于特定方法,还能用***方法引用***进一步地简化Lambda表达式的写法。
语法
主要的语法就是“参数 -> 方法体”。Lambda表达式还有四条较为重要的语法:
- 参数类型的声明是可选的
- 当只有一个参数时,参数两侧的括号是可选的
- 方法体使用花括号是可选的(除非方法体包含多条语句)
- 当使用单个表达式返回返回值时,
return
关键字是可选的
Arrays.sort(strArray, (foo, bar) -> foo.length() - bar.length());
上面的例子中,Lambda表达式实现了Comparator
接口完成排序。
作用域
Lambda表达式中能引用final
变量或或者实际final
变量,所谓实际final
变量就是变量仅被赋值一次。例如:
// 正例
String sql = "delete * from User";
getHibernateTemplate().execute(session ->
session.createSQLQuery(sql).uniqueResult());
// 反例
String sql = "delete * from User";
getHibernateTemplate().execute(session ->
session.createSQLQuery(~~sql~~ ).uniqueResult());
sql = "select * from User";
Lambda表达式的极简模式——方法引用
定义
方法引用,引用的是已存在的方法,可以算是Lambda表达式的一种简化写法。对于方法体中只调用某个已存在方法的Lambda表达式,就可以改写为直接引用该方法。
语法
通过::
进行方法引用,以下方法可被引用2:
- 静态方法:
ContainingClass::staticMethodName
- 类实例的实例方法:
containingObject::instanceMethodName
- 类的实例方法:全称是“特定类型的任意对象的实例方法”,
ContainingType::methodName
- 类/数组构造器(ie. TreeSet::new/TreeSet[]:new):
ClassName::new
“特定类型的任意对象的实例方法”,这句话容易和“类实例的实例方法”混淆。先说“类实例的实例方法”,很直观,就是通过某个类实例引用它的实例方法,比如要对一个数组Clazz[] clazzArr
排序,我们可以实例化了一个Comparator<Clazz> comparator
并引用它的comparator::compare
方法进行排序;再说“特定类型的任意对象的实例方法”,还是对Clazz[] clazzArr
排序,但此时Clazz
实现了Comparable
接口,定义了Clazz::compareTo
方法,那对于数组里的任意对象,都可以直接地引用它们自身的compareTo
实例方法比较大小,也就是引用Clazz
类的任意对象的实例方法。
但从使用者的角度来说,“特定类型的任意对象的实例方法”,其实就是通过类名引用实例方法——ContainingType::methodName
,所以我们可以直观地理解为“类的实例方法”。
Lambda表达式的类型——函数式接口
定义
既然有了Lambda表达式,那在Java里如果声明它们呢?于是就有了函数式接口。函数式接口是只包含一个方法的接口,每个Lambda表达式,编译器最后都会根据上下文判断它所对应的函数式接口。
语法
Java 8在java.util.function
包中定义几个函数式接口:
Function<T, R>
:接收T
类型的对象,并返回R
Supplier<T>
:返回T
类型的对象Predicate<T>
:基于T
类型的输入,返回一个布尔值Consumer<T>
:对T
类型的对象执行一定的动作BiFunction<T, U, R>
BiConsumer<T, U, R>
示例如下:
// Function<T, R>
Function<String, Integer> length = String::length;
System.out.println(length.apply("GHD")); // 输出3
// Consumer<T>
User user = new User("GHD");
Consumer<User> userConsumer = foo -> foo.setName("HD G.");
userConsumer.accept(user; // public interface Consumer<T> { void accept(T t);}
System.out.println(user.getName); // 输出HD G.
Lambda铁蹄踏遍Java
最适合应用Lambda表达式的场景,莫过于集合类的操作。前朝功臣Iterator
利用hasNext
和next
也曾立下过汗马功劳,但写法不简练,并且不易于并行化。Java 8要顺利地推广Lambda表达式,就需要先从集合类下手。然而事情没有这么简单,java.util.Collection
是个接口,不能实现方法,于是Java 8就引入了***默认方法***来解决这个问题。集合类的操作就通过Stream
API来拓展新特性。
默认方法
定义
默认方法,就是接口方法的默认实现。Java 8之所以要引入接口的默认方法,原因是在扩展java.util.Collection
的特性时,考虑到可能产生的兼容性问题,例如如果直接给java.util.Collection
添加一个新的方法,那么所有实现了该接口的类,都需要实现新方法。于是在扩展类似java.util.Collection
的接口时,Java 8允许在接口里定义默认方法,这样所有实现该接口的类都自动添加了新方法的实现,以此实现“向后兼容”(Backwards Compatibility / Virtual Extension Methods)。
那可否用静态方法来实现“向后兼容”呢?答案是不行3。类的静态方法不能被子类继承,因此子类都无法实现静态方法;调用静态方法时,也就不能通过子类名调用父类的静态方法。
示例如下:
public interface Example {
default void newMethod() {
System.out.println("new method is here");
}
}
多个接口的继承问题——钻石问题
如果一个类实现了两个接口,而这两个接口都包含一个相同方法签名的默认方法,这种情况就类似于“钻石问题”4。钻石问题描述的是下图所示的继承关系,B、C类都覆盖了A类的方法,而D类同时继承了B、C类,那么通过D实例在调用该方法时,调用的是B类还是C类的方法?
-
情况一:如果两个父接口都包含一个相同方法签名的默认方法,那在编译过程就会直接报错。要解决这个问题,就需要在子类里显式地覆盖这个方法。覆盖时,方法体内可以同时调用父接口的
super
来显式地调用默认方法。 -
情况二:如果类实现了一个包含默认方法的接口,同时继承一个包含相同方法签名的类,那么编译是可以通过的,子类会调用父类的方法,原因是“类优先”。之所以采用“类优先”的策略,原因可能也和“向后兼容”有关5,升级到Java 8后,即使被扩展的接口引入了新的默认方法,也要保证不会影响到那些实现该接口的类的正常使用。
接口的默认方法 V.S. 抽象类
接口 | 抽象类 | 备注 | |
---|---|---|---|
覆盖方法中引用子类成员变量 | 否 | 是 | |
提供便利方法(Convenience Methods)6 | 是 | 是 | 例如,Arrays 和Conllections 所提供的方法,以及工厂方法 |
有构造器 | 否 | 是 | |
包含成员变量 | 否 | 是 |
接口的默认方法,本质上还是为了实现“向后兼容”而出现的,为Java 8和Lambda编程提供桥梁,与抽象类还是有比较大的区别的。
Stream
API
Stream
API包含两类操作7:中间操作(Intermediate Operation)和终结操作(Terminal Operation)。Stream
API是懒加载模式,中间操作不会立即执行,只有到遇到第一个终结操作,中间操作才会执行。中间操作包括map/filter/flatmap
等。