本文简单介绍了 Stream 这个自1.8开始引入的新特性,然后简单介绍了一些基本概念和流的创建方式,在接下来的文章中还会介绍流的一些实用操作,希望能和大家一起学会使用 Stream 这个实用的特性,当然本文也难免有错误之处,希望得到各位的指正。

前言

相信很多人(包括我自己),在很长一段时间内虽然使用了 JDK 1.8 ,却从来没有使用过自1.8开始增加的 Stream 这一强大使用的新特性,本文则将先从如何创建 Stream 开始,逐步去学会 Stream 的使用。本文不会涉及对流中数据的操作,而只讨论创建流的几种方法,以及一些基础概念,关于流的实用操作将会在后续文章中一一介绍。

Stream 与 Collection 的区别

  1. 用途与关注点不同
    Collection 主要关注于对象的存储方面,通过使用 ListMapSet等等数据结构,让数据被更好的组织起来,以便于使用。而 Stream 则关注于对象的操作方面,包含reducemapfilter等等实用的操作。

  2. 流是懒搜索(Laziness-seeking)的
    先看一个例子,考虑一下代码:

Random random = new Random(29); random.ints() .filter(v -> v > 5 && v < 31) .limit(3) .forEach(System.out::println); // output: // 21 // 22 // 28

代码首先创建了一个随机整数流,然后过滤得到其中在(5, 31)范围内的数,最终得到其中的3个数并输出,这里创建的流就是3中所说的无限流,而流在执行的过程中一旦得到一个满足条件的整数就会加到结果序列中,并且开始进行下一轮的搜索,直到找到3个满足的整数为止。流只会完成所给任务(找到3个满足指定范围的整数并输出),不会有额外的操作。

  1. 流的大小可以是无限的
    尽管 Collection 的数据量也可以动态扩展改变,但由于计算机内存是有限的,所以其数据量大小始终可以看成只能为有限的大小。但 Stream 则不同,由于流是懒加载的,所以当使用limit类似的短路操作时,就可以利用特性2的原因去接收一个无限流。

  2. 流操作不存在副作用
    Collection 中的某些操作,例如remove会删除集合中的元素不同,流不会修改生成流的原有集合中的数据,例如使用filter时,会产生一个经过元素过滤后的新流,而不会修改原集合中的数据。

  3. 流属于消耗品(Consumable)
    不同与 Collection 没有访问次数与使用的限制,一个流在其生命周期中只能被执行一次,当执行了终端操作(terminal operation,在之后的文章中会具体介绍)后,即使没有将流关闭,例如上述代码中的forEach,也无法再次访问了(类似迭代器),如下代码所示,想要再操作,必须重新创建一个流。

IntStream stream = new Random(29).ints(); stream.filter(v -> v > 5 && v < 31) .limit(3) .forEach(System.out::println); // 当执行了终端操作后再使用,就会出现一下异常提示信息 // java.lang.IllegalStateException: stream has already been operated upon or closed stream.forEach(System.out::println);

创建流

流可以通过很多种方式被创建,下面进行一一介绍:

  1. Collection 家族创建的方式
    对于实现了Collection 接口的类,都可以通过stream()parallelStream()创建对应流,如下代码所示:
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); // 创建一个普通的流 Stream<Integer> stream = list.stream(); // 创建一个并行流 Stream<Integer> parallelStream = list.parallelStream();
  1. 数组家族创建的方式
    对于数组类型的元素,都可以使用Arrays类的stream()创建对应的流,如果想获得并行流则需要使用parallel()方法,如下所示:
IntStream stream = Arrays.stream(new int[]{1, 2, 3}); // 生成流对应的并行流 IntStream parallelStream = stream.parallel();
  1. Stream家族的工厂方法
    通过工厂方法来创建流的方式比较多,可以通过emptyofconcatgenerateiteraterangerangeClosed以及builder等方法创建流,下面就通过代码样例来一一介绍:
