6.2 函数式编程基础
在正式进入函数式编程之前,有必要先了解一下Java 8为支持函数式编程所做的基础性的改进,这里,将简要介绍一下FunctionalInterface注释、接口默认方法和方法句柄。
6.2.1 FunctionalInterface注释
Java 8提出了函数式接口的概念。所谓函数式接口,简单来说,就是只定义了单一抽象方法的接口。比如下面的定义:
@FunctionalInterface public static interface IntHandler{ void handle(int i); }
注释FunctionalInterface用于表明IntHandler接口是一个函数式接口,该接口被定义为只包含一个抽象方法handle(),因此它符合函数式接口的定义。如果一个函数满足函数式接口的定义,那么即使不标注为@FunctionalInterface,编译器依然会把它看做函数式接口。这有点像@Override注释,如果你的函数符合重载的要求,无论你是否标注了@Override,编译器都会识别这个重载函数,但一旦你进行了标注,而实际的代码不符合规范,那么就会得到一个编译错误。如图6.1所示,展示了一个不符合规范,却被标注为@FunctionalInterfacede接口。很显然,该IntHandler包含两个抽象方法,因此不符合函数式接口的要求,又因为IntHandler接口被标注为函数式接口,产生矛盾,故编译出错。
图6-1 不符合规范的函数式接口
这里需要强调的是,函数式接口只能有一个抽象方法,而不是只能有一个方法。这分两点来说明:首先,在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); }
函数式接口的实例可以由方法引用或者lambda表达式进行构造,这个我们将在后面进一步举例说明。
6.2.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("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单一继承的一些不便。但同时也要知道,它也将遇到和多继承相同的问题,如图6.2所示。如果IDonkey也存在一个默认的run()方法,那么同时实现它们的Mule,就会不知所措,因为它不知道应该以哪个方法为准。
图6-2 接口默认方法带来的多继承问题
增加一个IDonkey的实现:
public 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 eat"); } 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 public void run(){ IHorse.super.run(); } @Override public void eat() { System.out.println("Mule eat"); } 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);
6.2.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定义,程序依然可以编译通过!但千万不要以为这样你就可以修改num的值了。实际上,这只是Java 8做了一个掩人耳目的小处理,它会自动地将在lambda表达式中使用的变量视为final。因此,下述代码是可以编译通过的:
int num = 2; Function<Integer, Integer> stringConverter = (from) -> from * num; System.out.println(stringConverter.apply(3));
但是,如果像下面这么写,就不行:
int num = 2; Function<Integer, Integer> stringConverter = (from) -> from * num; num++; System.out.println(stringConverter.apply(3));
上述的num++会引起一个编译错误:
Local variable num defined in an enclosing scope must be final or effectively final
6.2.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=1;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().map(Double::toString).forEach(System.out::println); } }
上述代码试图将所有的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 void 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对象实例。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论