1 函数式编程简介
我们最常用的面向对象编程(Java)属于命令式编程(Imperative Programming)这种编程范式。常见的编程范式还有逻辑式编程(Logic Programming),函数式编程(Functional Programming)。
首先我们来解释下这三者的含义:
1.1命令式编程:
命令式编程的主要思想是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么。我们最常用的面向对象编程(Java)就属于命令式编程(Imperative Programming)这种编程范式。
比如:如果你想在一个数字集合 collection(变量名) 中筛选大于 5 的数字,你需要这样告诉计算机:
- 第一步,创建一个存储结果的集合变量 results;
- 第二步,遍历这个数字集合 collection;
- 第三步:一个一个地判断每个数字是不是大于 5,如果是就将这个数字添加到结果集合变量 results 中。
代码实现如下:
Listresults = new List (); for(int num : collection){ if (num > 5) results.add(num); }
1.2声明式编程:
声明式编程是以数据结构的形式来表达程序执行的逻辑。它的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。
SQL 语句就是最明显的一种声明式编程的例子,例如:
SELECT * FROM collection WHERE num > 5
除了 SQL,网页编程中用到的 HTML 和 CSS 也都属于声明式编程。
通过观察声明式编程的代码我们可以发现它有一个特点是它不需要创建变量用来存储数据,另一个特点是它不包含循环控制的代码如 for, while。
1.3函数式编程:
什么是函数式编程?简单的回答:一切都是数学函数。函数式编程语言里也可以有对象,但通常这些对象都是恒定不变的 —— 要么是函数参数,要什么是函数返回值。函数式编程语言里没有 for/next 循环,因为这些逻辑意味着有状态的改变。相替代的是,这种循环逻辑在函数式编程语言里是通过递归、把函数当成参数传递的方式实现的。
java1.8中的lambda表达式可以实现函数式编程,如下:
Listresults = collection.stream() .filter(n -> n > 5) .collect(Collectors.toList());
对比后可以发现,相比于以前的命令式编程,在 Java 中函数式编程的方法可以让代码的逻辑更清晰更优雅。
函数式编程中,经常会提到的几个特点:
- 函数式一等公民(一个值可以作为参数传递,可以从子程序中返回,可以赋值给变量,就称它为一等公民)
- 惰性计算(即表达式不是在绑定到变量时立即计算,而是在求值程序需要产生表达式的值时进行计算,联系java实际,即stream流内容只有在终端操作时,才开始计算)
- 尾递归代替循环
尾调用之所以与其他调用不同,就在于它的特殊的调用位置。 我们知道,函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A, B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"(call stack)。 尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。 函数调用自身,称为递归。如果尾调用自身,就称为尾递归。 递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。
2. 优势
2.1 代码简洁、开发快速
最简单的例子其实是日常中我们多表连续查询。
命令式编程代码如下:
public static List getVendorAccountIds(List list) {
if (CollectionUtils.isEmpty(list)) {
return Collections.emptyList();
}
Set set = new HashSet<>();
for (PerformanceOrderDO performanceOrderDO : list) {
if (performanceOrderDO.getVendorId() != null) {
set.add(performanceOrderDO.getVendorId());
}
}
return new ArrayList<>(set);
}
转换成函数式编程,如下:
public static List getVendorAccountIds(List list) {
return list.stream().map(PerformanceOrderDO::getVendorId).distinct().collect(Collectors.toList());
}
2.2 易于理解,抽象度高
比如我们实际开发中,遇到一个要验证区间无重合的场景
命令式编程实例代码如下:
private void checkOverlap(LinkedList activeExtendDOList) {
activeExtendDOList.sort(Comparator.comparing(ActiveExtendDTO::getCommodityMinNum));
Long temp = activeExtendDOList.poll().getCommodityMaxNum();
for (ActiveExtendDTO activeExtend : activeExtendDOList) {
if (temp >= activeExtend.getCommodityMinNum()) {
throw new Exception("区间有重叠");
}
temp = activeExtend.getCommodityMaxNum();
}
}
函数式编程实例代码如下:
private void checkOverlap2(Listlist) { list.stream().sorted(Comparator.comparing(ActiveExtendDTO::getCommodityMinNum)) .reduce((a, b) -> { if (a.getCommodityMaxNum() >= b.getCommodityMinNum()) { throw new Exception("区间有重叠"); } return b; }); }
2.3方便管理,便于维护(不修改状态,无副作用)
简单来讲,函数式编程抽象于函数,对指定输入用于给出一样的输出,不依赖于外部变量。结合java开发实际情况,stream流过程中要求使用到的外部变量为fina
如果一个函数内外有依赖于外部变量或者环境时,常常我们称之为其有副作用,如果我们仅通过函数签名不打开内部代码检查并不能知道该函数在干什么,作为一个独立函数我们期望有明确的输入和输出,副作用是bug的发源地,作为程序员开发者应尽量少的开发有副作用的函数或方法,副作用也使得方法通用性下降不适合扩展和可重用性。
2.4易于进行并发开发
函数式编程因为它不修改变量,所以根本不存在"锁"线程的问题,不需要考虑"死锁"(deadlock)。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程中。毕竟,无共享变量
2.5代码热升级
函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。所以,可以在运行状态下直接升级代码,不需要重启,也不需要停机。Erlang语言早就证明了这一点,它是瑞典爱立信公司为了管理电话系统而开发的,电话系统的升级当然是不能停机的,即java
3. 概念分析
3.1 闭包
闭包就是能够读取其他函数内部变量的函数。
闭包这个词,更多出现于javascript。我们无妨先来理解下他在JavaScript中的实现:
function f1(){ var n=999; function f2(){ alert(n); // 999 } }
上述代码中,f2就是一个闭包。
在java8中,闭包的实现如下:
private void test(Listnumbers) { final int factor = 3; numbers.stream() .map(e -> e * factor) .collect(Collectors.toList()); }
其中e->e*factor就是一个闭包。如果还不能理解,可以看下匿名函数部分
3.2 高阶函数
高阶函数即可以把函数作为参数或者返回值的函数。
同样,为了便于理解,我们先看JavaScript中的实现
function add(x, y, f) { return f(x) + f(y); }
以下场景的ajax也是一种高阶函数的运用,如下:
var getUserInfo = function( userId, callback ){ $.ajax( 'http://xx.com/getUserInfo?' + userId, function( data ){ if ( typeof callback === 'function' ){ callback( data ); } }); } getUserInfo( 123, function( data ){ alert ( data.userName ); });
接下来让我们想继续看java8中的高阶函数:
private void test(Listnumbers) { numbers.stream() .map(e -> e ==2) .collect(Collectors.toList()); }
其中e->e==2就是一个高阶函数,他接受的是另一个函数,即Function。(Function类其实是一个函数式接口,具体定义看此处不谈)
3.3 匿名函数
作为一个成熟的java开发人员,匿名函数这个东西,定义就不多提了。我们简单举例下:
new Thread(new Runnable() { public void run() { System.out.println("hello"); } }).start();
这是大家在java1.8前中的常见写法,而在java1.8引入函数式编程的概念后,提供了语法糖来简化这个代码,如下:
new Thread(() -> {System.out.println("hello");}).start();
可以看到这段代码比上面创建线程的代码精简了很多,也有很好的可读性。
() -> {System.out.println("hello");} 就是lambda表达式,等同于上面的new Runnable(), lambda大体分为3部分:
- 最前面的部分是一对括号,里面是参数,这里无参数,就是一对空括号
- 中间的是 -> ,用来分割参数和body部分。
- body部分可以是一个表达式或者一个语句块。如果是一个表达式,表达式的值会被作为返回值返回;如果是语句块,需要用return语句指定返回值。
3.4 柯里化与偏函数
柯里化,又译为卡瑞化
或加里化
。在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
柯里化或偏函数主要是对于参数进行一些操作,将多个参数转换为单一参数
或者减少参数个数
的过程。如果参数不足的话它们就会处在一种中间状态
,我们可以利用这种中间状态做任何事!!!而传统函数调用则需要预先确定所有实参。如果你在代码某一处只获取了部分实参,然后在另一处确定另一部分实参,这个时候柯里化和偏应用就能派上用场。
归纳下来,主要为以下常见的三个用途:
(1)动态生成函数
(2)减少参数
(3)延迟计算
照例,我们先看下JavaScript中的实现:
function add(a, b, c) { return a + b + c; } console.log(add(2, 3, 5)); // 10
如果上述代码要实现柯里化,则转换为如下代码:
function add(a) { return function(b) { return function(c) { return a+b+c; } } } console.log(add(2)(3)(5)) //10
此处也可使用偏函数,如下:
function partialAdd(b, c) { return add(2,b,c); } console.log(add(3, 5)); // 10
最后我们看下java8中函数柯里化的实现,首先,原函数如下:
private Integer calcule(Integer x, Integer y, Integer z) { return (x + y) * z; }
柯里化方式有以下三种:
- 第一种方式,嵌套多层Function
private Integer calcule(Integer x, Integer y, Integer z) { Function>> currying = a -> b -> c -> (a+b)*c; return currying.apply(x).apply(y).apply(z); }
- 第二种方式,需要先定义一个TriFunction函数接口:
@FunctionalInterface public interface TriFunction { /** * Applies this function to the given arguments. * @param * @param* @param*@return the function result */ R apply(T t, U u, S s); }
然后借助TriFunction来实现柯里化:
private Integer calcule(Integer x, Integer y, Integer z) { TriFunctiontriFunction = (a,b,c) -> (a+b)*c; return currying.apply(x).apply(y).apply(z); }
- 第三种方式,借助匿名内部类,每次调用都返回一个新的函数:
Function>> currying = new Function >>() { @Override public Function > apply(Integer x) { return new Function >() { @Override public Function apply(Integer y) { return new Function () { @Override public Integer apply(Integer z) { return (x + y) * z; } }; } }; } }; System.out.println(currying.apply(4).apply(5).apply(6));
注:此处笔者认为原文中第一种与第二种实现实际上是一种方式。
3.5 语法糖
Java 中对于函数式编程的支持,大多在于Lambda表达式上,而 Lambda 表达式一共有五种基本形式,具体如下:
//➊
Runnable noArguments = () -> System.out.println("Hello World");
//➋
ActionListener oneArgument = event -> System.out.println("button clicked");
//➌
Runnable multiStatement = () -> { System.out.print("Hello"); System.out.println(" World"); };
//➍
BinaryOperatoradd = (x, y) -> x + y;
//➎
BinaryOperatoraddExplicit = (Long x, Long y) -> x + y;
➊中所示的 Lambda 表达式不包含参数,使用空括号 () 表示没有参数。该 Lambda 表达式 实现了 Runnable 接口,该接口也只有一个 run 方法,没有参数,且返回类型为 void。
➋中所示的 Lambda 表达式包含且只包含一个参数,可省略参数的括号。
➌中所示的Lambda 表达式的主体是一段代码块,使用大括号 ({})将代码块括起来,如所示。该代码块和普通方法遵循的规则别无二致,可以用返回(return)或抛出异常来退出。只有一行代码的 Lambda 表达式也可使用大括号,用以明确 Lambda表达式从何处开始、到哪里结束。
➍中所示Lambda 表达式也可以表示包含多个参数的方法,这时就有必要思考怎样去阅读该 Lambda 表达式。这行代码并不是将两个数字相加,而是创建了一个函数,用来计算 两个数字相加的结果。变量 add 的类型是 BinaryOperator,它不是两个数字的和, 而是将两个数字相加的那行代码。
➎中所示的Lambda 表达式显式声明了参数类型,此时就需要使用小括号将参数括起来。
4. Java8中的函数式接口
4.1 定义
接口中只有一个抽象方法时,那么这个接口叫做函数式接口。函数式接口可以使用@FunctionalInterface修饰,jvm会自动检查该接口是否为函数式接口。
实际在java8中,Lamada表达式的入参,均为函数式接口,java8中在java.util.function包中内置了大量函数式接口,供我们快速开发,并提供对应stream流方法,具体函数式接口如下
4.2 五种核心函数式接口
1.Predicates断言式接口
Predicate函数式接口的主要作用就是提供一个test方法,接受一个参数返回一个布尔类型,Predicate在stream api中进行一些判断的时候非常常用。实际使用中,我们用到的filter内,接受的就是此类型。源码如下:
@FunctionalInterface public interface Predicate{ /** * 核心方法,判断 */ boolean test(T t); /** * and */ default Predicate and(Predicate super T> other) { Objects.requireNonNull(other); return (t) -> test(t) && other.test(t); } /** * 取反 */ default Predicate negate() { return (t) -> !test(t); } /** * or */ default Predicate or(Predicate super T> other) { Objects.requireNonNull(other); return (t) -> test(t) || other.test(t); } /** * 比较 */ static Predicate isEqual(Object targetRef) { return (null == targetRef) ? Objects::isNull : object -> targetRef.equals(object); } }
可以看到这个接口中实际并不只一个方法,其中isEqual为静态方法略过,剩余三个方法实际为defalut方法,已在此接口内实现,defaul为t虚拟扩展方法。具体是指在接口内部包含了一些默认的方法实现(也就是接口中可以包含方法体,这打破了Java之前版本对接口的语法限制),从而使得接口在进行扩展的时候,不会破坏与接口相关的实现类代码。
2.Function函数型接口
简单来讲,Function函数型接口主要作用是提供一个apply方法,输入一个类型得参数,输出一个类型得参数,当然两种类型可以一致。实际使用中,我们用到的map,接受的就是此类型参数
源码如下:
@FunctionalInterface public interface Function{ /** * 将参数赋予给相应方法 * * @param t * @return */ R apply(T t); /** * 先执行参数(即也是一个Function)的,再执行调用者(同样是一个Function) */ default Function compose(Function super V, ? extends T> before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); } /** * 先执行调用者,再执行参数,和compose相反。 */ default Function andThen(Function super R, ? extends V> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); } /** * 返回当前正在执行的方法 */ static Function identity() { return t -> t; } }
3.Supplier供给型接口
简单来讲,Supplier供给型接口主要作用是提供一个get方法,用于创建对象。示例如下:
public void test(){ Suppliersupplier = ()->new User(); User user = supplier.get(); logger.info(user.toString()); logger.info(supplier.get().toString()); }
源码如下:
@FunctionalInterface public interface Supplier{ /** * Gets a result. * * @return a result */ T get(); }
实际使用中,我们可以把耗资源运算放到get方法里,在程序里,我们传递的是Supplier对象,直到调用get方法时,运算才会执行。这就是所谓的惰性求值。
4.Consumer消费型接口
简单来讲,Consumer消费型接口主要作用是提供一个accept方法,用于消费对象,实际stream流的使用者,foreach和peek方法,接受此类型。源码如下:
@FunctionalInterface public interface Consumer{ /** * 可实现方法,接受一个参数且没有返回值 */ void accept(T t); /** * 默认方法,提供链式调用方式执行。执行流程:先执行本身的accept在执行传入参数after.accept方法。 * 该方法会抛出NullPointerException异常。 * 如果在执行调用链时出现异常,会将异常传递给调用链功能的调用者,且发生异常后的after将不会在调用。 */ default Consumer andThen(Consumer super T> after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t); }; } }
5.Operator算子接口
Operator其实就是Function,函数有时候也叫作算子。算子在Java8中接口描述更像是函数的补充,和上面的很多类型映射型函数类似。它包含UnaryOperator和BinaryOperator。分别对应单元算子和二元算子。对应源码如下:
@FunctionalInterface public interface UnaryOperatorextends Function { static java.util.function.UnaryOperator identity() { return t -> t; } } @FunctionalInterface public interface BinaryOperator extends BiFunction { public static java.util.function.BinaryOperator minBy(Comparator super T> comparator) { Objects.requireNonNull(comparator); return (a, b) -> comparator.compare(a, b) <= 0 ? a : b; } public static java.util.function.BinaryOperator maxBy(Comparator super T> comparator) { Objects.requireNonNull(comparator); return (a, b) -> comparator.compare(a, b) >= 0 ? a : b; } } !
实际reduce方法入参即为二原算子
其余所有函数式接口,均可视为上述五种的扩充。
5. Java8中的Stream流
函数式编程说了这么多,落地到实际使用,个人感觉还是要回到stream流中。我们先聊定义
5.1 Stream流的理解
首先对stream的操作可以分为两类,中间操作(intermediate operations)和结束操作(terminal operations):
- 中间操作总是会惰式执行,调用中间操作只会生成一个标记了该操作的新stream。
- 结束操作会触发实际计算,计算发生时会把所有中间操作积攒的操作以pipeline的方式执行,这样可以减少迭代次数。计算完成之后stream就会失效。
虽然大部分情况下stream是容器调用Collection.stream()方法得到的,但stream和collections有以下不同:
- 无存储。stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。
- 为函数式编程而生。对stream的任何修改都不会修改背后的数据源,比如对stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新stream。
- 惰式执行。stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
- 可消费性。stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。
- 使用parallelStream可开启并行流
具体流操作可见下图
5.2 Stream流的方法介绍
1. stream() / parallelStream()
最常用到的方法,将集合转换为流
List list = new ArrayList(); // return Streamlist.stream();
而 parallelStream() 是并行流方法,能够让数据集执行并行操作。
2. filter(T -> boolean)
保留 boolean 为 true 的元素
保留年龄为 20 的 person 元素 list = list.stream() .filter(person -> person.getAge() == 20) .collect(toList()); 打印输出 [Person{name='jack', age=20}]
collect(toList()) 可以把流转换为 List 类型,这个以后会讲解
3. distinct()
去除重复元素,这个方法是通过类的 equals 方法来判断两个元素是否相等的,如例子中的 Person 类,需要先定义好 equals 方法,不然类似[Person{name='jack', age=20}, Person{name='jack', age=20}]
这样的情况是不会处理的
4. sorted() / sorted((T, T) -> int)
如果流中的元素的类实现了 Comparable 接口,即有自己的排序规则,那么可以直接调用 sorted() 方法对元素进行排序,如 Streamsorted((T, T) -> int)
实现 Comparator 接口
//根据年龄大小来比较:
list = list.stream() .sorted((p1, p2) -> p1.getAge() - p2.getAge()) .collect(toList());
当然这个可以简化为
list = list.stream()
.sorted(Comparator.comparingInt(Person::getAge))
.collect(toList());
5. limit(long n)
返回前 n 个元素
list = list.stream() .limit(2) .collect(toList()); 打印输出 [Person{name='jack', age=20}, Person{name='mike', age=25}]
6. skip(long n)
去除前 n 个元素
list = list.stream() .skip(2) .collect(toList()); 打印输出 [Person{name='tom', age=30}]
tips:
- 用在 limit(n) 前面时,先去除前 m 个元素再返回剩余元素的前 n 个元素
- limit(n) 用在 skip(m) 前面时,先返回前 n 个元素再在剩余的 n 个元素中去除 m 个元素
list = list.stream() .limit(2) .skip(1) .collect(toList()); 打印输出 [Person{name='mike', age=25}]
7. map(T -> R)
将流中的每一个元素 T 映射为 R(类似类型转换)
Listnewlist = list.stream().map(Person::getName).collect(toList());
newlist 里面的元素为 list 中每一个 Person 对象的 name 变量
8. flatMap(T -> Stream)
将流中的每一个元素 T 映射为一个流,再把每一个流连接成为一个流
Listlist = new ArrayList<>(); list.add("aaa bbb ccc"); list.add("ddd eee fff"); list.add("ggg hhh iii"); list = list.stream().map(s -> s.split(" ")).flatMap(Arrays::stream).collect(toList());
上面例子中,我们的目的是把 List 中每个字符串元素以" "分割开,变成一个新的 List
首先 map 方法分割每个字符串元素,但此时流的类型为 StreamArrays::stream
将每个 String[ ] 元素变成一个 Stream
9. anyMatch(T -> boolean)
流中是否有一个元素匹配给定的 T -> boolean
条件
是否存在一个 person 对象的 age 等于 20: boolean b = list.stream().anyMatch(person -> person.getAge() == 20);
10. allMatch(T -> boolean)
流中是否所有元素都匹配给定的 T -> boolean
条件
11. noneMatch(T -> boolean)
流中是否没有元素匹配给定的 T -> boolean
条件
12. findAny() 和 findFirst()
- findAny():找到其中一个元素 (使用 stream() 时找到的是第一个元素;使用 parallelStream() 并行时找到的是其中一个元素)
- findFirst():找到第一个元素
值得注意的是,这两个方法返回的是一个 Optional
13. reduce((T, T) -> T) 和 reduce(T, (T, T) -> T)
用于组合流中的元素,如求和,求积,求最大值等
计算年龄总和: int sum = list.stream().map(Person::getAge).reduce(0, (a, b) -> a + b); 与之相同: int sum = list.stream().map(Person::getAge).reduce(0, Integer::sum);
其中,reduce 第一个参数 0 代表起始值为 0,lambda (a, b) -> a + b
即将两值相加产生一个新值,同样地:
计算年龄总乘积: int sum = list.stream().map(Person::getAge).reduce(1, (a, b) -> a * b);
当然也可以
Optionalsum = list.stream().map(Person::getAge).reduce(Integer::sum);
即不接受任何起始值,但因为没有初始值,需要考虑结果可能不存在的情况,因此返回的是 Optional 类型
13. count()
返回流中元素个数,结果为 long 类型
14. collect()
收集方法,我们很常用的是 collect(toList())
,当然还有 collect(toSet())
等,参数是一个收集器接口,这个后面会另外讲
15. forEach()
返回结果为 void,很明显我们可以通过它来干什么了,比方说:
### 16. unordered()
还有这个比较不起眼的方法,返回一个等效的无序流,当然如果流本身就是无序的话,那可能就会直接返回其本身
打印各个元素:
list.stream().forEach(System.out::println);
再比如说 MyBatis 里面访问数据库的 mapper 方法:
向数据库插入新元素:
list.stream().forEach(PersonMapper::insertPerson);
5.2 Stream流的收集
coollect 方法作为终端操作,接受的是一个 Collector 接口参数,能对数据进行一些收集归总操作
最常用的方法,把流中所有元素收集到一个 List, Set 或 Collection 中
- toList
- toSet
- toMap
List newlist = list.stream.collect(toList());
具体汇总如下
(1)counting
用于计算总和:
long l = list.stream().collect(counting());
没错,你应该想到了,下面这样也可以:
long l = list.stream().count();
推荐第二种,实际上日常使用大多不会选择第一种。
(2)summingInt ,summingLong ,summingDouble
summing,没错,也是计算总和,不过这里需要一个函数参数,计算 Person 年龄总和:
int sum = list.stream().collect(summingInt(Person::getAge));
当然,这个可以也简化为:
int sum = list.stream().mapToInt(Person::getAge).sum();
除了上面两种,其实还可以:
int sum = list.stream().map(Person::getAge).reduce(Interger::sum).get();
推荐第二种,由此可见,函数式编程通常提供了多种方式来完成同一种操作
(3)averagingInt,averagingLong,averagingDouble
看名字就知道,求平均数
Double average = list.stream().collect(averagingInt(Person::getAge));
当然也可以这样写
OptionalDouble average = list.stream().mapToInt(Person::getAge).average();
不过要注意的是,这两种返回的值是不同类型的。推荐第二种
(4)summarizingInt,summarizingLong,summarizingDouble
这三个方法比较特殊,比如 summarizingInt 会返回 IntSummaryStatistics 类型
IntSummaryStatistics l = list.stream().collect(summarizingInt(Person::getAge));
IntSummaryStatistics 包含了计算出来的平均值,总数,总和,最值,可以通过下面这些方法获得相应的数据
3. 取最值
maxBy,minBy 两个方法,需要一个 Comparator 接口作为参数
Optionaloptional = list.stream().collect(maxBy(comparing(Person::getAge)));
我们也可以直接使用 max 方法获得同样的结果
Optionaloptional = list.stream().max(comparing(Person::getAge));
4. joining 连接字符串
也是一个比较常用的方法,对流里面的字符串元素进行连接,其底层实现用的是专门用于字符串连接的 StringBuilder。如果list是一个如下的数组["jack","mike","tom
"]
String s = list.stream().map(Person::getName).collect(joining()); 结果:jackmiketom String s = list.stream().map(Person::getName).collect(joining(",")); 结果:jack,mike,tom
joining 还有一个比较特别的重载方法:
String s = list.stream().map(Person::getName).collect(joining(" and ", "Today ", " play games."));
结果:Today jack and mike and tom play games.
即 Today 放开头,play games. 放结尾,and 在中间连接各个字符串
5. groupingBy 分组
groupingBy 用于将数据分组,最终返回一个 Map 类型
Map> map = list.stream().collect(groupingBy(Person::getAge));
例子中我们按照年龄 age 分组,每一个 Person 对象中年龄相同的归为一组
另外可以看出,Person::getAge
决定 Map 的键(Integer 类型),list 类型决定 Map 的值(List 类型)
多级分组
groupingBy 可以接受一个第二参数实现多级分组:
Map>> map = list.stream().collect(groupingBy(Person::getAge, groupBy(...)));
其中返回的 Map 键为 Integer 类型,值为 Map
按组收集数据
Mapmap = list.stream().collect(groupingBy(Person::getAge, summingInt(Person::getAge)));
该例子中,我们通过年龄进行分组,然后 summingInt(Person::getAge))
分别计算每一组的年龄总和(Integer),最终返回一个 Map
根据这个方法,我们可以知道,前面我们写的:
groupingBy(Person::getAge)
其实等同于:
groupingBy(Person::getAge, toList())
6. partitioningBy 分区
分区与分组的区别在于,分区是按照 true 和 false 来分的,因此partitioningBy 接受的参数的 lambda 也是 T -> boolean
根据年龄是否小于等于20来分区 Map> map = list.stream() .collect(partitioningBy(p -> p.getAge() <= 20)); 打印输出 { false=[Person{name='mike', age=25}, Person{name='tom', age=30}], true=[Person{name='jack', age=20}] }
同样地 partitioningBy 也可以添加一个收集器作为第二参数,进行类似 groupBy 的多重分区等等操作。
参考链接:
1、编程范式:命令式编程(Imperative)、声明式编程(Declarative)和函数式编程(Functional):https://www.cnblogs.com/sirkevin/p/8283110.html
2、JDK 8 函数式编程入门:https://www.cnblogs.com/snowInPluto/p/5981400.html
3、java8匿名函数:https://blog.csdn.net/luzhensmart/article/details/85227586
4、你真的理解函数式编程么:https://gitbook.cn/books/5a111ccbfcbb793593d41e7f/index.html
5、借助java8实现柯里化:https://www.jianshu.com/p/c623b8b2aec8
6、简洁又快速地处理集合——Java8 Stream:https://www.jianshu.com/p/0bb4daf6c800?from=groupmessage