JDK8新特性05:Stream操作

JDK8新特性05:Stream操作

  • Stream概述
    • Stream组成
    • Stream的使用示例
    • Stream的优势
  • Stream的使用
    • Stream的创建
    • Stream的中间操作
      • `filter()`: 对流进行过滤
      • `map()`与`flatMap()`:对流中的元素进行映射
      • `skip()`,`limit()`:对流进行分页
      • `sorted()`:对流排序
      • `distinct()`:对流中的元素进行去重
    • Stream的终止操作
      • `reduce()`:将流中元素聚合为一个结果
      • `collect()`:将流中元素收集到容器中
      • `max()`,`min()`,`average()`,`count()`,`findAny()`,`findFirst()`:见名知义的终止操作
    • 分组与分区
      • 分组查询`Collectors.groupingBy()`
      • 分区查询`Collectors.partitionBy()`
    • 短路操作(`short-circuiting`)
    • 并行流(`parallelStream`)
  • 使用Stream时注意的问题
    • Stream是消耗性的(`Consumable`),关闭后不能再被使用
    • 不应对流执行有副作用(`side-effect`)的操作
    • 注意无限流产生的问题

Stream概述

Stream是JDK8提供的一种新的资源管理方式.Stream不存储值,而是通过管道的方式获取值,因此对流的操作不会修改底层的数据源.

Stream组成

一个完整的流由三部分组成

  1. 数据源
  2. 零个或多个中间操作: 中间操作都是惰性求值的,例如执行流的filter()方法不会真的对该流进行过滤,而是会返回一个新流,该流在遍历时只会包含原流中通过过滤条件的值.
  3. 一个终止操作: 终止操作是立即求值的,例如forEach()方法,会触发对流的遍历.流一旦执行了终止操作,可以认为该流被消耗了(consumed),不能被再次使用.

Stream的使用示例

  1. 输出区间[3,8)内的所有整数:

    IntStream.of(3, 4, 5, 6, 7).forEach(System.out::println);
    IntStream.range(3, 8).forEach(System.out::println);
    IntStream.rangeClosed(3, 7).forEach(System.out::println);
    
  2. 对集合进行map-reduce运算:

    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);
    list.stream().map(num -> num * 2).reduce(0, Integer::sum);	// 42
    

Stream的优势

  1. Stream操作类似SQL语句是描述性语言而非命令式语言.流操作描述的是结果,而不规定如何实现,由JDK做出优化.
  2. 使用并行流ParallelStream,可以方便地执行多线程操作.

Stream的使用

Stream的创建

  1. 使用of()stream()方法从集合生成流

    Stream<String> stream1 = Stream.of("hello", "world", "hello world");
    
    String[] array = new String[]{"hello", "world", "hello world"};
    Stream<String> stream2 = Stream.of(array);
    
    Stream<String> stream3 = Arrays.stream(array);
    
    List<String> list = Arrays.asList("hello", "world", "hello world");
    Stream<String> stream4 = list.stream();
    
  2. 使用generate(Supplier supplier)方法生成无限流:

    generate()方法通过调用supplier对象的get()方法生成一个无限流.

  3. 使用iterate(T seed, UnaryOperator function)方法生成无限流:

    iterate()方法通过对种子seed迭代调用function对象的apply()方法,生成一个无限流

    Stream<Integer> series = Stream.iterate(1, num -> num * 2);
    // series 为等比数列流{1, 2, 4, 8, ...}
    
  4. 使用concat(Stream stream1, Stream stream2)拼接流:

    Stream<String> stream1 = Stream.of("item1", "item2");
    Stream<String> stream2 = Stream.of("item3", "item4", "item5");
    Stream<String> sumStream = Stream.concat(stream1, stream2);
    // sumStream 为流{"item1", "item2", "item3", "item4", "item5"}
    

Stream的中间操作

Stream的所有中间操作都是惰性求值的,并不会立即进行运算,并返回一个新的Stream.