// 产生一个不包含任何元素的流 Stream<Object> stream1 = Stream.empty(); // 由给定元素所生成的流 Stream<Integer> stream2 = Stream.of(1, 2, 3); // 合并两个流产生一个新的流 Stream<Object> stream3 = Stream.concat(stream1, stream2); // 创建一个<无限流>,流中的数据是通过调用所传函数产生的 Stream<Double> stream4 = Stream.generate(Math::random); // 创建一个<无限流>,流中的数据由第一个参数、将 // 第一个参数作为函数参数调用产生的值以及不断将 // 函数调用得到的值作为参数继续调用所组成, // 例如下面会生成1,2,3....的整数流 Stream<Integer> stream5 = Stream.iterate(1, v -> v + 1); // 创建范围为[1, 5)组成的整数流 IntStream stream6 = IntStream.range(1, 5); // 创建范围为[1, 5]组成的整数流 IntStream stream7 = IntStream.rangeClosed(1, 5); // 通过流的建造者模式创建流 Stream.Builder<Integer> builder = Stream.builder(); for (int i = 0; i < 10; i++) { // add 与 accept 方法均可将元素添加到流中 // 区别是 accept 无返回值, add 会返回当前 builder 的 this 对象 // 底层 add 方法也是调用了 accept 然后返回 this // 因此对于 add 方法可以进行链式调用 builder.add(i); builder.accept(i); } Stream<Integer> stream8 = builder.build();
  1. IO/NIO家族中的方法
    除了两种获取lines生成的流外,其它几种方式都很少使用,这一部分了解即可。
try { String dir = System.getProperty("user.dir"); // 以下两种方法均是获取文件中行数据组成的流 Stream<String> stream1 = new BufferedReader(new FileReader(dir + "\\demo.txt")).lines(); Stream<String> stream2 = Files.lines(Paths.get(dir + "\\demo.txt")); // 获取指定路径下所有文件/文件夹的路径组成的流 Stream<Path> stream3 = Files.list(Paths.get("d:\\temp")); // 获取指定路径下以及指定最深文件层级内(在这里为2)且满足函数条件的所有文件/文件夹的路径组成的流 Stream<Path> stream4 = Files.find( Paths.get("d:\\temp"), 1, (path, basicFileAttributes) -> path.isAbsolute()); // 获取指定路径下以及指定最深文件层级内(在这里为2)所有文件/文件夹的路径组成的流 Stream<Path> stream5 = Files.walk(Paths.get("d:\\temp"), 2); } catch (IOException e) { e.printStackTrace(); }
  1. Random 获取流的方式
    由于直接使用 Random 类生成随机数无限流,均为基本数据类型组成的流,因此通常还需要使用boxed方法进行装箱(以前凡是生成的为IntStreamDoubleStreamLongStream均同此),以便可以使用更加丰富的特性。
Random random = new Random(); // 以下三种方式得到的均是随机数组成的<无限流> IntStream stream1 = random.ints(); DoubleStream stream2 = random.doubles(); LongStream stream3 = random.longs(); Stream<Integer> boxedStream = stream1.boxed();

下面就先举一个具体的实用的例子,在之后的文章中会详细介绍一些实用操作,这里可以先做了解:

// 对数组元素进行倒序排序 // 如果不进行装箱(boxed)处理,则只能使用默认的升序排序方法 // 通过装箱,则可以通过自定义比较器,实现更加多样的排序 int[] arr = {1, 5, 4, 6, 3, 9, 4, 5, 6, 4}; int[] reverseArr = Arrays.stream(arr) .boxed() .sorted(Comparator.reverseOrder()) .mapToInt(Integer::valueOf) .toArray(); // output: [9, 6, 6, 5, 5, 4, 4, 4, 3, 1] System.out.println(Arrays.toString(reverseArr));
  1. 其它可以生成流的类
    除了以上介绍的几个主要可以生成流的类之外,还有一些其它不太常见的可以流的类,下面是部分代码展示:
String s = "1,2,3,4,5,6,7"; // 由分割后的字符串组成的流 // 在这里就是"1", "2", "3", "4", "5", "6", "7"组成的流 Stream<String> stream1 = Pattern.compile(",").splitAsStream(s); BitSet bitSet = new BitSet(); for (int i = 0; i < 10; i++) { if (i % 2 == 0) { bitSet.set(i); } } // 由 bitset 中被设置为 true 的位下标所组成的流 // 在这里就是0, 2, 4, 6, 8 IntStream stream2 = bitSet.stream(); try { String dir = System.getProperty("user.dir"); JarFile jarFile = new JarFile(dir + "\\demo.jar"); // 由指定 jar 包中所有文件及文件夹的 JarEntry 对象所组形成的流 Stream<JarEntry> stream3 = jarFile.stream(); } catch (IOException e) { e.printStackTrace(); }

此外还可以通过 StreamSupport工具类进行产生和操作流,由于本文包括之后的文章主要是为了入门和先简单上手,所以这里不做详细讨论,感兴趣的可以自己进行查阅资料。

总结

本文简单介绍了 Stream 这个自1.8开始引入的新特性,然后简单介绍了一些基本概念和流的创建方式,在接下来的文章中还会介绍流的一些实用操作,希望能和大家一起学会使用 Stream 这个实用的特性,当然本文也难免有错误之处,希望得到各位的指正。