Java8新特性-Stream流

563 阅读8分钟

简介

  • 什么是Stream流?

举个简单的例子,当我们看在线视频的时候,应用或者浏览器会先将一小部分视频文件加载到计算机中并开始播放,也就是说在看视频之前,不需要下载完整的视频,这是视频流。类比到Stream中,这个视频文件的一小部分,就是流(Stream),整个视频就是集合(Collection)。

  1. Cllection是一种数据结构,所有经过计算或者确定的元素才能塞入集合,存储在内存中。
  2. Stream流不是数据结构,它在概念上就是一个管道,其中的元素是按需计算的。使用者从stream中提取他们需要的值。并且提取流中的元素这个过程对用户不可见,更像是一种生产者-消费者的模型。
  • Stream流有哪些特性?
  1. Stream流是java 8的新特性,搭配Lambda表达式使用效果更佳,更高效,下班更快!!!
  2. 根据数据源获取Stream流之后,每次流操作后都会返回一个新的流对象,方便进行链式操作,且不会对原有流对象数据有任何影响。唯有peek操作除外,只有peek操作可以修改流中元素。
  3. 惰性求值,流在中间处理过程中,只是对操作进行了记录,并不会立即执行,需要等到执行终止操作的时候才会进行实际计算。

注:本文中使用到的示例代码,已上传至Github [点击查看] (github.com/DAQ121/Best…)

创建流

  • 使用Stream.of() Stream的静态方法
Stream<Integer> stream = Stream.of(1,2,3,4,5,6,7,8,9); 
stream.forEach(p -> System.out.println(p));
  • 使用Collection.stream() 获取集合中的元素创建流
List<String> list = new ArrayList<Integer>(); 
Stream<String> stream = list.stream(); //获取一个顺序流
Stream<String> parallelStream = list.parallelStream(); //获取一个并行流
  • 使用Stream.builder() 创建流
Stream<Object> stream = Stream.builder().add("12").add("qq").build();
  • 使用 BufferedReader.lines()方法,将文件每行内容转成流
BufferedReader reader = new BufferedReader(new FileReader("F:\\test_stream.txt"));
Stream<String> lineStream = reader.lines();
lineStream.forEach(System.out::println);
  • 使用 Pattern.splitAsStream()方法,将字符串分隔成流
Pattern pattern = Pattern.compile(",");
Stream<String> stringStream = pattern.splitAsStream("a,b,c,d");
stringStream.forEach(System.out::println);

在实际工作中,最常用的应该就是基于集合创建流。在Java7中添加的Fork/Join 框架,我们可以很轻松的在程序中实现并行操作,使用Stream时启用并行性也很简单。创建方式也很简单,调用stream()创建顺序流,调用parallelStream()获取并行流。

  • 串行流: 单线程按照顺序执行流中的操作。
  • 并行流: 充分使用多核CPU,就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流,底层使用Fork/Join框架进行处理,效率要比串行流要高,也会存在线程不安全问题。

操作流

Stream流的操作可以分为两种类型:

  • 中间操作: 可以有多个,每次返回一个新的流,可进行链式操作。
  1. 无状态: 指元素的处理不受之前元素的影响;
  2. 有状态: 指该操作只有拿到所有元素之后才能继续下去。
  • 结束操作: 只能有一个,执行完这个流也就用光了,无法执行下一个操作,因此只能放在最后。
  1. 非短路操作: 指必须处理所有元素才能得到最终结果;
  2. 短路操作: 指遇到某些符合条件的元素就可以得到最终结果,如 A || B,只要A为true,则无需判断B的结果。

流操作.png

中间操作

筛选与切片

  • filter:过滤流中的某些元素,留下满足filter中表达式的元素
  • limit(n):获取前n个元素
  • skip(n):跳过前n元素,配合limit(n)可实现分页
  • distinct:通过流中元素的hashCode()和equals()去除重复元素
Stream.of(6, 4, 6, 7, 3, 9, 8, 10, 12, 14, 14) 
    .filter(s -> s > 5) // 6 6 7 9 8 10 12 14 14
    .distinct() //6 7 9 8 10 12 14
    .skip(2) //9 8 10 12 14
    .limit(2) //9 8
    .forEach(System.out::println);

映射 / 消费

  • map:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
  • flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。当有嵌套集合的数据时,可以使用flatMap操作。
List<Student> students = new ArrayList<>();
students.add(Student.builder().name("daiaoqi").age(23).sex("man").grade(80).build());
students.add(Student.builder().name("wutong").age(25).sex("woman").grade(78).build());
students.add(Student.builder().name("kanghaike").age(19).sex("woman").grade(98).build());
students.add(Student.builder().name("jieweicheng").age(23).sex("man").grade(89).build());
students.add(Student.builder().name("jieweicheng").age(23).sex("man").grade(89).build());

// 简单的map映射
students.stream().map(student -> {
    student.setGrade(student.getGrade() + 100);
    return student;
}).forEach(System.out::println);


List<Person> persons = new ArrayList<>();
persons.add(Person.builder().age(12).money(10000).name("daq").sex("nan").students(students).build());
persons.add(Person.builder().age(13).money(12123).name("keke").sex("nv").students(students).build());

// 当一个对象中的对象时,例如要操作persons中的students列表时,就需要将students中的元素全部转换成流一起操作
persons.stream().flatMap(person -> person.getStudents().stream())
        .map(Student::getAge)
        .collect(Collectors.toSet())
        .forEach(System.out::println);
  • peek:如同于map,能得到流中的每一个元素。但map接收的是一个Function表达式,有返回值,而peek接收的是Consumer表达式,没有返回值。而且peek是唯一可以改变原有流对象的数据的操作。