filter(): 对流进行过滤

filter(Predicate predicate)方法调用predicate对象的test()方法对集合中元素进行过滤.

List<Integer> collect = IntStream.range(3, 8).filter(num -> num > 4).boxed().collect(Collectors.toList());
// 得到 {5, 6, 7}

map()flatMap():对流中的元素进行映射

map(Function mapper)是Stream最常用的中间操作,返回一个新的Stream,包含对原流中的每个元素应用mapper中的apply()方法得到的结果.

List<String> oldList = Arrays.asList("hello", "world", "hello world");
List<String> newList = oldList.stream().map(String::toUpperCase).collect(Collectors.toList());	
// 得到 {"HELLO", "WORLD", "HELLO WORLD"}

flatMap(Function mapper)可以将二维的Stream拍平成为一维的Stream,其中function对象的apply()方法返回的应是Stream对象.

List<String> greetings = Arrays.asList("hello", "hi", "你好");
List<String> names = Arrays.asList("张三", "李四", "王五");

List<String> results = greetings.stream().flatMap(greeting -> names.stream().map(name -> greeting + " " + name))	// 将二维Stream拍平成一维Stream
	.collect(Collectors.toList());
// 得到 {"hello 张三", "hello 李四", "hello 王五", "hi 张三", "hi 李四", "hi 王五", "你好 张三", "你好 李四", "你好 王五"}

skip(),limit():对流进行分页

skip(long n)方法返回一个新Stream对象,其中保存的是原Stream对象中内容除去前n个之后剩下的内容.若原Stream对象中保存元素小于n,则返回一个空的Stream对象.

limit(long maxSize)方法返回一个新Stream对象,其中保存的是原Stream对象中保存内容的前maxSize个.

Stream<Integer> stream = Stream.iterate(1, num -> num * 2);
List<Integer> list = stream.skip(3).limit(5).collect(Collectors.toList());
// 得到 {8, 16, 32, 64, 128}

sorted():对流排序

sorted()sorted(Comparator comparator)方法返回一个新Stream对象,其中保存的是原Stream对象中内容排序后得到的结果.

List<String> unsortedList = Arrays.asList("element3", "element1", "element2");
List<String> sortedList = unsortedList.stream().sorted().collect(Collectors.toList());
// 得到 {"element1", "element2", "element3"}

distinct():对流中的元素进行去重

distinct()方法返回一个新Stream对象,其保存的是原Stream对象中的内容去重之后得到的结果.若原流有序,则去重过程是稳定的(重复元素保留第一次出现者);若原流是无序的,则去重操作不保证稳定性.

List<String> duplicateList = Arrays.asList("element1", "element2", "element3", "element1");
List<String> distinctList1 = duplicateList.stream().distinct().collect(Collectors.toList());
// 得到 {"element1", "element2", "element3"}

Stream的终止操作

Stream所有的终止操作都是及早求值的,也就是说会立即进行运算.执行终止操作之后的流不能被复用.

reduce():将流中元素聚合为一个结果

reduce()方法将流中的所有元素聚合为一个结果,有以下三个重载的方法:

  1. Optional reduce(BinaryOperator accumulator)

  2. T reduce(T identity, BinaryOperator accumulator)

    该重载方法得到的结果与流中元素是同类型的,其两个参数意义如下:

    1. identity: 表示结果的初始值
    2. accumlator: 其apply()方法表示如何将流中的元素结果相聚合,应满足结合律

    T reduce(T identity, BinaryOperator accumulator)方法的结果等价于以下代码块,但执行过程未必是顺序的.

    T result = identity;
    for (T element : thisStream)
        result = accumulator.apply(result, element);
    return result;
    
  3. U reduce(U identity, BiFunction accumulator, BinaryOperator combiner)

    该重载方法得到的结果与流中元素不是同类型的,同时又增加了一个combiner参数.combiner参数仅在并行流中起作用,表示如何将多个并行的流得到的结果汇总.

    Stream<String> unparallelStream = Stream.of("item1", "item2", "item3", "item4");
    Stream<String> parallelStream = Stream.of("item1", "item2", "item3", "item4").parallel();
    
    System.out.println(unparallelStream.reduce("^",
            String::concat,
            // null));	// 非并行流,combiner参数不起作用,甚至可以设为null
            (str1, str2) -> str1 + str2 + "$"));	
    // 得到 "^item1item2item3item4"
    
    System.out.println(parallelStream.reduce("^",
            String::concat,
            (str1, str2) -> str1 + str2 + "$"));
    // 得到 "^item1^item2$^item3^item4$$"
    

