我们为什么需要 Stream API
Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。
集合讲的是数据,流讲的是计算
Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。
同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。
浅谈聚合操作(Stream API能协助解决)
在传统的 J2EE 应用中,Java 代码经常不得不依赖于关系型数据库的聚合操作来完成诸如:
客户每月平均消费金额
最昂贵的在售商品
取十个数据样本作为首页推荐
但在当今这个数据大爆炸的时代,在数据来源多样化、数据海量化的今天,很多时候不得不脱离 RDBMS,或者以底层返回的数据为基础进行更上层的数据统计。
这个时候,如果没有Java8提供的Stream API,那简直就是噩梦。在 Java 8 使用 Stream,代码更加简洁易读;而且使用并发模式,程序执行速度更快。
对Stream进一步理解
简单说,对 Stream 的使用就是实现一个 filter-map-reduce 过程,产生一个最终结果,或者导致一个副作用(side effect)。
Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。
对于 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。
Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。
Java 的并行 API 演变历程基本如下:
1.1.0-1.4 中的 java.lang.Thread
2.5.0 中的 java.util.concurrent
3.6.0 中的 Phasers 等
4.7.0 中的 Fork/Join 框架
5.8.0 中的 Stream
Stream 的另外一大特点是,数据源本身可以是无限的(即无限流)。
对流的操作概述
流的操作类型分为两种:
Intermediate(中间操作):一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
Terminal(终止操作):一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。
forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
还有一种操作被称为 short-circuiting(短路操作)。用以指:
对于一个 intermediate 操作,如果它接受的是一个无限流,它可以返回一个有限的新 Stream。
对于一个 terminal 操作,如果它接受的是一个无限流,但能在有限的时间计算出结果。
anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
下面汇总了Stream所有的操作:
IntStream、LongStream、DoubleStream
IntStream、LongStream、DoubleStream。当然我们也可以用 Stream、Stream >、Stream,但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。
Java 8 中还没有提供其它数值型 Stream,因为这将导致扩增的内容较多。而常规的数值型聚合运算可以通过上面三种 Stream 进行。
数值流的构造:
IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println); IntStream.range(1, 3).forEach(System.out::println); IntStream.rangeClosed(1, 3).forEach(System.out::println);
range,需要传入开始节点和结束节点两个参数,返回的是一个有序的LongStream。包含开始节点和结束节点两个参数之间所有的参数,间隔为1.
rangeClosed的功能和range类似。差别就是rangeClosed包含最后的结束节点,range不包含。
进阶:自己生成流(无限流)
public static<T> Stream<T> generate(Supplier<T> s) { Objects.requireNonNull(s); return StreamSupport.stream( new StreamSpliterators.InfiniteSupplyingSpliterator.OfRef<>(Long.MAX_VALUE, s), false); }
可以自己来控制流的生成。这种情形通常用于随机数、常量的 Stream,或者需要前后元素间维持着某种状态信息的 Stream。把 Supplier 实例传递给 Stream.generate() 生成的 Stream,默认是串行(相对 parallel 而言)但无序的(相对 ordered 而言)。由于它是无限的,在管道中,必须利用 limit 之类的操作限制 Stream 大小。
生成 10 个随机整数:
/////
public static void main(String[] args) { Stream.generate(new Random()::nextInt).limit(10).forEach(System.out::println); //采用IntStream流的方式(推荐使用 逼格很高) IntStream.generate(() -> (int) (System.nanoTime() % 100)). limit(10).forEach(System.out::println); }
另外一种方式自己生成流(这个非常好用):
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) {}
iterate 跟 reduce 操作很像,接受一个种子值,和一个 UnaryOperator(例如 f)。然后种子值成为 Stream 的第一个元素,f(seed) 为第二个,f(f(seed)) 第三个,以此类推。
如生成一个等差数列:
Stream.iterate(0, n -> n + 3).limit(10).forEach(x -> System.out.print(x + " ")); //0 3 6 9 12 15 18 21 24 27
与 Stream.generate 相仿,在 iterate 时候管道必须有 limit 这样的操作来限制 Stream 大小。
Stream实操案例
创建流Stream
Java8 中的 Collection 接口被扩展,提供两个获取流的方法 :
default Stream stream() : 返回一个顺序流
default Stream parallelStream() : 返回一个并行流
由数组创建流
Java8 中的 Arrays 的静态方法 stream() 可以获取数组流 :static Stream stream(T[] array) : 返回一个流
重载形式,能够处理对应基本类型的数组IntStream/LongStream/DoubleStream :
由值创建流
可以使用静态方法 Stream.of(), 通过显示值创建一个流,它可以接收任意数量的参数:public static Stream of(T… values) : 返回一个流
由方法创建流 : 创建无限流
可以使用静态方法 Stream.iterate() 和 Stream.generate(), 创建无限流
迭代
public static Stream iterate(final T seed, final UnaryOperator f)
生成
public static Stream generate(Supplier s)
Stream的中间操作实操
多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理。而在终止操作时一次性全部处理,称为**“惰性求值”**
筛选与切片系列
emps.parallelStream().filter((e) -> e.getSalary() >= 5000).skip(2).forEach(System.out::println);
这块相对比较简单,就一笔带过了。
peek方法用得比较少,这里特殊介绍一下:
Stream.of("one", "two", "three", "four").peek(e -> System.out.println(e)); 输出:这样不会有任何的输出; Stream.of("one", "two", "three", "four").peek(e -> System.out.println(e)).collect(Collectors.toList()); 输出: one two three four Stream.of("one", "two", "three", "four") .peek(e -> System.out.println("Peeked value: " + e)) .map(String::toUpperCase) .peek(e -> System.out.println("Mapped value: " + e)) .collect(Collectors.toList()); 输出: Peeked value: one Mapped value: ONE Peeked value: two Mapped value: TWO Peeked value: three Mapped value: THREE Peeked value: four Mapped value: FOUR
这个说白了。当元素被消费的时候,就会触发peek。有多少个就触发多少次。
unordered的使用案例:
public static void main(String[] args) { List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); list.stream().forEach(System.out::print); //123456789 System.out.println(); //使用unordered之后输出 list.stream().unordered().forEach(System.out::print); //123456789 }
我们会发现,输出的顺序没有改变。所以它并不是来打乱这个顺序的。所以大家使用的时候不要误解了。正确的使用 姿势:
//使stream无序:对于 distinct() 和 limit() 等方法,如果不关心顺序,则可以使用并行: LongStream.rangeClosed(5, 10).unordered().parallel().limit(3); IntStream.of(14, 15, 15, 14, 12, 81).unordered().parallel().distinct(); 这样使用,能提高CPU的利用率,进而提高处理的效率