Java 8 新特性系列文章索引。java
咱们都知道 Lambda 和 Stream 是 Java 8 的两大亮点功能,在前面的文章里已经介绍过 Lambda 相关知识,此次介绍下 Java 8 的 Stream 流操做。它彻底不一样于 java.io 包的 Input/Output Stream ,也不是大数据实时处理的 Stream 流。这个 Stream 流操做是 Java 8 对集合操做功能的加强,专一于对集合的各类高效、便利、优雅的聚合操做。借助于 Lambda 表达式,显著的提升编程效率和可读性。且 Stream 提供了并行计算模式,能够简洁的编写出并行代码,能充分发挥现在计算机的多核处理优点。git
在使用 Stream 流操做以前你应该先了解 Lambda 相关知识,若是还不了解,能够参考以前文章:还看不懂同事的代码?Lambda 表达式、函数接口了解一下 。github
Stream 不一样于其余集合框架,它也不是某种数据结构,也不会保存数据,可是它负责相关计算,使用起来更像一个高级的迭代器。在以前的迭代器中,咱们只能先遍历而后在执行业务操做,而如今只须要指定执行什么操做, Stream 就会隐式的遍历而后作出想要的操做。另外 Stream 和迭代器同样的只能单向处理,如同奔腾长江之水一去而不复返。面试
因为 Stream 流提供了惰性计算和并行处理的能力,在使用并行计算方式时数据会被自动分解成多段而后并行处理,最后将结果汇总。因此 Stream 操做可让程序运行变得更加高效。shell
Stream 流的使用老是按照必定的步骤进行,能够抽象出下面的使用流程。数据库
数据源(source) -> 数据处理/转换(intermedia) -> 结果处理(terminal )编程
数据源(source)
也就是数据的来源,能够经过多种方式得到 Stream 数据源,下面列举几种常见的获取方式。数组
数据处理/转换(intermedia)
步骤能够有多个操做,这步也被称为intermedia
(中间操做)。在这个步骤中无论怎样操做,它返回的都是一个新的流对象,原始数据不会发生任何改变,并且这个步骤是惰性计算
处理的,也就是说只调用方法并不会开始处理,只有在真正的开始收集结果时,中间操做才会生效,并且若是遍历没有完成,想要的结果已经获取到了(好比获取第一个值),会中止遍历,而后返回结果。惰性计算
能够显著提升运行效率。数据结构
数据处理演示。app
@Test public void streamDemo(){ List<String> nameList = Arrays.asList("Darcy", "Chris", "Linda", "Sid", "Kim", "Jack", "Poul", "Peter"); // 1. 筛选出名字长度为4的 // 2. 名字前面拼接 This is // 3. 遍历输出 nameList.stream() .filter(name -> name.length() == 4) .map(name -> "This is "+name) .forEach(name -> System.out.println(name)); } // 输出结果 // This is Jack // This is Poul
数据处理/转换
操做天然不止是上面演示的过滤 filter
和 map
映射两种,另外还有 map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered 等。
结果处理(terminal )
是流处理的最后一步,执行完这一步以后流会被完全用尽,流也不能继续操做了。也只有到了这个操做的时候,流的数据处理/转换
等中间过程才会开始计算,也就是上面所说的惰性计算
。结果处理
也一定是流操做的最后一步。
常见的结果处理
操做有 forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator 等。
下面演示了简单的结果处理
的例子。
/** * 转换成为大写而后收集结果,遍历输出 */ @Test public void toUpperCaseDemo() { List<String> nameList = Arrays.asList("Darcy", "Chris", "Linda", "Sid", "Kim", "Jack", "Poul", "Peter"); List<String> upperCaseNameList = nameList.stream() .map(String::toUpperCase) .collect(Collectors.toList()); upperCaseNameList.forEach(name -> System.out.println(name + ",")); } // 输出结果 // DARCY,CHRIS,LINDA,SID,KIM,JACK,POUL,PETER,
有一种 Stream 操做被称做 short-circuiting
,它是指当 Stream 流无限大可是须要返回的 Stream 流是有限的时候,而又但愿它能在有限的时间内计算出结果,那么这个操做就被称为short-circuiting
。例如 findFirst
操做。
Stream 流在使用时候老是借助于 Lambda 表达式进行操做,Stream 流的操做也有不少种方式,下面列举的是经常使用的 11 种操做。
获取 Stream 的几种方式在上面的 Stream 数据源里已经介绍过了,下面是针对上面介绍的几种获取 Stream 流的使用示例。
@Test public void createStream() throws FileNotFoundException { List<String> nameList = Arrays.asList("Darcy", "Chris", "Linda", "Sid", "Kim", "Jack", "Poul", "Peter"); String[] nameArr = {"Darcy", "Chris", "Linda", "Sid", "Kim", "Jack", "Poul", "Peter"}; // 集合获取 Stream 流 Stream<String> nameListStream = nameList.stream(); // 集合获取并行 Stream 流 Stream<String> nameListStream2 = nameList.parallelStream(); // 数组获取 Stream 流 Stream<String> nameArrStream = Stream.of(nameArr); // 数组获取 Stream 流 Stream<String> nameArrStream1 = Arrays.stream(nameArr); // 文件流获取 Stream 流 BufferedReader bufferedReader = new BufferedReader(new FileReader("README.md")); Stream<String> linesStream = bufferedReader.lines(); // 从静态方法获取流操做 IntStream rangeStream = IntStream.range(1, 10); rangeStream.limit(10).forEach(num -> System.out.print(num+",")); System.out.println(); IntStream intStream = IntStream.of(1, 2, 3, 3, 4); intStream.forEach(num -> System.out.print(num+",")); }
forEach
是 Strean 流中的一个重要方法,用于遍历 Stream 流,它支持传入一个标准的 Lambda 表达式。可是它的遍历不能经过 return/break 进行终止。同时它也是一个 terminal
操做,执行以后 Stream 流中的数据会被消费掉。
如输出对象。
List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); numberList.stream().forEach(number -> System.out.println(number+",")); // 输出结果 // 1,2,3,4,5,6,7,8,9,
使用 map
把对象一对一映射成另外一种对象或者形式。
/** * 把数字值乘以2 */ @Test public void mapTest() { List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); // 映射成 2倍数字 List<Integer> collect = numberList.stream() .map(number -> number * 2) .collect(Collectors.toList()); collect.forEach(number -> System.out.print(number + ",")); System.out.println(); numberList.stream() .map(number -> "数字 " + number + ",") .forEach(number -> System.out.println(number)); } // 输出结果 // 2,4,6,8,10,12,14,16,18, // 数字 1,数字 2,数字 3,数字 4,数字 5,数字 6,数字 7,数字 8,数字 9,
上面的 map
能够把数据进行一对一的映射,而有些时候关系可能不止 1对 1那么简单,可能会有1对多。这时可使用 flatMap。下面演示
使用 flatMap
把对象扁平化展开。
/** * flatmap把对象扁平化 */ @Test public void flatMapTest() { Stream<List<Integer>> inputStream = Stream.of( Arrays.asList(1), Arrays.asList(2, 3), Arrays.asList(4, 5, 6) ); List<Integer> collect = inputStream .flatMap((childList) -> childList.stream()) .collect(Collectors.toList()); collect.forEach(number -> System.out.print(number + ",")); } // 输出结果 // 1,2,3,4,5,6,
使用 filter
进行数据筛选,挑选出想要的元素,下面的例子演示怎么挑选出偶数数字。
/** * filter 数据筛选 * 筛选出偶数数字 */ @Test public void filterTest() { List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); List<Integer> collect = numberList.stream() .filter(number -> number % 2 == 0) .collect(Collectors.toList()); collect.forEach(number -> System.out.print(number + ",")); }
获得以下结果。
2,4,6,8,
findFirst
能够查找出 Stream 流中的第一个元素,它返回的是一个 Optional 类型,若是还不知道 Optional 类的用处,能够参考以前文章 Jdk14都要出了,还不能使用 Optional优雅的处理空指针? 。
/** * 查找第一个数据 * 返回的是一个 Optional 对象 */ @Test public void findFirstTest(){ List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); Optional<Integer> firstNumber = numberList.stream() .findFirst(); System.out.println(firstNumber.orElse(-1)); } // 输出结果 // 1
findFirst
方法在查找到须要的数据以后就会返回再也不遍历数据了,也所以 findFirst
方法能够对有无限数据的 Stream 流进行操做,也能够说 findFirst
是一个 short-circuiting
操做。
Stream 流能够轻松的转换为其余结构,下面是几种常见的示例。
/** * Stream 转换为其余数据结构 */ @Test public void collectTest() { List<Integer> numberList = Arrays.asList(1, 1, 2, 2, 3, 3, 4, 4, 5); // to array Integer[] toArray = numberList.stream() .toArray(Integer[]::new); // to List List<Integer> integerList = numberList.stream() .collect(Collectors.toList()); // to set Set<Integer> integerSet = numberList.stream() .collect(Collectors.toSet()); System.out.println(integerSet); // to string String toString = numberList.stream() .map(number -> String.valueOf(number)) .collect(Collectors.joining()).toString(); System.out.println(toString); // to string split by , String toStringbJoin = numberList.stream() .map(number -> String.valueOf(number)) .collect(Collectors.joining(",")).toString(); System.out.println(toStringbJoin); } // 输出结果 // [1, 2, 3, 4, 5] // 112233445 // 1,1,2,2,3,3,4,4,5
获取或者扔掉前 n 个元素
/** * 获取 / 扔掉前 n 个元素 */ @Test public void limitOrSkipTest() { // 生成本身的随机数流 List<Integer> ageList = Arrays.asList(11, 22, 13, 14, 25, 26); ageList.stream() .limit(3) .forEach(age -> System.out.print(age+",")); System.out.println(); ageList.stream() .skip(3) .forEach(age -> System.out.print(age+",")); } // 输出结果 // 11,22,13, // 14,25,26,
数学统计功能,求一组数组的最大值、最小值、个数、数据和、平均数等。
/** * 数学计算测试 */ @Test public void mathTest() { List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6); IntSummaryStatistics stats = list.stream().mapToInt(x -> x).summaryStatistics(); System.out.println("最小值:" + stats.getMin()); System.out.println("最大值:" + stats.getMax()); System.out.println("个数:" + stats.getCount()); System.out.println("和:" + stats.getSum()); System.out.println("平均数:" + stats.getAverage()); } // 输出结果 // 最小值:1 // 最大值:6 // 个数:6 // 和:21 // 平均数:3.5
分组聚合功能,和数据库的 Group by 的功能一致。
/** * groupingBy * 按年龄分组 */ @Test public void groupByTest() { List<Integer> ageList = Arrays.asList(11, 22, 13, 14, 25, 26); Map<String, List<Integer>> ageGrouyByMap = ageList.stream() .collect(Collectors.groupingBy(age -> String.valueOf(age / 10))); ageGrouyByMap.forEach((k, v) -> { System.out.println("年龄" + k + "0多岁的有:" + v); }); } // 输出结果 // 年龄10多岁的有:[11, 13, 14] // 年龄20多岁的有:[22, 25, 26]
/** * partitioningBy * 按某个条件分组 * 给一组年龄,分出成年人和未成年人 */ public void partitioningByTest() { List<Integer> ageList = Arrays.asList(11, 22, 13, 14, 25, 26); Map<Boolean, List<Integer>> ageMap = ageList.stream() .collect(Collectors.partitioningBy(age -> age > 18)); System.out.println("未成年人:" + ageMap.get(false)); System.out.println("成年人:" + ageMap.get(true)); } // 输出结果 // 未成年人:[11, 13, 14] // 成年人:[22, 25, 26]
/** * 生成本身的 Stream 流 */ @Test public void generateTest(){ // 生成本身的随机数流 Random random = new Random(); Stream<Integer> generateRandom = Stream.generate(random::nextInt); generateRandom.limit(5).forEach(System.out::println); // 生成本身的 UUID 流 Stream<UUID> generate = Stream.generate(UUID::randomUUID); generate.limit(5).forEach(System.out::println); } // 输出结果 // 793776932 // -2051545609 // -917435897 // 298077102 // -1626306315 // 31277974-841a-4ad0-a809-80ae105228bd // f14918aa-2f94-4774-afcf-fba08250674c // d86ccefe-1cd2-4eb4-bb0c-74858f2a7864 // 4905724b-1df5-48f4-9948-fa9c64c7e1c9 // 3af2a07f-0855-455f-a339-6e890e533ab3
上面的例子中 Stream 流是无限的,可是获取到的结果是有限的,使用了 Limit
限制获取的数量,因此这个操做也是 short-circuiting
操做。
正确使用而且正确格式化的 Stream 流操做代码不只简洁优雅,更让人赏心悦目。下面对比下在使用 Stream 流和不使用 Stream 流时相同操做的编码风格。
/** * 使用流操做和不使用流操做的编码风格对比 */ @Test public void diffTest() { // 不使用流操做 List<String> names = Arrays.asList("Jack", "Jill", "Nate", "Kara", "Kim", "Jullie", "Paul", "Peter"); // 筛选出长度为4的名字 List<String> subList = new ArrayList<>(); for (String name : names) { if (name.length() == 4) { subList.add(name); } } // 把值用逗号分隔 StringBuilder sbNames = new StringBuilder(); for (int i = 0; i < subList.size() - 1; i++) { sbNames.append(subList.get(i)); sbNames.append(", "); } // 去掉最后一个逗号 if (subList.size() > 1) { sbNames.append(subList.get(subList.size() - 1)); } System.out.println(sbNames); } // 输出结果 // Jack, Jill, Nate, Kara, Paul
若是是使用 Stream 流操做。
// 使用 Stream 流操做 String nameString = names.stream() .filter(num -> num.length() == 4) .collect(Collectors.joining(", ")); System.out.println(nameString);
上面有提到,数据处理/转换(intermedia)
操做 map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered 等这些操做,在调用方法时并不会当即调用,而是在真正使用的时候才会生效,这样可让操做延迟到真正须要使用的时刻。
下面会举个例子演示这一点。
/** * 找出偶数 */ @Test public void lazyTest() { // 生成本身的随机数流 List<Integer> numberLIst = Arrays.asList(1, 2, 3, 4, 5, 6); // 找出偶数 Stream<Integer> integerStream = numberLIst.stream() .filter(number -> { int temp = number % 2; if (temp == 0 ){ System.out.println(number); } return temp == 0; }); System.out.println("分割线"); List<Integer> collect = integerStream.collect(Collectors.toList()); }
若是没有 惰性计算
,那么很明显会先输出偶数,而后输出 分割线
。而实际的效果是。
分割线 2 4 6
可见 惰性计算
把计算延迟到了真正须要的时候。
获取 Stream 流时可使用 parallelStream
方法代替 stream
方法以获取并行处理流,并行处理能够充分的发挥多核优点,并且不增长编码的复杂性。
下面的代码演示了生成一千万个随机数后,把每一个随机数乘以2而后求和时,串行计算和并行计算的耗时差别。
/** * 并行计算 */ @Test public void main() { // 生成本身的随机数流,取一千万个随机数 Random random = new Random(); Stream<Integer> generateRandom = Stream.generate(random::nextInt); List<Integer> numberList = generateRandom.limit(10000000).collect(Collectors.toList()); // 串行 - 把一千万个随机数,每一个随机数 * 2 ,而后求和 long start = System.currentTimeMillis(); int sum = numberList.stream() .map(number -> number * 2) .mapToInt(x -> x) .sum(); long end = System.currentTimeMillis(); System.out.println("串行耗时:"+(end - start)+"ms,和是:"+sum); // 并行 - 把一千万个随机数,每一个随机数 * 2 ,而后求和 start = System.currentTimeMillis(); sum = numberList.parallelStream() .map(number -> number * 2) .mapToInt(x -> x) .sum(); end = System.currentTimeMillis(); System.out.println("并行耗时:"+(end - start)+"ms,和是:"+sum); }
获得以下输出。
串行耗时:1005ms,和是:481385106 并行耗时:47ms,和是:481385106
效果显而易见,代码简洁优雅。
从上面的使用案例中,能够发现使用 Stream 流操做的代码很是简洁,并且可读性更高。可是若是不正确的排版,那么看起来将会很糟糕,好比下面的一样功能的代码例子,多几层操做呢,是否是有些让人头大?
// 不排版 String string = names.stream().filter(num -> num.length() == 4).map(name -> name.toUpperCase()).collect(Collectors.joining(",")); // 排版 String string = names.stream() .filter(num -> num.length() == 4) .map(name -> name.toUpperCase()) .collect(Collectors.joining(","));
若是想要你的 Stream 流对于每次的相同操做的结果都是相同的话,那么你必须保证 Lambda 表达式的纯度,也就是下面亮点。
这两点对于保证函数的幂等很是重要,否则你程序执行结果可能会变得难以预测,就像下面的例子。
@Test public void simpleTest(){ List<Integer> numbers = Arrays.asList(1, 2, 3); int[] factor = new int[] { 2 }; Stream<Integer> stream = numbers.stream() .map(e -> e * factor[0]); factor[0] = 0; stream.forEach(System.out::println); } // 输出结果 // 0 // 0 // 0
文中代码都已经上传到
<完>
我的网站:https://www.codingme.net
若是你喜欢这篇文章,能够关注公众号,一块儿成长。
关注公众号回复资源能够没有套路的获取全网最火的的 Java 核心知识整理&面试资料。