collect():将流中元素收集到容器中

collect()方法可以将流中的元素收集到容器中,是一种特化的reduce操作.有两个重载方法:

  1. R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)

    其三个参数意义如下:

    1. supplier: 提供容器的生产者,在并行流中,该生产者的get()方法会被反复调用,每次都生成一个全新的容器.
    2. accumulator: 其accept()方法表示如何将流中的元素存入容器.
    3. combiner: 仅在并行流中有效,其accept()方法表示如何将多个并行的流得到的结果汇总.

    下面两个例子演示collect()方法的使用

    // 将流中的元素收集到List中
    List<String> List = stringStream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
    
    // 将流中的元素收集到String中
    String concat = stringStream.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();    
    
  2. R collect(Collector collector)

    Collector类将supplier,accumlator,combiner三个参数封装到一个对象中,其帮助类Collectors提供很多方法获取常用的Collector对象.

    stream.collect(Collectors.toList());	// 将流收集为List
    stream.collect(Collectors.toSet());		// 将流收集为Set
    stream.collect(Collectors.toCollection(ArrayList::new));	// 将流收集为某种集合
    stream.collect(Collectors.joining());	// 将字符串流拼接成字符串   
    

max(),min(),average(),count(),findAny(),findFirst():见名知义的终止操作

上述终止操作返回的都是Optional对象,其函数行为可以见名知义.

分组与分区

分组查询Collectors.groupingBy()

Collectors.groupingBy(Function classifier, Collector downstream)可以对流进行分组查询.返回值为一个Map,其keyclassifier参数得到的结果,value为对组内的元素使用downstream收集得到的结果.两个参数的意义如下:

  • classfier: 分类器,表示返回结果的key的取值.
  • downstream: 收集器,对组内元素进行收集后作为返回参数的value的取值.其默认取值为Collectors.toList(),表示将分组结果收集到List中.
Student student1 = new Student("zhangsan", 100, 20);
Student student2 = new Student("lisi", 90, 20);
Student student3 = new Student("wangwu", 90, 30);
Student student4 = new Student("zhangsan", 80, 40);
List<Student> students = Arrays.asList(student1, student2, student3, student4);

Map<String, List<Student>> listMap = students.stream()
    .collect(Collectors.groupingBy(Student::getName));
/* 得到
{ lisi=[Student{name='lisi', age=90, score=20}], 
  zhangsan=[Student{name='zhangsan', age=100, score=20}, Student{name='zhangsan', age=80, score=40}], 
  wangwu=[Student{name='wangwu', age=90, score=30}]} */

Map<String, Long> longMap = students.stream()
    .collect(Collectors.groupingBy(Student::getName, Collectors.counting()));
// 得到 {lisi=1, zhangsan=2, wangwu=1}

分区查询Collectors.partitionBy()

分区查询是分组查询的一种,其分类条件应为一个bool值,只有truefalse两种情况.

Student student1 = new Student("zhangsan", 100, 20);
Student student2 = new Student("lisi", 90, 20);
Student student3 = new Student("wangwu", 90, 30);
Student student4 = new Student("zhangsan", 80, 40);
List<Student> students = Arrays.asList(student1, student2, student3, student4);

Map<Boolean, List<Student>> listMap = students.stream()
        .collect(Collectors.partitioningBy(student -> student.getAge() >= 90));
