Java 函数式编程 :为什么要关心 Java8
为什么 Java 一直在变
日新月异的计算应用背景:多核和处理大型数据集(大数据)改进的压力:函数式比命令式更适应新的体系架构 Java 8 的核心新特性:Lambda(匿名函数)、流、默认方法为什么应该关心 Java8:因为 Java8 所做出的改进,比以往任意一个版本都要深远,并且让编程变得容易。平常的 CPU 都是多核心的,但是实际上只使用了一个核心,其他的核心都浪费掉了,而使用 Java8 可以方便的使用这些功能。使用 Java8 的新特性,可以写出更加简洁的代码,见名知意。在 Java8 中,接口支持默认方法、把代码传递给方法的简洁方式(方法引用、Lambda),其根本原因就是为了支持 Streams 什么是方法引用:方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。方法引用的本质就是:引用方法。函数式编程提供了一种新的方式,其简洁的表达了行为参数化。代码本身的含义就可以轻易的阅读处理,就像读一本漫画书一样轻松。它提供了更多的编程工具和概念,能以更快,更重要的是能以更为简洁、更易于维护的方式解决新的或现有的编程问题。
引用方法使用场景
我们用 Lambda 表达式来实现匿名方法,但是有些情况下,我们使用 Lambda 表达式仅仅是调用一些已经存在的方法,除了调用动作之外,没有其他任何多余的动作,在这种情况下,我们更倾向于通过方法名来调用它,而 Lambda 表达式可以帮助我们实现这一要求,它使得 Lambda 在调用那些已经拥有方法名的代码更加简洁、更容易理解。方法引用可以理解为 Lambda 表达式的另外一种表现形式。
方法引用的分类
方法引用使用举例静态方法引用
类名::静态方法 Person 类
java 复制代码 @Datapublic class Person {
}
class PersonAgeComparator implements Comparator<Person> {public int compare(Person a, Person b) {return a.getAge().compareTo(b.getAge());}}
现假设,一个部门有 30 人,把他们放在一个数组中,并按照年龄排序,可以自顶一个比较器,如上
java 复制代码 public class Client {public static void main(String[] args) {Random random = new Random();Person[] personList = new Person[30];for (int i = 0; i < 30; i++) {Person person = new Person();person.setName(String.valueOf(i));person.setAge(random.nextInt(40));personList[i] = person;}Arrays.sort(personList, new PersonAgeComparator());for (Person person : personList) {System.out.println(person);}}}
Arrays.sort 的声明为:public static void sort(T[] a, Comparator < ? super T > c),比较器参数 Comparator 为一个函数式接口,利用上一节 Lambda 表达式所学知识,可以改写为以下代码
java 复制代码 public class Client {public static void main(String[] args) {Random random = new Random();Person[] personList = new Person[30];for (int i = 0; i < 30; i++) {Person person = new Person();person.setName(String.valueOf(i));person.setAge(random.nextInt(40));personList[i] = person;}Arrays.sort(personList, (a, b) -> {return a.getAge().compareTo(b.getAge());});for (Person person : personList) {System.out.println(person);}}}
然而,你会发现,Perdon 类中已经有了一个静态方法的比较器:compareByAge,因此,我们改用 Person 类已经提供的比较器
java 复制代码 public class Client {public static void main(String[] args) {Random random = new Random();Person[] personList = new Person[30];for (int i = 0; i < 30; i++) {Person person = new Person();person.setName(String.valueOf(i));person.setAge(random.nextInt(40));personList[i] = person;}Arrays.sort(personList, (a,b) -> Person.compareByAge(a,b));for (Person person : personList) {System.out.println(person);}}}
以上代码,因为 Lambda 表达式调用了一个已经存在的静态方法,根据我们第 2 节表格中的语法,上面的代码可以最终改写成静态方法引用
java 复制代码 public class Client {public static void main(String[] args) {Random random = new Random();Person[] personList = new Person[30];for (int i = 0; i < 30; i++) {Person person = new Person();person.setName(String.valueOf(i));person.setAge(random.nextInt(40));personList[i] = person;}Arrays.sort(personList, Person::compareByAge);for (Person person : personList) {System.out.println(person);}}}
实例方法引用
实例::非静态方法实例方法引用,顾名思义就是调用已经存在的实例的方法,与静态方法引用不同的是类要先实例化,静态方法引用类无需实例化,直接用类名去调用。
java 复制代码 public class TestInstanceReference {public static void main(String[] args) {Person person = new Person();person.setName("icanci");person.setAge(18);Supplier<String> supplier = () -> person.getName();System.out.println(supplier.get());Supplier<String> supplier2 = person::getName;System.out.println(supplier2.get());}}
对象方法引用
实例::静态方法若 Lambda 参数列表中的第一个参数是实例方法的参数调用者,而第二个参数是实例方法的参数时,可以使用对象方法引用。String 的 equals()方法
java 复制代码 public boolean equals(Object anObject) {if (this == anObject) {return true;}if (anObject instanceof String) {String anotherString = (String)anObject;int n = value.length;if (n == anotherString.value.length) {char v1[] = value;char v2[] = anotherString.value;int i = 0;while (n-- != 0) {if (v1[i] != v2[i])return false;i++;}return true;}}return false;}
java 复制代码 public class StringTest {public static void main(String[] args) {BiPredicate<String, String> bp = (x, y) -> x.equals(y);BiPredicate<String, String> bp1 = String::equals;boolean test = bp1.test("xy", "xx");System.out.println(test);}}
BiPredicate 的 test()方法接受两个参数,x 和 y,具体实现为 x.equals(y),满足 Lambda 参数列表中的第一个参数是实例方法的参数调用者,而第二个参数是实例方法的参数,因此可以使用对象方法引用。
构造方法的引用
类名::new 注意:需要调用的构造器的参数列表要与函数式接口中抽象方法的参数列表保持一致如:要获取一个空的 Person 列表
java 复制代码 public class Client2 {public static void main(String[] args) {Supplier<List<Person>> supplier = () -> new ArrayList<Person>();List<Person> personList = supplier.get();Supplier<List<Person>> supplier2 = ArrayList<Person>::new;List<Person> personList1 = supplier2.get();}}
Java 中的函数
编程语言中的函数一词通常是指方法,尤其是静态方法;这是在数学函数,也就是没有副作用的函数之外的新含义。在 Java 8 谈到函数时,这两种用法几乎是一致的。Java 8 中新增了函数——值的一种新形式 Java 可以操作任何值,编程语言的整个目的就在于操作值,如果按照历史上编程语言的传统,这些值称为一等值。编程语言中的其他结构也许有助于我们表示值的结构,但在程序执行期间不能传递,因而是二等值。前面所说的值是 Java 中的一等值,但其他很多 Java 概念(如方法和类等)则是二等值。用方法来定义类很不错,类还可以实例化来产生值,但方法和类本身都不是值。这又有什么关系呢?还真有,人们发现,在运行时传递方法能将方法变成一等公民。方法和 Lambda 作为一等值 Scala 和 Groovy 等语言证明,让方法等概念称为一等值可以扩充程序员的工具库,让编程变得更加容易。其也成为 Stream 的基础。函数参见上文:引用方法方法引用:把这个方法作为值。与用对象引用传递对象类似(对象引用是用 new 创建的),在 Java 8 里写下 File::isHidden 的时候,你就创建了一个方法引用,你同样可以传递它,参见以下案例
java 复制代码 public class FileTest {public static void main(String[] args) {File[] files = new File(".").listFiles(new FileFilter() {@Overridepublic boolean accept(File file) {return file.isHidden();}});}}
java 复制代码 public class FileTest {public static void main(String[] args) {File[] files = new File(".").listFiles(File::isHidden);}}
Lambda 匿名函数 ,后续会讲使用这些概念的程序为函数式编程风格,这句话的意思是“编写把函数作为一等值来传递的程序”传递代码的例子有个 Apple 类和获取 Apple 的 Client
java 复制代码 @Datapublic class Apple {private String color;private int weight;}
java 复制代码 public class AppleClient {public static List<Apple> getApples() {List<Apple> apples = new ArrayList<>();
}
筛选绿色苹果
java 复制代码 public class Test1 {public static void main(String[] args) {System.out.println(filterGreenApples(AppleClient.getApples()));}
}// [Apple(color=green, weight=160)]
筛选超过 150g 的苹果
java 复制代码 public class Test1 {public static void main(String[] args) {System.out.println(filterHeavyApples(AppleClient.getApples()));}
}// [Apple(color=red, weight=180), Apple(color=green, weight=160)]
上述的方法只有一行不同,其他都是一样的,现在将其抽取出来,如下
java 复制代码 @Datapublic class Apple {private String color;private int weight;
}
java 复制代码 public class Test2 {public static void main(String[] args) {// 是不是非常简单了 System.out.println(filterApples(AppleClient.getApples(), Apple::isGreenApple));System.out.println(filterApples(AppleClient.getApples(), Apple::isHeavyApple));}
}
java 复制代码 public interface Predicate<T> {boolean test(T t);}
这只是初步了解这种爽快,后续会深入学习。从传递方法到 Lambda
java 复制代码 public class Test3 {public static void main(String[] args) {System.out.println(filterApples(AppleClient.getApples(), (Apple a) -> "green".equals(a.getColor())));System.out.println(filterApples(AppleClient.getApples(), (Apple a) -> a.getWeight() > 150));}public static List<Apple> filterApples(List<Apple> apples, Predicate<Apple> predicate) {List<Apple> arrayList = new ArrayList<>();for (Apple apple : apples) {if (predicate.test(apple)) {arrayList.add(apple);}}return arrayList;}}
如上,这已经不需要为只用 1 次的方法做定义;代码更加干净、更清晰,不需要去找自己传递了什么代码。如果不考虑并行,Stream 已经结束了。
流处理
Java 8 在 java.util.stream 中添加了一个 Stream API;Stream 就是一系列 T 类型的项目。可以把它看成一种比较花哨的迭代器。Stream API 的很多方法可以链接起来形成一个复杂的流水线。举例:在 Unix 或者 Linux 系统中,很多程序都支持标准输入。Unix 的 cat 命令会把两个文件连接起来创建一个流,tr 会转换流中的字符,sort 会对流中的行进行排序,而 tail-3 则给出流的最后三行。Unix 命令行允许这些程序通过管道(|)连接在一起,如下
java 复制代码 cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3
假设 file1 和 file2 每行都只有一个单词,会先把字母转成小写字母。然就打印了按照字段排序出现咋最后的三个单词请注意在 Unix 中,命令 cat、tr、sort 和 tail 是同时执行的,这样 sort 就可以在 cat 或者 tr 完成前先处理头几行。基于这种思想,Stream API。Stream API 的很多方法可以链接起来形成一个复杂的流水线,就像先前例子里面链接起来的 Unix 命令一样。现在你可以在一个更高的抽象层次上写 Java 8 程序了:思路变成了把这样的流变成那样的流(就像写数据库查询语句时的那种思路),而不是一次只处理一个项目。并且可以简单的方式处理并行。用行为参数化吧代码传递给方法并行与共享的可变数 Java 需要演变,才能持续立足于编程世界几乎每个 Java 应用都会制造和处理集合。但集合用起来并不总是那么理想。比方说,你需要从一个列表中筛选金额较高的交易,然后按货币分组。你需要写一大堆套路化的代码来实现这个数据处理命令。就和上述的筛选苹果一样,当然,苹果的案例只是简单案例。针对目标数据的遍历和取值,非常麻烦;而且对于大数据内存,单个 CPU 无法高效的处理,但是其他的 CPU 又用不上,这就是很头疼的问题。所以 Java8Stream 流,支持并行流。这个在后面章节会讲解。Java 的 Stream 解决了 2 个问题:集合处理时的套路和晦涩;以及难以利用多核。多线程很难,比如两个线程同时向共享变量 sum 加上一个数时,可能出现的问题
一些筛选操作可以并行化,如下图:在两个 CPU 上筛选列表,可以让一个 CPU 处理列表的前一半,第二个 CPU 处理后一半,这称为分支步骤(1)。CPU 随后对各自的半个列表做筛选(2)。最后(3),一个 CPU 会把两个结果合并起来。
到这里,我们只是说新的 Stream API 和 Java 现有的集合 API 的行为差不多:它们都能够访问数据项目的序列。不过,现在最好记得,Collection 主要是为了存储和访问数据,而 Stream 则主要用于描述对数据的计算。这里的关键点在于,Stream 允许并提倡并行处理一个 Stream 中的元素。稍微体验一下,如何利用 Stream 和 Lambda 表达式顺序或并行地从一个列表里筛选绿色的苹果。
java 复制代码// 顺序处理 public class Test4 {public static void main(String[] args) {AppleClient.getApples().stream().filter((Apple a) -> "green".equals(a.getColor())).collect(Collectors.toList());}}
java 复制代码// 并行处理 public class Test4 {public static void main(String[] args) {AppleClient.getApples().parallelStream().filter((Apple a) -> "green".equals(a.getColor())).collect(Collectors.toList());}}
Java 中并行和无共享可变状态
并行执行某段逻辑函数式编程中的函数的主要意思是“把函数作为一等值”,不过它也常常隐含着第二层意思,即“执行时在元素之间无互动”
可以友好的处理 NPE:空指针异常
为什么要新增默认方法和静态方法
为了适配 Stream 方法,因为集合在 Java 中最常用,如果在顶级接口 Collection 添加了一个方法,需要子类去实现,那将是整个 Java 生态的灾难,所有的版本要不不升级,要么全部的实现都需要修改。你如何改变已发布的接口而不破坏已有的实现呢?所以索性直接改 JDK,提供默认实现的方法,解决。使用 default 关键字修饰
小结
请记住语言生态系统的思想,以及语言面临的“要么改变,要么淘汰”的压力。虽然 Java 可能现在非常有活力,但你可以回忆一下其他曾经也有活力但未能及时改进的语言的命运,如 COBOL。Java 8 中新增的核心内容提供了令人激动的新概念和功能,方便我们编写既有效又简洁的程序。现有的 Java 编程实践并不能很好地利用多核处理器。函数是一等值;记得方法如何作为函数式值来传递,还有 Lambda 是怎样写的。Java 8 中 Streams 的概念使得 Collections 的许多方面得以推广,让代码更为易读,并允许并行处理流元素。你可以在接口中使用默认方法,在实现类没有实现方法时提供方法内容。
评论