一 、为什么引入Stream

首先了解一项技术前,先需要了解为什么需要这项技术以及这项技术主要用来做什么的。在工作中集合对于很多编程任务来说都是非常基本的,但是关于集合上的很多处理方式都是类似于数据库一样的操作,比如匹配某个值,筛选部分元素,对数据进行分组等等,java8之前使用集合处理这些问题就比较麻烦,而且如果集合数据量比较大,为了提高性能,你还需要额外写代码进行并发处理,那就变的更加复杂了。

而引入Stream就是为了让关于集合的操作更加简单,Java 8 中的 Stream 是对集合功能的增强,它允许你以声明性方式处理数据集合。就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外, Stream还可以透明地并行处理,你无需写任何多线程代码,极大的提高编程效率和程序可读性。

比如以下是一个比较经典的对比代码。对集合简单排序的一个demo,以下是java8之前代码:

List<Transaction> groceryTransactions = new Arraylist<>();
for(Transaction t: transactions){
  if(t.getType() == Transaction.GROCERY){
    groceryTransactions.add(t);
  }
}
Collections.sort(groceryTransactions, new Comparator(){
  public int compare(Transaction t1, Transaction t2){
    return t2.getValue().compareTo(t1.getValue());
  }
});
List<Integer> transactionIds = new ArrayList<>();
for(Transaction t: groceryTransactions){
  transactionsIds.add(t.getId());
}

而使用Stream之后,上述代码可以直接简化成如下所示的代码:

List<Integer> transactionsIds = 
    transactions.stream()
                .filter(t -> t.getType() == Transaction.GROCERY)
                .sorted(comparing(Transaction::getValue).reversed())
                .map(Transaction::getId)
                .collect(toList());

可以很明显的看出,使用Stream之后的确是极大的提高了编程效率和程序可读性。

二、 简介

2.1 Stream定义及基本特征

那么Stream是什么?简短的定义就是“源中支持聚合操作的一系列元素”。让我们分解一下:

元素序列:流为特定元素类型的序列值集提供接口。但是,流实际上并不存储元素。它们是按需计算的。
源:流从提供数据的源(例如集合,数组或I / O资源)进行消耗。
聚合操作:流支持像SQL一样功能的编程语言操作和常用的操作,如filter,map,reduce,find,match,sorted等。
此外,流操作具有两个基本特征:

流水线:许多流操作本身都会返回一个流。这允许将操作链接在一起以形成更大的管道。这使某些优化,如懒惰和短路,这是我们后来探索。

内部迭代:与显式迭代的集合(外部迭代)相反,流操作为您在后台进行迭代。

理解上面概念后,我们再回顾下刚才展示的使用Stream代码编写的排序操作,其内部的实现流程大致如下:

java stream 从类中取字段_List

2.2 Stream与集合差异

有了基本概念后,我们再来了解下Stream集合的差异,既然Stream是对集合的增强,那两者之间具体有什么区别?可以参考以下官方文档中给出的区别:

1 Sream没有存储空间。流不是存储元素的数据结构。相反,它通过一系列计算操作从数据结构,数组,生成器功能或I / O通道等源中传递元素。

2 Sream本质上是功能性的。对流的操作会产生结果,但不会修改其源。例如,对Stream从集合中获取的Stream进行过滤会产生一个不包含过滤后元素的新元素,而不是从源集合中删除元素。

3 懒惰查找。许多流操作(例如过滤,映射或重复删除)可以延迟实施,从而暴露出优化的机会。例如,“String使用三个连续的元音查找第一个”不需要检查所有输入字符串。流操作分为中间(产生Stream)操作和最终(产生值或副作用)操作。中间操作总是很懒。

4 可以无界。尽管集合的大小是有限的,但流不是必需的。诸如limit(n)或findFirst() 的短路操作可以允许对无限流的计算在有限时间内完成。

5 消耗品。在流的生存期内,流的元素只能访问一次。与 Iterator一样,必须生成新的流以重新访问源中的相同元素。

三 、Stream 构成

