1、接口中可以定义default方法,实现类则不必实现这个方法
2、解决默认方法冲突
如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义了同样的方法,会发生什么情况?诸如Scala和C++等语言对于解决这种二义性有一些复杂的规则。幸运的是,Java的相应规则要简单得多。规则如下:
1)超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
2)接口冲突。如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突。
方法引用
有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。例如,假设你希望只要出现一个定时器事件就打印这个事件对象。当然,为此也可以调用:
Timer t= new Timer(1000,event -> System.out.printIn(event));
但是,如果直接把println方法传递到Timer构造器就更好了。具体做法如下:
Timer t = new Timer(1000,System.out::println);
表达式System.out::println是一个方法引用(method reference),它等价于lambda表达式x->System.out.println(x)。
再来看一个例子,假设你想对字符串排序,而不考虑字母的大小写。可以传递以下方法表达式:
Arrays.sort(strings,String::compareToIgnorecase);
从这些例子可以看出,要用::操作符分隔方法名与对象或类名。主要有3种情况:
- object::instanceMethod
- Class::staticMethod
- Class::instanceMethod
- 在前2种情况中,方法引用等价于提供方法参数的lambda表达式。前面已经提到,System.out::println等价于x->System.out.println(x)。类似地,Math::pow等价于(x,y)->Math.pow(x,y)。
对于第3种情况,第1个参数会成为方法的目标。例如,String::compareToIgnoreCase等同于(x,y)->x.comcompareToIgnoreCase(y)。注释:如果有多个同名的重载方法,编译器就会尝试从上下文中找出你指的那一个方法。例如,Math.max方法有两个版本,一个用于整数,另一个用于double值。选择哪一个版本取决于Math::max转换为哪个函数式接口的方法参数。类似于lambda表达式,方法引用不能独立存在,总是会转换为函数式接口的实例。
可以在方法引用中使用this参数。例如,this::equals等同于x->this.equals(x)。使用super也是合法的。下面的方法表达式
super::instancemethod
使用this作为目标,会调用给定方法的超类版本。
为了展示这一点,下面给出一个假想的例子:

TimedGreeter.greet方法开始执行时,会构造一个Timer,它会在每次定时器滴答时执行super::greet方法。这个方法会调用超类的greet方法。
构造器引用
构造器引用与方法引用很类似,只不过方法名为new。例如,Person::new是Person构造器的一个引用。哪一个构造器呢?这取决于上下文。假设你有一个字符串列表。可以把它转换为一个Person对象数组,为此要在各个字符串上调用构造器,调用如下:

我们将在卷Ⅱ的第1章讨论stream、map和collect方法的详细内容。就现在来说,重点是map方法会为各个列表元素调用Person(String)构造器。如果有多个Person构造器,编译器会选择有一个String参数的构造器,因为它从上下文推导出这是在对一个字符串调用构造器。
可以用数组类型建立构造器引用。例如,int[]::new是一个构造器引用,它有一个参数:即数组的长度。这等价于lambda表达式x->new int[x]。
Java有一个限制,无法构造泛型类型T的数组。数组构造器引用对于克服这个限制很有用。表达式new T[n]会产生错误,因为这会改为new Object[n]。对于开发类库的人来说,这是一个问题。例如,假设我们需要一个Person对象数组。Stream接口有一个toArray方法可以返回Object数组:
Object[] people = stream.toArray();
不过,这并不让人满意。用户希望得到一个Person引用数组,而不是Object引用数组。流库利用构造器引用解决了这个问题。可以把Person[]::new传入toArray方法:
Person[] people = stream.toArray(Person[]::New);
toArray方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。
lambda表达式变量作用域
通常,你可能希望能够在lambda表达式中访问外围方法或类中的变量。考虑下面这个例子:

来看这样一个调用:
现在来看lambda表达式中的变量text。注意这个变量并不是在这个lambda表达式中定义的。实际上,这是repeatMessage方法的一个参数变量。
如果再想想看,这里好像会有问题,尽管不那么明显。lambda表达式的代码可能会在repeatMessage调用返回很久以后才运行,而那时这个参数变量已经不存在了。如何保留text变量呢?
要了解到底会发生什么,下面来巩固我们对lambda表达式的理解。lambda表达式有3个部分:
1)一个代码块;
2)参数;
3)自由变量的值,这是指非参数而且不在代码中定义的变量。
在我们的例子中,这个lambda表达式有1个自由变量text。表示lambda表达式的数据结构必须存储自由变量的值,在这里就是字符串“Hello”。我们说它被lambda表达式捕获(captured)。(下面来看具体的实现细节。例如,可以把一个lambda表达式转换为包含一个方法的对象,这样自由变量的值就会复制到这个对象的实例变量中。)
注释:关于代码块以及自由变量值有一个术语:闭包(closure)。如果有人吹嘘他们的语言有闭包,现在你也可以自信地说Java也有闭包。在Java中,lambda表达式就是闭包。
可以看到,lambda表达式可以捕获外围作用域中变量的值。在Java中,要确保所捕获的值是明确定义的,这里有一个重要的限制。在lambda表达式中,只能引用值不会改变的变量。例如,下面的做法是不合法的:

之所以有这个限制是有原因的。如果在lambda表达式中改变变量,并发执行多个动作时就会不安全。对于目前为止我们看到的动作不会发生这种情况,不过一般来讲,这确实是一个严重的问题。关于这个重要问题的更多内容参见第14章。
另外如果在lambda表达式中引用变量,而这个变量可能在外部改变,这也是不合法的。例如,下面就是不合法的:

这里有一条规则:lambda表达式中捕获的变量必须实际上是最终变量(effectively final)。实际上的最终变量是指,这个变量初始化之后就不会再为它赋新值。在这里,text总是指示同一个String对象,所以捕获这个变量是合法的。不过,i的值会改变,因此不能捕获i。
lambda表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。
在方法中,不能有两个同名的局部变量,因此,lambda表达式中同样也不能有同名的局部变量。
在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。例如,考虑下面的代码:

处理lambda表达式
到目前为止,你已经了解了如何生成lambda表达式,以及如何把lambda表达式传递到需要一个函数式接口的方法。下面来看如何编写方法处理lambda表达式。
使用lambda表达式的重点是延迟执行(deferred execution)。毕竟,如果想要立即执行代码,完全可以直接执行,而无需把它包装在一个lambda表达式中。之所以希望以后再执行代码,这有很多原因,如:
·在一个单独的线程中运行代码;
·多次运行代码;
·在算法的适当位置运行代码(例如,排序中的比较操作);
·发生某种情况时执行代码(如,点击了一个按钮,数据到达,等等);
·只在必要时才运行代码。
下面来看一个简单的例子。假设你想要重复一个动作n次。将这个动作和重复次数传递到一个repeat方法:

需要说明,调用action.run()时会执行这个lambda表达式的主体。
表6-1 常用函数式接口

现在让这个例子更复杂一些。我们希望告诉这个动作它出现在哪一次迭代中。为此,需要选择一个合适的函数式接口,其中要包含一个方法,这个方法有一个int参数而且返回类型为void。处理int值的标准接口如下:

下面给出repeat方法的改进版本:

可以如下调用它:
表6-2列出了基本类型int、long和double的34个可能的规范。最好使用这些特殊化规范来减少自动装箱。出于这个原因,我在上一节的例子中使用了IntConsumer而不是Consumer<Integer>。
表6-2 基本类型的函数式接口

提示:最好使用表6-1或表6-2中的接口。例如,假设要编写一个方法来处理满足某个特定条件的文件。对此有一个遗留接口java.io.FileFilter,不过最好使用标准的Predicate<File>。只有一种情况下可以不这么做,那就是你已经有很多有用的方法可以生成FileFilter实例。
注释:大多数标准函数式接口都提供了非抽象方法来生成或合并函数。例如,Predicate.isEqual(a)等同于a::equals,不过如果a为null也能正常工作。已经提供了默认方法and、or和negate来合并谓词。例如,Predicate.isEqual(a).or(Predicate.isEqual(b))就等同于x->a.equals(x)||b.equals(x)。
注释:如果设计你自己的接口,其中只有一个抽象方法,可以用@FunctionalInterface注解来标记这个接口。这样做有两个优点。如果你无意中增加了另一个非抽象方法,编译器会产生一个错误消息。另外javadoc页里会指出你的接口是一个函数式接口。
并不是必须使用注解。根据定义,任何有一个抽象方法的接口都是函数式接口。不过使用@FunctionalInterface注解确实是一个很好的做法。
再谈Comparator
Comparator接口包含很多方便的静态方法来创建比较器。这些方法可以用于lambda表达式或方法引用。
静态comparing方法取一个“键提取器”函数,它将类型T映射为一个可比较的类型(如String)。对要比较的对象应用这个函数,然后对返回的键完成比较。例如,假设有一个Person对象数组,可以如下按名字对这些对象排序:

与手动实现一个Comparator相比,这当然要容易得多。另外,代码也更为清晰,因为显然我们都希望按人名来进行比较。
可以把比较器与thenComparing方法串起来。例如,

如果两个人的姓相同,就会使用第二个比较器。
这些方法有很多变体形式。可以为comparing和thenComparing方法提取的键指定一个比较器。例如,可以如下根据人名长度完成排序:

另外,comparing和thenComparing方法都有变体形式,可以避免int、long或double值的装箱。要完成前一个操作,还有一种更容易的做法:

如果键函数可以返回null,可能就要用到nullsFirst和nullsLast适配器。这些静态方法会修改现有的比较器,从而在遇到null值时不会抛出异常,而是将这个值标记为小于或大于正常值。例如,假设一个人没有中名时getMiddleName会返回一个null,就可以使用Comparator.comparing(Person::getMiddleName(),Comparator.nullsFirst(...))。
nullsFirst方法需要一个比较器,在这里就是比较两个字符串的比较器。naturalOrder方法可以为任何实现了Comparable的类建立一个比较器。在这里,Comparator.<String>naturalOrder()正是我们需要的。下面是一个完整的调用,可以按可能为null的中名进行排序。这里使用了一个静态导入java.util.Comparator.*,以便理解这个表达式。注意naturalOrder的类型可以推导得出。

静态reverseOrder方法会提供自然顺序的逆序。要让比较器逆序比较,可以使用reversed实例方法。例如naturalOrder().reversed()等同于reverseOrder()。