/* 得到
{ false=[Student{name='zhangsan', age=80, score=40}],
  true=[Student{name='zhangsan', age=100, score=20}, Student{name='lisi', age=90, score=20}, Student{name='wangwu', age=90, score=30}]}
 */

Map<Boolean, Double> doubleMap = students.stream()
        .collect(Collectors.partitioningBy(student -> student.getAge() >= 90, Collectors.averagingDouble(Student::getScore)));
// 得到 {false=40.0, true=23.333333333333332}

短路操作(short-circuiting)

一些流操作是短路(short-circuiting)的(例如limit(),findFirst()).对于这些操作,传入无限流,会返回有限流.这些短路操作使得对无限流进行操作和计算成为可能.

下面程序中,findFirst()方法将流操作短路了:

OptionalInt firstLength = list.stream().mapToInt(str -> {
    System.out.println("遍历到 " + str);
    return str.length();
}).findFirst();

程序仅输出:

遍历到 hello

但短路操作不是总能发挥短路作用:

List<String> list = Arrays.asList("hello", "world", "helloworld");
OptionalInt firstLength = list.stream().mapToInt(str -> {
    System.out.println("遍历到 " + str);
    return str.length();
}).sorted().findFirst();

程序输出:

遍历到 hello
遍历到 world
遍历到 helloworld

并行流(parallelStream)

并行流(parallelStream)可以使用Collection.parallelStream()BaseStream.parallel()方法生成.

并行流的所有操作及其结果与顺序流是完全相同的,二者的唯一区别在于执行终止操作时,sequentialStream顺序遍历所有元素,而parallelStream开启多个线程访问每个元素,其底层是由fork-join线程池实现的.

int sumOfWeights = widgets.parallelStream()
    .filter(b -> b.getColor() == RED)
    .mapToInt(b -> b.getWeight())
    .sum();

可以使用BaseStream.isParallel()方法判断当前流是否为并行流,使用BaseStream.sequential()BaseStream.parallel()完成并行流和顺序流之间的转换.

使用Stream时注意的问题

Stream是消耗性的(Consumable),关闭后不能再被使用

iterator类似,Stream是消耗性的资源,只能被使用一次.对Stream进行任何终止操作都会导致流的关闭,使用关闭后的流会抛出IllegalStateException异常.

IntStream intStream = IntStream.range(1, 11);
System.out.println(intStream.average());	// OptionalDouble[5.5]
System.out.println(intStream.sum());		// java.lang.IllegalStateException: stream has already been operated upon or closed

不应对流执行有副作用(side-effect)的操作

对于绝大多数流的操作,都不应该执行有副作用(side-effect)的操作:因为流操作不保证执行顺序;同时有可能带来线程安全问题.只有在少数流操作(如forEach()peek()方法)中,才可以执行带有副作用的操作.

很多带有副作用的操作可以用归约(reduce)操作来代替,例如下面代码在并行流中会带来线程安全问题:

ArrayList<String> results = new ArrayList<>();
stream.filter(s -> pattern.matcher(s).matches())
    .forEach(s -> results.add(s));  // Unnecessary use of side-effects!

上述forEach()操作会带来线程安全问题,可以用collect()操作来代替

List<String>results = stream.filter(s -> pattern.matcher(s).matches())
    .collect(Collectors.toList());  // No side-effects!

注意无限流产生的问题

对无限流进行终止操作时要注意是否做出了有效的截断,否则会很危险:

// 没做出截断
IntStream.iterate(0, i -> (i + 1) % 2).distinct().forEach(System.out::println);

// 做出无效截断
IntStream.iterate(0, i -> (i + 1) % 2).distinct().limit(6).forEach(System.out::println);

// 做出有效截断
IntStream.iterate(0, i -> (i + 1) % 2).limit(6).distinct().forEach(System.out::println);

你可能感兴趣的:(#,JDK8新特性)