3.1 Stream 分类

对Stream有了一定了解后,我们再来看看Sream的构成,Stream主要分为串行流和并行流,可分别通过stream()和 parallelStream()生成,其中parallelStream()其底层使用Fork/Join框架实现,不需要显示的写并行代码,就可直接对流进行并行操作,非常方便,但也需要注意线程安全问题。详细的可以自己去了解下,这里就不再展开。下面再来了解下Stream的操作类型。

3.2 操作类型

流操作分为中间操作和 终端操作,并合并以形成流管道。流管道由源(例如Collection,数组,生成器函数或I / O通道)组成;随后是零个或多个中间操作,例如Stream.filter或Stream.map;以及诸如Stream.forEach或Stream.reduce的终端操作。

3.2.1 中间操作
中间操作返回一个新的流。他们总是 懒惰; 执行诸如这样的中间操作filter(),实际上并不执行任何过滤,而是创建一个新的流,该新流在遍历时将包含与给定谓词匹配的初始流的元素。在执行管道的终端操作之前,不会开始遍历管道源。中间操作又分为无状态操作 和有状态操作。

无状态操作:在处理新元素时,诸如filter 和等无状态操作map不会保留先前看到的元素的状态-每个元素都可以独立于其他元素上的操作进行处理。

有状态操作:处理元素时会记录状态,例如distinct和sorted在处理新元素时可以合并先前看到的元素的状态。

3.2.2 终端操作
终端操作(例如Stream.forEach或IntStream.sum)可能会遍历流以产生结果或作用。执行终端操作后,流管道被视为已消耗,无法再使用;如果需要再次遍历同一数据源,则必须返回到数据源以获取新的流。终端操作可分为短路操作(如findFirst,allMatch)和非短路操作(如forEach,reduce)。

短路操作:获取到预期结果就会终止。比如anyMatch,findFirst等。

非短路操作:处理完所有的数据才会中止,比如collect,count等。

四 、常用API 使用

基本概念介绍完之后,下面开始介绍下怎么使用Stream,以下会从生成方式,常用中间操作以及常用终端操作三部分,介绍下Stream的常用API接口以及使用方式。

4.1 生成方式

1 、通过集合Collection的stream()与 parallelStream()方法获取。

List<String> list = new ArrayList<>();
//串行流
Stream<String> listStream = list.stream();
//并行流
Stream<String> listParallelStream = list.parallelStream();

2、 数组通过Arrays.stream(Object[]) 或者 Stream.of()生成。

String[] attr = {"1","2","3"};
Stream<String> arrayStream = Arrays.stream(attr);

3 、通过类静态工厂方法,如 Stream.of(Object[])]),Stream.generate(Supplier s),IntStream.range(int, int) 或Stream.iterate(Object, naryOperator)。

Stream<String> ofStream = Stream.of(attr);
Stream<Double> generateStream = Stream.generate(Math::random);
IntStream rangeStream = IntStream.range(1, 2);
Stream<String> iterateStream = Stream.iterate("1", n -> n + 1);

4、 BufferedReader.lines()读取文件的行。

BufferedReader reader = new BufferedReader(new FileReader("D:\\test_stream.txt"));
Stream<String> bufferedStream = reader.lines();

5 、Files.lines() 通过路径读取读取文件的行。

Stream<String> fileStream = Files.lines(Paths.get("D:\\test_stream.txt"), Charset.defaultCharset());

6 、随机数流Random.ints()。

Random random = new Random();
IntStream randomStream = random.ints();

7、 其他流的生成方法,包括

BitSet.stream(),Pattern.splitAsStream(java.lang.CharSequence)和JarFile.stream()。

BitSet bitSet = new BitSet();
IntStream bitSetStream = bitSet.stream();
Pattern pattern = Pattern.compile(",");
Stream<String> patternStream = pattern.splitAsStream("1,2,3,4");
JarFile jarFile = new JarFile("D:\\test_stream.txt");
Stream<JarEntry> jarStream = jarFile.stream();

8 、其他第三方架包实现。