students.stream().map(student -> 
    student.setGrade(student.getGrade() + 100)
    ).forEach(System.out::println);
    
// peek是唯一可以改变原有流对象的数据的操作
students.stream().peek(student -> student.setGrade(student.getGrade() + 100))
                 .forEach(System.out::println);

排序

  • sorted():自然排序,流中元素需实现Comparable接口
  • sorted(Comparator com):定制排序,自定义Comparator排序器
List<String> list = Arrays.asList("aa", "ff", "dd");

//String 类自身已实现Compareable接口
list.stream().sorted().forEach(System.out::println); // aa dd ff

Student s1 = new Student("aa", 10);
Student s2 = new Student("bb", 20);
Student s3 = new Student("aa", 30);
Student s4 = new Student("dd", 40);
List<Student> studentList = Arrays.asList(s1, s2, s3, s4);
 
//自定义排序:先按姓名升序,姓名相同则按年龄升序
studentList.stream().sorted(
        (o1, o2) -> {
            if (o1.getName().equals(o2.getName())) {
                return o1.getAge() - o2.getAge();
            } else {
                return o1.getName().compareTo(o2.getName());
            }
        }
).forEach(System.out::println);

结束操作

匹配、聚合操作

  • allMatch: 接收一个 Predicate 函数,当流中每个元素都符合条件时才返回true,否则返回false。
  • noneMatch: 接收一个 Predicate 函数,当流中每个元素都不符合条件时才返回true,否则返回false。
  • anyMatch: 接收一个 Predicate 函数,只要流中有一个元素符合条件即返回true,否则返回false。
  • findFirst: 返回流中第一个元素
  • findAny: 返回流中的任意元素
  • count: 返回流中元素的总个数
  • max: 返回流中元素最大值
  • min: 返回流中元素最小值
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
 
boolean allMatch = list.stream().allMatch(e -> e > 10); //false
boolean noneMatch = list.stream().noneMatch(e -> e > 10); //true
boolean anyMatch = list.stream().anyMatch(e -> e > 4);  //true
 
Integer findFirst = list.stream().findFirst().get(); //1
Integer findAny = list.stream().findAny().get(); //1
 
long count = list.stream().count(); //5
Integer max = list.stream().max(Integer::compareTo).get(); //5
Integer min = list.stream().min(Integer::compareTo).get(); //1

收集操作

  • collect: 接收一个Collector实例,将流中元素收集成另外一个数据结构。
Student s1 = new Student("aa", 10,1);
Student s2 = new Student("bb", 20,2);
Student s3 = new Student("cc", 10,3);
List<Student> list = Arrays.asList(s1, s2, s3);

// 转List
List<Integer> ageList = list.stream().map(Student::getAge).collect(Collectors.toList()); // [10, 20, 10]

// 转set
Set<Integer> ageSet = list.stream().map(Student::getAge).collect(Collectors.toSet()); // [20, 10]

// 转成map,注:key不能相同,否则报错
Map<String, Integer> studentMap = list.stream().collect(Collectors.toMap(Student::getName, Student::getAge)); // {cc=10, bb=20, aa=10}

// 字符串分隔符连接
String joinName = list.stream().map(Student::getName).collect(Collectors.joining(",", "(", ")")); // (aa,bb,cc)
  • 收集时进行分组 / 分区 / 规约 等聚合操作
//分组
Map<Integer, List<Student>> ageMap = list.stream().collect(Collectors.groupingBy(Student::getAge));

//多重分组,先根据类型分再根据年龄分
Map<Integer, Map<Integer, List<Student>>> typeAgeMap = list.stream()
        .collect(Collectors.groupingBy(Student::getType,
                Collectors.groupingBy(Student::getAge)));

//分区,分成两部分,一部分大于10岁,一部分小于等于10岁
Map<Boolean, List<Student>> partMap = list.stream().collect(
        Collectors.partitioningBy(v -> v.getAge() > 10)
);

//规约
Integer allAge = list.stream().map(Student::getAge)
        .collect(Collectors.reducing(Integer::sum))
        .get(); //40

总结

  • 使用Stream流有哪些优缺点?
  1. 使用Stream API能够写出更简短的逻辑代码,可阅读性也比较强。
  2. 但是提升了易用度的同时势必会出现性能问题。比起使用for循环或者迭代器,性能肯定是差很多的。
  3. 在使用并行流时,由于底层使用的是Java7中引入的Fork/Join框架,可能导致程序崩溃或者多线程错误。
  • 该如何使用Stream?
  1. 对于简单操作,如最简单的遍历,Stream串行API性能不如显示迭代,但并行的Stream API能够发挥多核特性。
  2. 对于复杂操作,串行API性能可以和手动实现的效果匹敌,在并行执行时Stream API效果远超手动实现。
  3. 数据量小时,肯定是for循环性能最好。stream的优势是并行处理,数据量越大,stream的优势越明显,但是当数据量过大时,就会在数据库中完成这些操作。
  4. 在多核情况下,推荐使用并行parallelStream()来发挥多核优势。单核情况下不建议使用parallelStream()。同时,由于并行流是线程不安全的,所以在执行代码块时,也要做好同步处理。
  • 使用并行流时需要注意什么?
  1. parallelStream并行流是线程不安全的。
  2. I/O密集型 磁盘I/O、网络I/O都属于I/O操作,这部分操作是较少消耗CPU资源,并行流不适用于I/O密集型的操作,就比如使用并流行进行大批量的消息推送,涉及到了大量I/O,使用并行流反而效率会降低很多。
  3. 使用并行流的时候是无法保证元素的顺序的,即使用了同步集合也只能保证元素都正确但无法保证其中的顺序。

待补充...