1)FunctionalInterface注释
Java 8提出了函数式接口的概念。所谓函数式接口,简单来说,就是只定义了单一抽象方法的接口。比如下面的定义:
@FunctionalInterface
public static interface IntHandler {
void handle(int i);
}
注释FunctionalInterface用于表明IntHandler接口是一个函数式接口,该接口被定义为只包含一个抽象方法handle(),因此它符合函数式接口的定义。如果一个函数满足函数式接口的定义,那么即使不标注为@FunctionalInterface,编译器依然会把它看做是函数式接口。这有点像@Oveeride注释,如果函数符合重载的要求,无论是否标注了@Oveeride,编译器都会识别这个重载函数,但一旦进行了标注,而实际的代码不符合规范,那么就会得到一个编译错误。
需要强调的是,函数式接口只能有一个抽象方法,而不是一个方法。这分两点来说明:首先,在Java 8中,接口运行存在实例方法,其次任何被java.lang.Object实现的方法,都不能视为抽象方法,因此,下面的NonFunc接口不是函数式接口,因为equals()方法在java.lang.Object中已经实现。
interface NonFunc {
boolean equals(Object obj);
}
同理,下面实现的IntHandler接口符合函数式接口要求。
@FunctionalInterface
public static interface IntHandler {
void handle(int i);
boolean equals(Object obj);
}
@FunctionalInterface
public static interface IntHandler {
void handle(int i);
boolean equals(Object obj);
}
函数式接口的实例可以由方法引用或者lambda表达式进行构造。
2)接口默认方法
在Java 8之前的版本,接口只能包含抽象方法。从Java 8之后,接口也可以包含若干个实例方法。这一改进使得Java 8拥有了类似于多继承的能力。一个对象实例,将拥有来自多个不同接口的实例方法。
比如,对于接口IHorse,实现如下:
public interface IHorse {
void eat();
default void run() {
System.out.println("hourse run");
}
}
在Java 8中,使用default关键字,可以在接口内定义实例方法。注意,这个方法并非抽象方法,而是拥有特定逻辑的具体实例方法。
所有的动物都能自由呼吸,所有,这里可以再定义一个IAnimal接口,它包含一个默认方法breath()。
public interface IAnimal {
default void breath() {
System.out.println("hourse breath");
}
}
骡是马和驴的杂交物种,因此骡(Mule)可以实现为IHorse,同时骡也是动物,因此有:
public class Mule implements IHorse, IAnimal {
@Override
public void eat() {
System.out.println("Mule eat");
}
public static void main(String[] args) {
Mule m = new Mule();
m.run();
m.breath();
}
}
上述代码中Mule实例同时拥有来自不同接口的实现方法。这在Java 8之前是做不到的。从某种程度上说,这种模式可以弥补Java单一继承的一些不便。但同时也要知道,它也将遇到和多继承相同的问题。如果,IDonkey也存在一个默认的run()方法,那么同时实现他们的Mule,就会不知所措,因为它不知道应该以哪个方法为准。
增加一个IDonkey的实现:
public void interface IDonkey {
void eat();
default void run() {
System.out.println("Donkey run");
}
}
修改Mule的实现如下,注意它同时实现了IHorse和IDonkey:
public class Mule implements IHorse,IDonkey,IAnimal {
@Override
public void eat() {
System.out.println("Mule run");
}
public static void main(String[] args) {
Mule m = new Mule();
m.run();
m.breath();
}
}
此时,由于 IHorse和IDonkey拥有相同的默认实例方法,故编译器会抛出一个错误:
Duplicate default methods named run with the parameters () and () are inherited from the types IDonkey and IHorse
为了让Mule同时实现IHorse和IDonkey,不得不重新实现以下run()方法,让编译器可以进行方法绑定。修改Mule的实现如下:
public class Mule implements IHorse,IDonkey,IAnimal {
@Override
default void run() {
IHorse.super.run();
}
@Override
public void eat() {
System.out.println("Mule run");
}
public static void main(String[] args) {
Mule m = new Mule();
m.run();
m.breath();
}
}
在这里,将Mule的run()方法委托给IHorse实现。
接口默认实现对于整个函数式编程的流式表达非常重要。比如,java.util.Comparator接口,在JDK 1.2时已经 被引入,用于在排序时给出两个对象实例的具体比较逻辑。在Java 8中,Comparator接口新增了若干默认方法,用于多个比较器的整合。其中一个常用的默认方法如下:
default Comparator<T> thenComparing(Comparator<? super T> other) {
Objects.requireNonNull(other);
return (Comparator<T> & Serializable) (c1, c2) -> {
int res = compare(c1,c2);
return (res != 0) ? res : other.compare(c1,c2);
};
}
有了这个默认方法,在进行排序时,就可以非常方便地进行元素的多条件 排序,比如,如下代码构造一个比较器,它先按照字符串长度排序,继而按照大小写不敏感的字母顺序排序。
Comparator<String> cmp = Comparator.comparingInt(String:length)
.thenComparing(String.CASE_INSENSITIVE_ORDER);
3) lambda表达式
lambda表达式可以说是函数式编程的核心。lambda表达式即匿名函数,它是一段没有函数名的函数体,可以作为参数直接传递给相关的调用者。lambda表达式极大地增强了Java语言的表达能力。
下例展示了lambda表达式的使用,在forEach()函数中,传入的就是一个lambda表达式,它完成了对元素的标准输出操作。可以看到这段表达式并不像函数一样有名字,非常类似匿名内部类,它只是简单地描述了应该执行的代码段。
List<Integer> numbers = Arrays.asList(1,2,3,4,5,6);
numbers.forEach((Integer value) -> System.out.println(value));
和匿名对象一样,lambda表达式也可以访问外部的局部变量,如下所示:
final int num = 2;
Function<Integer, Integer> stringConverter = (from) -> from * num;
System.out.println(stringConverter.apply(3));
上述代码可以编译通过,正常执行,并输出6,。与匿名内部对象一样,在这种情况下,外部的num变量必须申明为final,这样才能保证在lambda表达式中合法的访问它。
对于lambda表达式而言,即使去掉了上述的final定义,程序依然可以编译通过。Java 8会自动地将在lambda表达式中使用的变量视为final。因此,上述代码是可以编译通过的
但是,如果像下面这样写,就不行:
final int num = 2;
Function<Integer, Integer> stringConverter = (from) -> from * num;
num++;
System.out.println(stringConverter.apply(3));
4)方法引用
方法引用是Java 8中提出来简化lambda表达式的一种手段。它通过类名和方法名来定位一个静态方法或者实例方法。
方法引用在Java 8中的使用非常灵活。可以分为以下几种。
静态方法引用:ClassName::methodName
实例上的实例方法引用:instanceReference::methodName
超类上的实例方法引用:super::methodName
类型上的实例方法引用:ClassName::methodName
构造方法引用:Class::new
数组构造方法引用:TypeName[]::new
首先,方法引用使用“::”定义,“::”的前半部分表示类名或者实例名,后半部分表示方法名称。如果是构造函数,则使用new表示。
下例展示了方法引用的基本使用:
public class InstanceMethodRef {
public static void main(String[] args) {
List<User> users = new ArrayList<User>();
for(int i=0; i<10; i++) {
users.add(new User(i, "billy"+Integer.toString(i)));
}
users.stream().map(User::getName).forEach(System.out::println);
}
}
对于第1个方法引用“User::getName”,表示User类的实例方法。在执行时,Java会自动识别流中的元素(这里指User实例)是作为调用目标还是调用方法的参数。在“User::getName”中,显然流内的元素都应该作为调用目标,因此实际上,在这里调用了每一个User对象实例的getName()方法,并将这些User的name作为一个新的流。同时,对于这里得到的所有name,使用方法引用System.out::println进行处理。这里的System.out为PrintStream对象实例,因此,这里表示System.out实例的println方法,系统也会自动判断,流内的元素此时应该作为方法的参数传入,而不是调用目标。
一般来说,如果使用的是静态方法,或者调用目标明确,那么流内的元素会自动作为参数使用。如果函数引用表示实例方法,并且不存在调用目标,那么流内元素就会自动作为调用目标。
如下例子:
public class BadMethodRef {
public static void main(String[] args) {
List<Double> numbers = new ArrayList<Double>();
for(int i=1; i<10; i++) {
numbers.add(Double.valueOf(i));
}
numbers.stream().
}
}
上述代码试图将所有的Double元素转为String并将其输出,但是很不幸,在Double中同时存在以下两个函数:
public static String toString(double d)
public String toString()
此时,对函数引用的处理就出现了歧义,因此,这段代码在编译时就会抛出如下错误:
Ambiguous method reference:both toString() and toString(double) from the type Double are eligible
方法引用也可以直接使用构造函数。首先,查看模型类User的定义:
public class User {
private int id;
private String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
//这里省略对字段的setter和getter
}
下面的方法引用调用了User的构造函数:
public class ConstrMethodRef {
@FunctionalInterface
interface UserFactory<U extends User> {
U create(int id, String name);
}
static UserFactory<User> uf = User::new;
public static main(String[] args) {
List<User> users = new ArrayList<User>();
for(int i=1; i<10; i++) {
users.add(uf.create(i, "billy" + Integer.toString(i)));
}
users.stream().map(User::getName).forEach(System.out::println);
}
}
在此,UserFactory作为User的工厂类,是一个函数式接口。当使用User::new创建接口实例时,系统会根据UserFactory.create()的函数签名来选择合适的User构造函数,在这里,很显然就是public User(int id, String name)。在创建UserFactory实例后,对UserFactory.create()的调用,都会委托给User的实际构造函数进行,从而创建User对象实例。