此外,对于基本数值型,目前可以直接使用三种对应的包装类型 Stream: IntStream、LongStream、DoubleStream

4.2 常用中间操作

4.2.1 常用无状态操作
1 、unordered 返回无序的等效流 。可能会返回自身,这是因为流已经无序,或者是因为基础流状态已被修改为无序,主要用来提高并发性能。注意此接口不会每次都返回一个随机排列的无序流。

List<String> numbers = Arrays.asList("1", "2", "1", "3", "3", "2", "4");
numbers.stream().unordered().forEach(System.out::println);

2 、filter 过滤流中的某些元素。

//过滤集合中等于2的元素
 List<String> numbers = Arrays.asList("1", "2", "1", "3", "3", "2", "4");
 numbers.stream().filter("2"::equals).forEach(System.out::println);

3 、map,mapToInt,mapToLong,mapToDuble

map: 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。

mapToInt,mapToLong,mapToDuble: 分别针对不同数据类型的专门map方法,可直接调用特有方法比如sum进行求和等。

//map 
numbers.stream().map(number -> number.concat("2")).forEach(System.out::println);
//mapToInt
int[] intArray = numbers.stream().mapToInt(NumberUtils::toInt).toArray();
//mapToLong
long[] longArray = numbers.stream().mapToLong(NumberUtils::toLong).toArray();
//mapToDuble
double[] doubleArray = numbers.stream().mapToDouble(NumberUtils::toDouble).toArray();

4、 flatMap ,flatMapToInt,flatMapToLong,flatMapToDuble

flatMap : 接口接受一个函数作为参数,并将其映射成一个新的元素。由方法生成的各个流会被合并或者扁平化为 一个单一的流

flatMapToInt,flatMapToLong,flatMapToDuble 同map一样 专门针对不同数据类型的特殊处理接口。

这里需要区别下map 和 flatMap。map会直接返回新生成的所有流,而flatMap会做合并或者扁平化所有流,最终返回一个单一流。比如如下代码:

List<String> list1 = Arrays.asList("apple", "bag", "led");
List<String> list2 = Arrays.asList("toy", "boy", "tree");
//map
System.out.println("map:");
Stream.of(list1,list2).map(str->str.stream().map(String::toUpperCase)).forEach(System.out::println);
//flatMap
System.out.println("flatMap:");
Stream.of(list1,list2).flatMap(str->str.stream().map(String::toUpperCase)).forEach(System.out::println);
输出结果:
       map:
    java.util.stream.ReferencePipeline$3@458ad742
    java.util.stream.ReferencePipeline$3@5afa04c
    flatMap:
    APPLE
    BAG
    LED
    TOY
    BOY
    TREE

由此可见,对于流组成的列表,map 返回的是流列表中的流,而flatMap是把所有流合并成的一个流,输出流中的内容。

5 、peek 遍历每一个元素,返回一个流,只可输出和做些外部处理。

List<String> list1 = Arrays.asList("apple", "bag", "led");
list1.stream().map(String::toUpperCase).peek(System.out::println).map(String::toLowerCase).forEach(System.out::println);

这里补充下 peek和map区别,首先看下peek和map接口:

Stream peek(Consumer<? super T> action);
Stream map(Function<? super T, ? extends R> mapper);
可以看到peek入参是Consumer,没有返回值,而map的入参是Function,可以返回处理后的数据,所以peek一般用来中间操作输出,便于debug 。

4.2.2 常用有状态操作
1 、distinct 去除流中的重复元素,注意元素需要实现 hashCode() 和 equals()。

List<String> numbers = Arrays.asList("1", "2", "1", "3", "3", "2", "4");
numbers.stream().distinct().forEach(System.out::println);

2 、sorted 排序,有两个方法,默认按照元素预定义的顺序排序进行排序,也可以传入Comparator 自定义排序。

如果没有定义顺序,则不保证每次都是有序的。

list.stream().sorted().forEach(System.out::println);
list.stream().sorted((s1, s2) -> s1.getId().compareTo(s2.getId())).forEach(System.out::println);

