Java并发(Stream API)

Stream(流) 的主要作用是对 集合(Collection) 中的数据进行各种操作,增加了集合对象的功能。

Stream 经常与 Lambda 一起使用,这里的流的Java8的新特性,与Java原本的文件流是完全不同的。

数据流的操作过程,可以看做一个管道,管道由多个节点组成,每个节点完成一个操作。数据流输入这个管道,按照顺序经过每个节点,这种下一个节点必须等待上一个节点执行完毕的方法叫做串行。

数据流的重点不再是对象的运用,而是数据的计算,其特征是:函数式风格,即弱化了面向对象的严格、完整的语法,重心变为通过函数完成数据计算。

流的创建

流的创建有好多种

直接创建

Stream<String> stream = Stream.of("苹果", "哈密瓜", "香蕉", "西瓜", "火龙果");

由数组转换

String[] fruitArray = new String[] {"苹果", "哈密瓜", "香蕉", "西瓜", "火龙果"};
Stream<String> stream = Stream.of(fruitArray);

由集合转换

List<String> fruits = new ArrayList<>();
fruits.add("苹果");
fruits.add("哈密瓜");
fruits.add("香蕉");
fruits.add("西瓜");
fruits.add("火龙果");
Stream<String> stream = fruits.stream();

Stream可以利用forEach()方法迭代

Stream<String> stream = Stream.of("苹果", "哈密瓜", "香蕉", "西瓜", "火龙果");
stream.forEach(System.out::println);

聚合操作

filter()方法

这个方法作用是对数据对象进行过滤

pupils.stream()
    .filter(pupil -> pupil.getAverageScore() >= 80 && pupil.getViolationCount() < 1)

使用 Lambda 语句告诉过滤器,需要哪些符合条件的数据

这里与 Lambda 语句不同的是,因为过滤条件语句为非可执行语句,写在小括号()中,而不是写在{} 中

map()方法

map()方法通称映射,作用为用新的元素将流中原本相同位置的元素替换掉,相对于每一个对象都经历以此转换。

如:

numbers.stream()
    .map(num -> {
        return num * num;
    })
    .forEach(System.out::println);

map() 方法的参数是一个 Lambda 表达式,在语句块中对流中的每个数据对象进行计算、处理,最后用 return 语句返回的对象,就是转换后的对象。

映射后的对象类型可以与流中原始的对象类型不一致,

如在流中,可以用字符串替换原来的整数。这就极大的提供了灵活性和拓展性,让流后继的操作可以更方便。

少数情况下,如果替换语句简单、系统能自动识别需要返回的值,代码可以简写为:

.map(num -> num * num)

当然,最好还是使用 return 来完成映射。

sorted()方法

sorted()方法就是用来排序的方法,把排序规则写成一个 Lambda表达式传给此方法即可,类似于 Collections的sort方法,如:

students.stream()
    // 实现升序排序
    .sorted((student1, student2) -> {
        return student1.getRollNo() - student2.getRollNo();
    })
    .forEach(System.out::println);

注意的是 .sorted((student1, student2) 使用时,student1指代的是后一个元素,student2指代的是前一个元素!

如果语句简单,系统可以自动识别,那么代码可以简写

.sorted((student1, student2) -> student1.getRollNo() - student2.getRollNo())

当然建议还是写完整。

这里需要注意,sort方法返回非正数则两个相比较的元素需要交换位置,返回为正数则不需要。

limit() 方法

limit()方法作用是返回流的前 n 个元素,当然 n 不能为负数。如:

numbers.stream()
    .sorted((n1, n2) -> n2 - n1)
    .limit(3)
    .forEach(System.out::println);

reduce()方法

reduce()方法的作用是合并了所有的元素,终止计算出一个结果,终止指流已经到达了终点结束了,不再继续流动。

forEach()也是流的终点。

reduce()方法返回的是一个比较复杂的对象,需要调用 get()方法返回最终的整数值

同理, get()方法的返回类型是系统自动根据流中的元素类型推定的。如求和:

int sum = numbers.stream()
    .reduce((a, b) -> a + b)
    .get();

reduce() 方法的参数:

  • a 在第一次执行计算语句 a + b 时,指代流的第一个元素;然后充当缓存作用以存放本次计算结果。此后执行计算语句时,a 的值就是上一次的计算结果并继续充当缓存存放本次计算结果。
  • b 参数第一次执行计算语句时指代流的第二个元素。此后依次指代流的每个元素。

注意:a、b 两个参数的作用是由位置决定的,变量名是任意的

结合下图理解:

即前一个参数(a)即为循环的集合

reduce() 方法的第一个参数(本例的 a)有多重作用,并且系统是自动完成参数(本例的 a, b)赋值的,所以仍然体现了 Stream 编程的重点仍然是计算(本例的 a + b)。

reduce()方法也可以操作对象:

Student result = students.stream().reduce(
        (a, b) -> {
            a.setMidtermScore(a.getMidtermScore() + b.getMidtermScore());
            return a;
        }
    )
    .get();

System.out.println(result.getName() + " - " + result.getMidtermScore());

但利用上面方法会导致集合第一个元素被当做了缓存角色,正确性被破坏

一般我们使用操作对象,都会用reduce() 的另一个参数,自己 new 一个对象充当缓冲角色。

Student result = students.stream()
    .reduce(new Student("", 0),
        (a, b) -> {
            a.setMidtermScore(a.getMidtermScore() + b.getMidtermScore());
            return a;
        }
    );

System.out.println(result.getName() + " - " + result.getMidtermScore());

这里reduce方法参数多加上了一个表示缓冲角色。而之后的a指代第一次为缓存

流收集 collect()方法

collect()方法的作用就是收集元素,但元素存放在哪呢?Collection.toList()是一个静态方法,作为参数告诉collect()方法存入一个List集合,所以collect()方法的返回值类型就是 List。

List<String> numResult = numbers.stream()
    .sorted((n1, n2) -> n2 - n1)
    .limit(3)
    .map(a -> "" + a)
    .collect(Collectors.toList());

并行流

上面我们所学的串行流如果计算过程越复杂,数据量就越大,串行性能会越来越低,而这种模式无法发挥多核CPU的优势而难以优化。

为此我们需要用到并行流。

并行流的使用是添加 .parallelStream()方法与串行流的 .stream()方法向对应。

但如果流中的每一个元素之间存在逻辑依赖关系,则不适合使用并行流计算

如想要让数字(1,2,3,4,5)按顺序输出,因为并行流的输出时机是CPU动态决定的,无法确定,所以不能使用并行运算。