java8函数式编程(转载)
1. 概述
1.1 函数式编程简介
我们最常⽤的⾯向对象编程(Java)属于命令式编程(Imperative Programming)这种编程范式。常见的编程范式还有逻辑式编程(Logic Programming),函数式编程(Functional Programming)。
函数式编程作为⼀种编程范式,在科学领域,是⼀种编写计算机程序数据结构和元素的⽅式,它把计算过程当做是数学函数的求值,⽽避免更改状态和可变数据。
函数式编程并⾮近⼏年的新技术或新思维,距离它诞⽣已有⼤概50多年的时间了。它⼀直不是主流的编程思维,但在众多的所谓顶级编程⾼⼿的科学⼯作者间,函数式编程是⼗分盛⾏的。
什么是函数式编程?简单的回答:⼀切都是数学函数。函数式编程语⾔⾥也可以有对象,但通常这些对象都是恒定不变的 —— 要么是函数参数,要什么是函数返回值。函数式编程语⾔⾥没有 for/next 循环,因为这些逻辑意味着有状态的改变。相替代的是,这种循环逻辑在函数式编程语⾔⾥是通过递归、把函数当成参数传递的⽅式实现的。
举个例⼦:
a = a + 1
这段代码在普通成员看来并没有什么问题,但在数学家看来确实不成⽴的,因为它意味着变量值得改变。
1.2 Lambda 表达式简介
Java 8的最⼤变化是引⼊了Lambda(Lambda 是希腊字母λ的英⽂名称)表达式——⼀种紧凑的、传递⾏为的⽅式。
先看个例⼦:
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("button clicked");
}
});
这段代码使⽤了匿名类。ActionListener是⼀个接⼝,这⾥ new 了⼀个类实现了ActionListener接⼝,然后重写了actionPerformed⽅法。actionPerformed⽅法接收ActionEvent类型参数,返回空。
这段代码我们其实只关⼼中间打印的语句,其他都是多余的。所以使⽤ Lambda 表达式,我们就可以简写为:
button.addActionListener(event -> System.out.println("button clicked"));
2. Lambda 表达式
2.1 Lambda 表达式的形式
Java 中 Lambda 表达式⼀共有五种基本形式,具体如下:
(1) Runnable noArguments = () -> System.out.println("Hello World");
(2) ActionListener oneArgument = event -> System.out.println("button clicked");
(3)Runnable multiStatement = () -> {
System.out.print("Hello");
System.out.println(" World");
};
(4) BinaryOperator<Long> add = (x, y) -> x + y;
(5) BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;
(1) 中所⽰的 Lambda 表达式不包含参数,使⽤空括号 () 表⽰没有参数。该 Lambda 表达式实现了 Runnable 接⼝,该接⼝也只有⼀个 run ⽅法,没有参数,且返回类型为void。(2)中所⽰的 Lambda 表达式包含且只包含⼀个参数,可省略参数的括号,这和例 2-2 中的形式⼀样。Lambda 表达式的主体不仅可以是⼀个表达式,⽽且也可以是⼀段代码块,使⽤⼤括号 ({})将代码块括起来,如(3)所⽰。该代码块和普通⽅法遵循的规则别⽆⼆致,可以⽤返回或抛出异常来退出。只有⼀⾏代码的 Lambda 表达式也可使⽤⼤括号,⽤以明确 Lambda表达式从何处开始、到哪⾥结束。Lambda 表达式也可以表⽰包含多个参数的⽅法,如(4)所⽰。这时就有必要思考怎样去阅读该 Lambda 表达式。这⾏代码并不是将两个数字相加,⽽是创建了⼀个函数,⽤来计算两个数字相加的结果。变量 add 的类型是 BinaryOperator
记住⼀点很重要,Lambda 表达式都可以扩写为原始的“匿名类”形式。所以当你觉得这个 Lambda 表达式很复杂不容易理解的时候,不妨把它扩写为“匿名类”形式来看。
2.2 闭包
如果你以前使⽤过匿名内部类,也许遇到过这样的问题。当你需要匿名内部类所在⽅法⾥的变量,必须把该变量声明为final。如下例⼦所⽰:
final String name = getUserName();
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("hi " + name);
}
});
Java 8放松了这⼀限制,可以不必再把变量声明为final,但其实该变量实际上仍然是final的。虽然⽆需将变量声明为 final,但在 Lambda 表达式中,也⽆法⽤作⾮终态变量。如果坚持⽤作⾮终态变量(即改变变量的值),编译器就会报错。
2.3 函数接⼝lambda编程
上⾯例⼦⾥提到了ActionListener接⼝,我们看⼀下它的代码:
public interface ActionListener extends EventListener {
/**  * Invoked when an action occurs.  */
public void actionPerformed(ActionEvent e);
}
ActionListener只有⼀个抽象⽅法:actionPerformed,被⽤来表⽰⾏为:接受⼀个参数,返回空。记住,由于actionPerformed定义在⼀个接⼝⾥,因此abstract关键字不是必需的。该接⼝也继承⾃⼀个不具有任何⽅法的⽗接⼝:EventListener。
我们把这种接⼝就叫做函数接⼝。
JDK 8 中提供了⼀组常⽤的核⼼函数接⼝:
接⼝参数返回类型描述
Predicate<T>T boolean⽤于判别⼀个对象。⽐如求⼀个⼈是否为男性
Consumer<T>T void⽤于接收⼀个对象进⾏处理但没有返回,⽐如接收⼀个⼈并打印他的名字
Function<T, R>T R转换⼀个对象为不同类型的对象
Supplier<T>None T提供⼀个对象
UnaryOperator<T>T T接收对象并返回同类型的对象
BinaryOperator<T>(T, T)T接收两个同类型的对象,并返回⼀个原类型对象
其中Cosumer与Supplier对应,⼀个是消费者,⼀个是提供者。
Predicate⽤于判断对象是否符合某个条件,经常被⽤来过滤对象。
Function是将⼀个对象转换为另⼀个对象,⽐如说要装箱或者拆箱某个对象。
UnaryOperator接收和返回同类型对象,⼀般⽤于对对象修改属性。BinaryOperator则可以理解为合并对象。
如果以前接触过⼀些其他 Java 框架,⽐如 Google Guava,可能已经使⽤过这些接⼝,对这些东西并不陌⽣。所以,其实 Java 8 的改进并不是闭门造车,⽽是集百家之长。
3. 集合处理
3.1 Stream 简介
在程序编写过程中,集合的处理应该是很普遍的。Java 8 对于Collection的处理花了很⼤的功夫,如果从 JDK 7 过渡到 JDK 8,这⼀块也可能是我们感受最为明显的。Java 8 中,引⼊了流(Stream)的概念,这个流和以前我们使⽤的 IO 中的流并不太相同。
所有继承⾃Collection的接⼝都可以转换为Stream。还是看⼀个例⼦。
假设我们有⼀个List包含⼀系列的Person,Person有姓名name和年龄age连个字段。现要求这个列表中年龄⼤于 20 的⼈数。
通常按照以前我们可能会这么写:
long count = 0;
for (Person p : persons) {
if (p.getAge() > 20) {
count ++;
}
}
但如果使⽤stream的话,则会简单很多:
long count = persons.stream()
.filter(person -> Age() > 20)
.count();
这只是stream的很简单的⼀个⽤法。现在链式调⽤⽅法算是⼀个主流,这样写也更利于阅读和理解编写者的意图,⼀步⽅法做⼀件事。
3.2 Stream 常⽤操作
Stream的⽅法分为两类。⼀类叫惰性求值,⼀类叫及早求值。
判断⼀个操作是惰性求值还是及早求值很简单:只需看它的返回值。如果返回值是 Stream,那么是惰性求值。其实可以这么理解,如果调⽤惰性求值⽅法,Stream只是记录下了这个惰性求值⽅法的过程,并没有去计算,等到调⽤及早求值⽅法后,就连同前⾯的⼀系列惰性求值⽅法顺序进⾏计算,返回结果。
通⽤形式为:
Stream.惰性求值.惰性求值. ... .惰性求值.及早求值
整个过程和建造者模式有共通之处。建造者模式使⽤⼀系列操作设置属性和配置,最后调⽤⼀个 build ⽅法,这时,对象才被真正创建。
3.2.1 collect(toList())
collect(toList())⽅法由Stream⾥的值⽣成⼀个列表,是⼀个及早求值操作。可以理解为Stream向Collection的转换。
注意这边的toList()其实是List(),因为采⽤了静态倒⼊,看起来显得简洁。
List<String> collected = Stream.of("a", "b", "c")
.List());
assertEquals(Arrays.asList("a", "b", "c"), collected);
3.2.2 map
如果有⼀个函数可以将⼀种类型的值转换成另外⼀种类型,map操作就可以使⽤该函数,将⼀个流中的值转换成⼀个新的流。
List<String> collected = Stream.of("a", "b", "hello")
.map(string -> UpperCase())
.collect(toList());
assertEquals(asList("A", "B", "HELLO"), collected);
map⽅法就是接受的⼀个Function的匿名函数类,进⾏的转换。
3.2.3 filter
遍历数据并检查其中的元素时,可尝试使⽤Stream中提供的新⽅法filter。
List<String> beginningWithNumbers =
Stream.of("a", "1abc", "abc1")
.filter(value -> isDigit(value.charAt(0)))
.collect(toList());
assertEquals(asList("1abc"), beginningWithNumbers);
filter⽅法就是接受的⼀个Predicate的匿名函数类,判断对象是否符合条件,符合条件的才保留下来。
3.2.4 flatMap
flatMap⽅法可⽤Stream替换值,然后将多个Stream连接成⼀个Stream。
List<Integer> together = Stream.of(asList(1, 2), asList(3, 4))
.flatMap(numbers -> numbers.stream())
.collect(toList());
assertEquals(asList(1, 2, 3, 4), together);
flatMap最常⽤的操作就是合并多个Collection。
3.2.5 max和min
Stream上常⽤的操作之⼀是求最⼤值和最⼩值。Stream API 中的max和min操作⾜以解决这⼀问题。
List<Integer> list = wArrayList(3, 5, 2, 9, 1);
int maxInt = list.stream()
.max(Integer::compareTo)
.get();
int minInt = list.stream()
.min(Integer::compareTo)
.get();
assertEquals(maxInt, 9);
assertEquals(minInt, 1);
这⾥有 2 个要点需要注意:
1. max和min⽅法返回的是⼀个Optional对象(对了,和 Google Guava ⾥的 Optional 对象是⼀样的)。Optional对象封装的就是实际的值,可能为空,所以保险起
见,可以先⽤isPresent()⽅法判断⼀下。Optional的引⼊就是为了解决⽅法返回null的问题。
2. Integer::compareTo也是属于 Java 8 引⼊的新特性,叫做⽅法引⽤(Method References)。在这边,其实就是(int1, int2) -> int1pareTo(int2)的简写,可以⾃⼰查阅了
解,这⾥不再多做赘述。
3.2.6 reduce
reduce操作可以实现从⼀组值中⽣成⼀个值。在上述例⼦中⽤到的count、min和max⽅法,因为常⽤⽽被纳⼊标准库中。事实上,这些⽅法都是reduce操作。
上图展⽰了reduce进⾏累加的⼀个过程。具体的代码如下:
int result = Stream.of(1, 2, 3, 4)
.reduce(0, (acc, element) -> acc + element);
assertEquals(10, result);
注意reduce的第⼀个参数,这是⼀个初始值。0 + 1 + 2 + 3 + 4 = 10。
如果是累乘,则为:
int result = Stream.of(1, 2, 3, 4)
.reduce(1, (acc, element) -> acc * element);
assertEquals(24, result);
因为任何数乘以 1 都为其⾃⾝嘛。1 * 1 * 2 * 3 * 4 = 24。
Stream的⽅法还有很多,这⾥列出的⼏种都是⽐较常⽤的。Stream还有很多通⽤⽅法,具体可以查阅 Java 8 的 API ⽂档。
3.3 数据并⾏化操作
Stream的并⾏化也是 Java 8 的⼀⼤亮点。数据并⾏化是指将数据分成块,为每块数据分配单独的处理单元。这样可以充分利⽤多核 CPU 的优势。
并⾏化操作流只需改变⼀个⽅法调⽤。如果已经有⼀个Stream对象,调⽤它的parallel()⽅法就能让其拥有并⾏操作的能⼒。如果想从⼀个集合类创建⼀个流,调
⽤parallelStream()就能⽴即获得⼀个拥有并⾏能⼒的流。
int sumSize = Stream.of("Apple", "Banana", "Orange", "Pear")
.parallel()
.map(s -> s.length())
.reduce(Integer::sum)
.get();
assertEquals(sumSize, 21);
这⾥求的是⼀个字符串列表中各个字符串长度总和。
如果你去计算这段代码所花的时间,很可能⽐不加上parallel()⽅法花的时间更长。这是因为数据并⾏化会先对数据进⾏分块,然后对每块数据开辟线程进⾏运算,这些地⽅会花费额外的时间。并⾏化操作只有在数据规模⽐较⼤或者数据的处理时间⽐较长的时候才能体现出有事,所以并不是每个地⽅都需要让数据并⾏化,应该具体问题具体分析。
3.4 其他
3.4.1 收集器
Stream转换为List是很常⽤的操作,其他Collectors还有很多⽅法,可以将Stream转换为Set, 或者将数据分组并转换为Map,并对数据进⾏处理。也可以指定转换为具体类型,如ArrayList, LinkedList或者HashMap。甚⾄可以⾃定义Collectors,编写⾃⼰的收集器。
3.4.2 元素顺序
另外⼀个尚未提及的关于集合类的内容是流中的元素以何种顺序排列。⼀些集合类型中的元素是按顺序排列的,⽐如 List;⽽另⼀些则是⽆序的,⽐如 HashSet。增加了流操作后,顺序问题变得更加复杂。
总之记住。如果集合本⾝就是⽆序的,由此⽣成的流也是⽆序的。⼀些中间操作会产⽣顺序,⽐如对值做映射时,映射后的值是有序的,这种顺序就会保留下来。如果进来的流是⽆序的,出去的流也是⽆序的。
如果我们需要对流中的数据进⾏排序,可以调⽤sorted⽅法:
List<Integer> list = wArrayList(3, 5, 1, 10, 8);
List<Integer> sortedList = list.stream()
.sorted(Integer::compareTo)
.List());
assertEquals(sortedList, wArrayList(1, 3, 5, 8, 10));
3.4.3 @FunctionalInterface
我们讨论过函数接⼝定义的标准,但未提及 @FunctionalInterface 注释。事实上,每个⽤作函数接⼝的接⼝都应该添加这个注释。
但 Java 中有⼀些接⼝,虽然只含⼀个⽅法,但并不是为了使⽤ Lambda 表达式来实现的。⽐如,有些对象内部可能保存着某种状态,使⽤带有⼀个⽅法的接⼝可能纯属巧合。
该注释会强制 javac 检查⼀个接⼝是否符合函数接⼝的标准。如果该注释添加给⼀个枚举类型、类或另⼀个注释,或者接⼝包含不⽌⼀个抽象⽅法,javac 就会报错。重

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。