3 、limit 获取前n个元素,注意如果是个有序且并行的流,则比较影响性能,如果并行流需要使用limit,对顺序又没要求,可以先使用unordered 置为无序的。

List<String> numbers = Arrays.asList("1", "2", "1", "3", "3", "2", "4");
numbers.stream().limit(3).forEach(System.out::println);
numbers.parallelStream().unordered().limit(3).forEach(System.out::println);

4、 skip 跳过前n元素,如果n大于元素数量,则返回空流,配合limit 可实现分页,同limit一样,如果并行流需要使用skip ,对顺序又没要求,可以先使用unordered 置为无序的。

List<String> numbers = Arrays.asList("1", "2", "1", "3", "3", "2", "4");
numbers.stream().limit(5).skip(2).forEach(System.out::println);
numbers.parallelStream().unordered().limit(3).forEach(System.out::println);
skip 和 limit其实底层调的是一个方法SliceOps.makeRef,只是传的参数不同

4.3 常用终端操作
4.3.1 常用非短路操作
1、forEach 为流中的每一个元素执行一个动作,并行时不确保执行顺序。

这里说下和peek区别,两者入参都是Consumer,没返回值,但是peek是中间操作,完成后会返回流,继续下一步操作,而forEach 是终端操作,直接结束流。

List<String> numbers = Arrays.asList("1", "2", "1", "3", "3", "2", "4");
numbers.stream().forEach(System.out::println);
numbers.parallelStream().forEach(System.out::println);
//集合还可以简写
numbers.forEach(System.out::println);

2、forEachOrdered 按顺序为流中的每一个元素执行一个动作,并行时也可确保执行顺序。

List<String> numbers = Arrays.asList("1", "2", "1", "3", "3", "2", "4");
numbers.stream().forEachOrdered(System.out::println);
numbers.parallelStream().forEachOrdered(System.out::println);

3、toArray 流转换成数组

List<String> numbers = Arrays.asList("1", "2", "1", "3", "3", "2", "4");
int[] array = numbers.stream().mapToInt(NumberUtils::toInt).toArray();

4、reduce 归约操作,该操作对每个元素重复应用一个操作(例如,将两个数字相加),直到产生结果为止。在函数式编程中,它通常被称为折叠操作。不少接口比如max,min的底层都是通过调用reduce来实现的。

reduce 有三种接口,下面分别介绍下:

第一种不设置初始值,只传一个有两个参数的函数式接口BinaryOperator,返回一个Optional

//接口定义
Optional<T> reduce(BinaryOperator<T> accumulator)
//求和    
Optional<Integer> reduce1 = Stream.of(2, 4, 6, 7).reduce(Integer::sum);

第二种是设置初始值,再加一个有两个参数的函数式接口BinaryOperator,可以直接返回Stream数据同类型结果,这种操作是从初始值开始计算。

//接口定义
T reduce(T identity, BinaryOperator<T> accumulator);
//求和
Optional<Integer> reduce1 = Stream.of(2, 4, 6, 7).reduce(0,Integer::sum);

第三种比较复杂,通常用于并行处理,可以提供一个不同于Stream中数据类型的初始值,通过accumulator计算,最终得到一个U类型的结果返回。然后combiner 再将各个不同线程的结果合并起来,最终得到一个同设置的初始值同类型的结果。

//接口定义
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
//求和
 List<Integer> integers = Arrays.asList(2, 4, 6, 7,Integer.MAX_VALUE);
 long reduce2 = integers.parallelStream().reduce(0L, (a, b) -> a + b, (a, b) -> a+b);

简单来说,前面两种方式得到的结果,必须与Stream元素类型一致,这样特定情况下就会出现问题,比如不同int相加,可能会超出int范围。但第三种方式的调用也需要注意,如果不是并行流进行处理,第三个参数是一直不会被调用的。

5、collect 将java.util.stream .Collector中的各种收集器作为参数, 用于将流的元素累积为汇总结果,这里可以直接用官方提供的Collectors类,里面的提供的方法非常丰富,比如可以把流中的数据汇总成各种集合,求最大值,最小值,求和,分组,求平均数等等

