参考资料:
关键问题
Java 8中的lambda为什么要设计成这样?(为什么要一个lambda对应一个接口?而不是Structural Typing?)
lambda和匿名类型的关系是什么?lambda是匿名对象的语法糖吗?
Java 8是如何对lambda进行类型推导的?它的类型推导做到了什么程度?
Java 8为什么要引入默认方法?
Java编译器如何处理lambda?
主要内容
lambda表达式(又被成为“闭包”或“匿名方法”)
方法引用和构造方法引用
扩展的目标类型和类型推导
接口中的默认方法和静态方法
1 、背景
Java API中定义了一个接口(一般被称为回调接口),用户通过提供这个接口的实例来传入指定行为,例如:
public interface ActionListener {
void actionPerformed(ActionEvent e);
}
这里并不需要专门定义一个类来实现ActionListener接口,因为它只会在调用处被使用一次。用户一般会使用匿名类型把行为内联(inline):
button.addActionListener(new ActionListener) {
public void actionPerformed(ActionEvent e) {
ui.dazzle(e.getModifiers());
}
}
随着回调模式和函数式编程风格的日益流行,我们需要在Java中提供一种尽可能轻量级的将代码封装为数据(Model code as data)的方法。匿名内部类并不是一个好的选择,因为:
语法过于冗余
匿名类中的this和变量名容易使人产生误解
类型载入和实例创建语义不够灵活
无法捕获非final的局部变量
无法对控制流进行抽象
上面的多数问题均在Java SE 8中得以解决:
通过提供更简洁的语法和局部作用域规则,Java SE 8彻底解决了问题1和问题2
通过提供更加灵活而且便于优化的表达式语义,Java SE 8绕开了问题3
通过允许编译器推断变量的“常量性”(finality),Java SE 8减轻了问题4带来的困扰
2 函数式接口(SAM类型)
ActionListener、Runnable、Comparator等
编译器根据接口格式自行判断;使用@FunctionalInterface注解判断;
除此之外,Java SE 8中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如:
Predicate<T>——接收T对象并返回boolean
Consumer<T>——接收T对象,不返回值
Function<T, R>——接收T对象,返回R对象
Supplier<T>——提供T对象(例如工厂),不接收值
UnaryOperator<T>——接收T对象,返回T对象
BinaryOperator<T>——接收两个T对象,返回T对象
3 lambda表达式
lambada表达式其实就是一个函数,广泛的被作为一个回调函数使用
解决‘高度问题’:
// Java 8之前:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Before Java8, too much code for too little to do");
}
}).start();
1
2
//Java 8方式:
new Thread( () -> System.out.println("In Java8, Lambda expression rocks !!") ).start();
几个lambda表达式:
(int x, int y) -> x + y
() -> 42
(String s) -> { System.out.println(s); }
表达式由参数列表、箭头符号和函数体构成;函数体既可以是一个表达式,也可以是一个语句块
FileFilter java = (File f) -> f.getName().endsWith("*.java");
new Thread(() -> {
connectToService();
sendNotification();
}).start();
4 目标类型
编译器负责推导lambda表达式的类型。它利用lambda表达式所在上下文所期待的类型进行推导,这个被期待的类型被称为目标类型。
Callable<String> c = () -> "done";
Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);
也因此参数列表也不需要再体现参数类型,同时也为了不将‘高度问题’变成‘宽度问题’;
检查是否符合目标类型
T是一个函数式接口
lambda表达式的参数和T的方法参数在数量和类型上一一对应
lambda表达式的返回值和T的方法返回值相兼容(Compatible)
lambda表达式内所抛出的异常和T的方法throws类型相兼容
目标类型上下文
List<Person> ps = ... Stream<String> names = ps.stream().map(p -> p.getName());
在上面的代码中,ps的类型是List<Person>,所以ps.stream()的返回类型是Stream<Person>。
map()方法接收一个类型为Function<T, R>的函数式接口,这里T的类型即是Stream元素的类型,也就是Person,而R的类型未知。
由于在重载解析之后lambda表达式的目标类型仍然未知,我们就需要推导R的类型。
通过对lambda表达式函数体进行类型检查,我们发现函数体返回String,因此R的类型是String,因而map()返回Stream<String>。
转型表达式(Cast expression)可以显式提供lambda表达式的类型,这个特性在无法确认目标类型时非常有用:
Object o = (Runnable) () -> { System.out.println("hi"); };
5 关于变量的问题
内部类中通过继承得到的成员(包括来自Object的方法)可能会把外部类的成员掩盖,lambda表达式函数体里面的变量和它外部环境的变量具有相同的语义。
java 7中的变量必须被声明为final,才能在内部类中使用;java 8 放宽限制,有效只读类型的变量也允许;简而言之,lambda表达式对值封闭,对变量开放。
int sum = 0; list.forEach(e -> { sum += e.size(); }); // Illegal, close over values List<Integer> aList = new List<>(); list.forEach(e -> { aList.add(e); }); // Legal, open over variables |
对于值的修改,可以使用reduce方法。
6 方法引用
方法引用和lambda表达式拥有相同的特性
Person[] people = ...
//lambda表达式
Comparator<Person> byName = Comparator.comparing(p -> p.getName());
//方法引用
Comparator<Person> byName = Comparator.comparing(Person::getName);
这里的Person::getName可以被看作为lambda表达式的简写形式。尽管方法引用不一定会把语法变的更紧凑,但它拥有更明确的语义 。
函数式接口的方法参数对应于隐式方法调用时的参数 :
Consumer<Integer> b1 = System::exit; // void exit(int status)
Consumer<String[]> b2 = Arrays:sort; // void sort(Object[] a)
Function<String, String> upperfier = String::toUpperCase; // String toUpperCase(String str)
Predicate<String> isKnown = knownNames::contains; // boolean contains(String str)
方法引用有很多种,它们的语法如下,静态方法和实例方法的引用并无不同,编译器会自己判断调用的方法:
静态方法引用:ClassName::methodName
实例上的实例方法引用:instanceReference::methodName
超类上的实例方法引用:super::methodName
类型上的实例方法引用:ClassName::methodName
构造方法引用:Class::new
数组构造方法引用:TypeName[]::new
7 默认方法和静态接口方法
Java SE 7时代为一个已有的类库增加功能是非常困难的
默认方法拥有其默认实现,实现接口的类型通过继承得到该默认实现(如果类型没有覆盖该默认实现)
可以向函数式接口中添加默认方法,而不用担心函数式接口的单抽象方法限制
interface Iterator<E> { boolean hasNext(); E next(); void remove(); default void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); while (hasNext()) action.accept(next()); } } |
当接口继承其它接口时,我们既可以为它所继承而来的抽象方法提供一个默认实现,也可以为它继承而来的默认方法提供一个新的实现,还可以把它继承而来的默认方法重新抽象化。
Java SE 8允许在接口中定义静态方法。这使得我们可以从接口直接调用和它相关的辅助方法(Helper method),
而不是从其它的类中调用(之前这样的类往往以对应接口的复数命名,例如Collections)。
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<T, U> keyExtractor) {
return (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
8 应用示例
java 8之前的写法
List<Person> people = ...
Collections.sort(people, new Comparator<Person>() {
public int compare(Person x, Person y) {
return x.getLastName().compareTo(y.getLastName());
}
})
利用lambda表达式去除匿名类
Collections.sort(people, (Person x, Person y) -> x.getLastName().compareTo(y.getLastName()));
使用Comparator接口提供的静态方法comparing提升代码的抽象程度
Collections.sort(people, Comparator.comparing((Person p) -> p.getLastName()));
通过java 8的类型推导机制,进一步简化代码
Collections.sort(people, comparing(p -> p.getLastName()));
使用方法引用
Collections.sort(people, comparing(Person::getLastName));
使用List的默认方法代替Collections,无需引入冗余的代码,开发者也无需知道还有Collections.sort方法用于处理List的排序问题。
people.sort(comparing(Person::getLastName));