目录
3.2.12 sorted /sorted ((t,t)->int)
3.2.18 Optional reduce(BinaryOperator accumulator)
1.什么是 Stream?
Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。
这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。
元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。
Stream(流)是一个来自数据源的元素队列并支持聚合操作
- 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
- 数据源 流的来源。 可以是集合,数组,I/O channel, 产生器generator 等。
- 聚合操作 类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。
和以前的Collection操作不同, Stream操作还有两个基础的特征:
- Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
- 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。
2.分类
- 无状态:指元素的处理不受之前元素的影响;
- 有状态:指该操作只有拿到所有元素之后才能继续下去。
- 非短路操作:指必须处理所有元素才能得到最终结果;
- 短路操作:指遇到某些符合条件的元素就可以得到最终结果,如 A || B,只要A为true,则无需判断B的结果。
3.实际操作
3.1 stream的生成方法
Stream可以通过集合数组创建。
1、通过 java.util.Collection.stream() 方法用集合创建流
2、使用java.util.Arrays.stream(T[] array)方法用数组创建流
3、使用Stream的静态方法:of()、iterate()、generate()
4. 文件生成流
Stream<String> stream = Files.lines(Paths.get("data.txt"));
每个元素是给定文件的其中一行
5. 函数生成流
两个方法:
iterate : 依次对每个新生成的值应用函数
generate :接受一个函数,生成一个新的值
Stream.iterate(0, n -> n + 2)
生成流,首元素为 0,之后依次加 2
Stream.generate(Math :: random)
生成流,为 0 到 1 的随机双精度数
3.2 stream的操作
3.2.1 anyMacth
检查集合中是否至少有一个元素满足条件 anyMacth 返回boolean类型 true/false
判断id是否有小于200的
list.stream().anyMatch(e -> 200L > e.getKsId());
3.2.2 allMatch
检查集合中的所有元素是否都满足条件allMatch返回boolean类型true/false
判断是否都大于200
list.stream().allMatch(e->200L<e.getKsId());
3.2.3 noneMatch(T -> boolean)
流中是否没有元素匹配给定的 T -> boolean 条件 判断是否没有空的
list.stream().noneMatch(e->null==e.getKsId());
3.2.4 filter 过滤
该操作会接受一个谓词(一个返回boolean的函数) 作为参数,并返回一个包含所有符合谓词的元素的流,当里面的表达式 = true的时候表明当前元素被过滤掉了,剩下的都是返回false的元素。
3.2.5 distinct 去重
distinct返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流
3.2.6 limit 直接获取前几条记录
3.2.7 skip跳过元素
返回一个去除前n个元的流,如果流中的元素不足n,返回一个空的流
skip和limit可以实现分页
deptDTOList.stream().skip(pageRequest.getPageNumber()*size).limit(size).collect(Collectors.toList());
3.2.8 map
将集合中的元素通过一个闭包转成其他类型 流支持map方法,他会接受一个函数作为参数,这个函数会被应用到每个元素上,并将其映射成一个新的元素
3.2.9 peek
如同于map,能得到流中的每一个元素。但map接收的是一个Function表达式,有返回值;而peek接收的是Consumer表达式,没有返回值。
3.2.10 findAny
返回任意一个元素
3.2.11 findFirst
返回第一个元素
3.2.12 sorted /sorted ((t,t)->int)
如果流中的元素的类实现了Comparable接口,即有自己的排序规则,那么可以直接调用sorted方法对元素进行排序。 如果没有,需要调用sorted((t,t))->int)
根据ksId去比较
list=list.stream().sorted(Comparator.comparingLong(WsDataReport::getKsId)).collect(Collectors.toList());
3.2.13 unordered()
返回一个等效的无序流,当然如果流本身就是无序的话,那可能就会直接返回其本身
3.2.14 maxBy,minBy
两个方法 比较做大或最小,需要一个 Comparator 接口作为参数
Optional<WsDataReport> max =list.stream().max(Comparator.comparingLong(WsDataReport::getKsId));
3.2.15 flatMap(T -> Stream)
将流中的每一个元素 T 映射为一个流,再把每一个流连接成为一个流
List<String> list = new ArrayList<>();
list.add("aaa bbb ccc");
list.add("ddd eee fff");
list = list.stream().map(s -> s.split(" ")).flatMap(Arrays::stream).collect(toList());
上面例子中,我们的目的是把 List 中每个字符串元素以" "分割开,变成一个新的 List
3.2.16 joining 连接字符串
也是一个比较常用的方法,对流里面的字符串元素进行连接,其底层实现用的是专门用于字符串连接的 StringBuilder
String s = list.stream().map(Person::getName).collect(joining());
结果:jackmiketom
String s = list.stream().map(Person::getName).collect(joining(","));
结果:jack,mike,tom
字符串分隔符连接
String joinName = list.stream().map(Student::getName).collect(Collectors.joining(",", "(", ")"));
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 在中间连接各个字符串
3.2.17 forEach
向数据库插入数据:
list.stream().forEach(mapper::insert);
3.2.18 Optional<T> reduce(BinaryOperator<T> accumulator)
第一次执行时,accumulator函数的第一个参数为流中的第一个元素,第二个参数为流中元素的第二个元素;第二次执行时,第一个参数为第一次函数执行的结果,第二个参数为流中的第三个元素;依次类推。
T reduce(T identity, BinaryOperator<T> accumulator):
流程跟上面一样,只是第一次执行时,accumulator函数的第一个参数为identity,而第二个参数为流中的第一个元素。
<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner):
在串行流(stream)中,该方法跟第二个方法一样,即第三个参数combiner不会起作用。在并行流(parallelStream)中,我们知道流被fork join出多个线程进行执行,此时每个线程的执行流程就跟第二个方法reduce(identity,accumulator)一样,而第三个参数combiner函数,则是将每个线程的执行结果当成一个新的流,然后使用第一个方法reduce(accumulator)流程进行规约。
用于组合流中的元素,如求和,求积,求最大值等
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);
当然也可以
Optional<Integer> sum = list.stream().map(Person::getAge).reduce(Integer::sum);
即不接受任何起始值,但因为没有初始值,需要考虑结果可能不存在的情况,因此返回的是 Optional 类型
3.2.19 groupingBy 分组
groupingBy 用于将数据分组,最终返回一个 Map 类型
Map<Integer, List<Person>> map = list.stream().collect(groupingBy(Person::getAge));
例子中我们按照年龄 age 分组,每一个 Person 对象中年龄相同的归为一组 另外可以看出,Person::getAge 决定 Map 的键(Integer 类型),list 类型决定 Map 的值(List 多级分组 groupingBy 可以接受一个第二参数实现多级分组:
Map<Integer, Map<T, List<Person>>> map = list.stream().collect(groupingBy(Person::getAge, groupBy(...)));
其中返回的 Map 键为 Integer 类型,值为 Map<T, List 按组收集数据
Map<Integer, Integer> map = list.stream().collect(groupingBy(Person::getAge, summingInt(Person::getAge)));
该例子中,我们通过年龄进行分组,然后 summingInt(Person::getAge)) 分别计算每一组的年龄总和(Integer),最终返回一个 Map<Integer, Integer>
3.2.20 partitioningBy
分区分区与分组的区别在于,分区是按照 true 和 false 来分的,因此partitioningBy 接受的参数的 lambda 也是 T -> boolean 根据年龄是否小于等于20来分区
Map<Boolean, List<Person>> 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 的多重分区等等操作。
3.2.21 数值流
int sum = list.stream().map(Person::getAge).reduce(0, Integer::sum); 计算元素总和的方法其中暗含了装箱成本,map(Person::getAge) 方法过后流变成了int Stream针对这个问题 Java 8 有良心地引入了数值流 IntStream, DoubleStream, LongStream,这种流中的元素都是原始数据类型,分别是 int,double,long 1. 流与数值流的转换 流转换为数值流 mapToInt(T -> int) : return IntStream mapToDouble(T -> double) : return DoubleStream mapToLong(T -> long) : return LongStream IntStream intStream = list.stream().mapToInt(Person::getAge); 数值流转换为流 很简单,就一个 boxed Stream<Integer> stream = intStream.boxed(); 2. 数值范围 (1)IntStream 与 LongStream 拥有 range 和 rangeClosed 方法用于数值范围处理 IntStream : rangeClosed(int, int) / range(int, int) LongStream : rangeClosed(long, long) / range(long, long) 这两个方法的区别在于一个是闭区间,一个是半开半闭区间: rangeClosed(1, 100) :[1, 100] range(1, 100) :[1, 100) 我们可以利用 IntStream.rangeClosed(1, 100) 生成 1 到 100 的数值流 求 1 到 10 的数值总和: IntStream intStream = IntStream.rangeClosed(1, 10); int sum = intStream.sum(); (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.2.22 收集操作
collect:接收一个Collector实例,将流中元素收集成另外一个数据结构。 Collector<T, A, R> 是一个接口,有以下5个抽象方法: Supplier<A> supplier():创建一个结果容器A BiConsumer<A, T> accumulator():消费型接口,第一个参数为容器A,第二个参数为流中元素T。 BinaryOperator<A> combiner():函数接口,该参数的作用跟上一个方法(reduce)中的combiner参数一样,将并行流中各个子进程的运行结果(accumulator函数操作后的容器A)进行合并。 Function<A, R> finisher():函数式接口,参数为:容器A,返回类型为:collect方法最终想要的结果R。 Set<Characteristics> characteristics():返回一个不可变的Set集合,用来表明该Collector的特征。有以下三个特征: CONCURRENT:表示此收集器支持并发。(官方文档还有其他描述,暂时没去探索,故不作过多翻译) UNORDERED:表示该收集操作不会保留流中元素原有的顺序。 IDENTITY_FINISH:表示finisher参数只是标识而已,可忽略。
将list中的对象的某个属性作为key,对象为value ,返回为 HashMap
list.stream().filter(e->null!=e.getId()). collect(Collectors.toMap(KwViolationOptionsJpa::getId, Function.identity(),(k1,k2)->k2,HashMap::new));
对list中的map求和
List<Map> list = new ArrayList(); Map<String,Integer> test = new HashMap(){{ put("id",1); put("qty",1); }}; list.add(test); 对list中的map求和 Map result = list.stream() .collect(Collectors.groupingBy(e -> e.get("id"))) .entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().stream().mapToInt(a -> (int) a.get("qty")) .sum(), (k1, k2) -> k1));
3.3 并行
parallelStream 方法能生成并行流,因此你通常可以使用 parallelStream 来代替 stream 方法,但是并行的性能问题非常值得我们思考 比方说下面这个例子 int i = Stream.iterate(1, a -> a + 1).limit(100).parallel().reduce(0, Integer::sum); 我们通过这样一行代码来计算 1 到 100 的所有数的和,我们使用了 parallel 来实现并行。 但实际上是,这样的计算,效率是非常低的,比不使用并行还低!一方面是因为装箱问题,还有一方面就是 iterate 方法很难把这些数分成多个独立块来并行执行,因此无形之中降低了效率。 流的可分解性 这就说到流的可分解性问题了,使用并行的时候,我们要注意流背后的数据结构是否易于分解。比如众所周知的 ArrayList 和 LinkedList,明显前者在分解方面占优。 顺序性 除了可分解性,和刚刚提到的装箱问题,还有一点值得注意的是一些操作本身在并行流上的性能就比顺序流要差,比如:limit,findFirst,因为这两个方法会考虑元素的顺序性,而并行本身就是违背顺序性的,也是因为如此 findAny 一般比 findFirst 的效率要高。
数据源 | 可分解性 |
ArrayList | 极佳 |
LinkedList | 差 |
IntStream.range | 极佳 |
Stream.iterate | 差 |
HashSet | 好 |
TreeSet | 好 |