List<Integer> integers = Arrays.asList(2, 4, 6, 7,Integer.MAX_VALUE);
integers.stream().map(a -> a + 1).collect(Collectors.toList());
integers.stream().map(a -> a + 1).collect(Collectors.toSet());
integers.stream().map(a -> a + 1).collect(Collectors.maxBy(Comparator.naturalOrder()));
integers.stream().map(a -> a + 1).collect(Collectors.minBy(Comparator.naturalOrder()));
//求平均值,分组
transactions.stream().collect(groupingBy(Transaction::getCity,groupingBy(Transaction::getCurrency, 
averagingInt(Transaction::getValue))));

当然,你也可以自定义汇总方法,只需要实现java.util.stream .Collector接口就可以,这里就不再展开。

6、max,min

max 返回流中元素最大值

min 返回流中元素最小值

这两个比较简单,其实底层使用的还是reduce

List<Integer> numbers = Arrays.asList(2, 4, 6, 7,8,9);
 Optional<Integer> max = numbers.stream().max(Comparator.naturalOrder());        
 Optional<Integer> min = numbers.stream().min(Comparator.naturalOrder());
 
 //其实底层调用的是reduce
 Optional<Integer> max = numbers.stream().reduce(BinaryOperator.maxBy(Comparator.naturalOrder()));
 Optional<Integer> min = numbers.stream().reduce(BinaryOperator.minBy(Comparator.naturalOrder()));

7、count 返回流中元素的总个数,这个比较简单,不过底层实现比较有意思,使用的是mapLong

List<String> numbers = Arrays.asList("1", "2", "1", "3", "3", "2", "4");
long count = numbers.stream().count();
//count 底层实现
numbers.stream().mapToLong(e -> 1L).sum();

4.3.2 短路操作
1、anyMatch,allMatch,noneMatch

anyMatch 流中有一个元素满足时返回true,否则返回false

allMatch 流中每个元素都符合时才返回true,否则返回false,流为空 返回true

noneMatch 流中每个元素都不符合时才返回true,否则返回false

三个接口底层都是调用MatchOps.makeRef,但有一点需要注意,如果流为空anyMatch 返回false,而allMatch 和 noneMatch 返回true,对此,allMatch的官方解释是此方法评估通用量化,声明流的元素(对于所有x P(x))。如果流为空,称量化为满意,并且始终为true,(与P(x)无关),noneMatch也差不多同样解释。

List<Integer> numbers = Arrays.asList(2, 4, 6, 7,8,9);
 boolean isAllMatch = numbers.parallelStream().allMatch(a -> a > 1);
 boolean isAnyMatch = numbers.parallelStream().anyMatch(a -> a > 3);
 boolean isNoneMatch = numbers.parallelStream().noneMatch(a -> a > 2);
 
 //流设置为空
 numbers = new ArrayList<>();
 boolean isAllMatch = numbers.parallelStream().allMatch(a -> a > 1);
 boolean isAnyMatch = numbers.parallelStream().anyMatch(a -> a > 3);
 boolean isNoneMatch = numbers.parallelStream().noneMatch(a -> a > 2);

2、findFirst,findAny

findFirst 返回流中第一个元素

findAny 返回流中的任意元素

两个接口都是调用FindOps.makeRef,但有点需要注意,如果是串行流,则findAny 会一直返回第一个元素,而不是随机返回

List<String> numbers = Arrays.asList("1", "2", "1", "3", "3", "2", "4");
Optional<Integer> first = numbers.stream().findFirst();
Optional<Integer> any = numbers.stream().findAny();
Optional<Integer> parallelAny = numbers.parallelStream().findAny();

五 总结
本次介绍了Stream引入原因、相关概念以及常用操作,并未太过深入,想要掌握Stream 还是需要日常工作中多使用,多研究。这里再给大家推荐一个idea 插件 java Stream debugger ,可以直接debug看流的内部过程。