JDK8流式编程Stream

413 阅读8分钟

例子

先看个例子,用户购买了三件商品,价格分别为[3.2, 7.3, 5.4],此时用户使用了一张6块钱的兑换券(可以免费兑换一件价格不超过兑换券金额的商品)。

要求:从所有订单商品中找出符合使用兑换券要求的价格最高的商品。

// 兑换金额
private BigDecimal discountAmount = BigDecimal.valueOf(6);
// 使用Stream实现
public BigDecimal orderStream(List<BigDecimal> prices) {
    // 过滤掉价格高于抵扣金额的商品,然后获取价格最高的商品
    return prices.stream()
            .filter(price -> price.compareTo(discountAmount) < 0)
            .max(Comparator.naturalOrder()).get();
}
// 不使用Stream实现
public BigDecimal order(List<BigDecimal> prices) {
    // 将价格从大到小排序,然后遍历所有价格,直到价格 <= 兑换金额
    prices.sort(Comparator.reverseOrder());
    for (BigDecimal price : prices) {
        if (price.compareTo(discountAmount) < 1) {
            return price;
        }
    }
    return null;
}

对比上面两种实现方式,大体思路都是去掉高于抵扣金额的商品然后从剩下的商品中选取金额最大的,但是使用Stream使程序看起来更加简洁易懂。

什么是Stream?

A sequence of elements supporting sequential and parallel aggregate operations.

这句话来源于JDK8中Stream类中,大概意思就是说Stream就是一个支持顺序和并行聚合操作的元素序列。集合和流虽然表面上有一些相似之处,但目标不同。集合主要涉及对其元素的存储和访问。相比之下,流不提供直接访问或操作其元素的方法,而是关注声明性地描述它们的源以及将在该源上聚合执行的计算操作。

Stream的优点以及与普通方式的对比

优缺点 Stream 普通方式
优点 1. 声明式的写法,代码更简单,更易于维护和扩展 1. 对流程更多的控制
2. 可以将多个简单操作复合,完成复杂流水线处理 2. 更强的定制化
3. 由Java来自动的进行并行,性能更好
缺点 1. 实现细节不可见 1. 代码更复杂,不易于维护和扩展
2. 无法自定义过程,只能使用Java Stream提供的操作来处理集合 2. 无法自动化并行

Stream与Iterator性能比较【引用链接

  • 在少低数据量的处理场景中(size<=1000),stream 的处理效率是不如传统的 iterator 外部迭代器处理速度快的,但是实际上这些处理任务本身运行时间都低于毫秒,这点效率的差距对普通业务几乎没有影响,反而 stream 可以使得代码更加简洁
  • 在大数据量(szie>10000)时,stream 的处理效率会高于 iterator,特别是使用了并行流,在cpu恰好将线程分配到多个核心的条件下(当然parallel stream 底层使用的是 JVM 的 ForkJoinPool,这东西分配线程本身就很玄学),可以达到一个很高的运行效率,然而实际普通业务一般不会有需要迭代高于10000次的计算;
  • Parallel Stream 受引 CPU 环境影响很大,当没分配到多个cpu核心时,加上引用 forkJoinPool 的开销,运行效率可能还不如普通的 Stream;
  • 使用建议
    • 简单的迭代逻辑,可以直接使用 iterator,对于有多步处理的迭代逻辑,可以使用 stream,损失一点几乎没有的效率,换来代码的高可读性是值得的
    • 单核 cpu 环境,不推荐使用 parallel stream,在多核 cpu 且有大数据量的条件下,推荐使用 paralle stream
    • stream 中含有装箱类型,在进行中间操作之前,最好转成对应的数值流,减少由于频繁的拆箱、装箱造成的性能损失

Stream的基本操作

Stream的操作大体可以分为以下三种:流创建、中间操作、终端操作。在这个过程中,Stream操作并不会改变源对象,只会返回一个持有结果的新Stream。大致过程如下(图片来源):

流创建

  1. 通过JDK8中Collection新加的stream()和parallelStream()方法创建

    List<Integer> testCollection = Arrays.asList(1, 2, 3, 4);
    Stream<Integer> stream = testCollection.stream();
    Stream<Integer> parallelStream = testCollection.parallelStream();
    
  2. 通过Arrays.stream(T[] array)创建

    // 这里会根据传入的数据类型返回对应的Stream
    int[] arrays = {1, 2, 3, 4};
    IntStream stream = Arrays.stream(arrays);
    
  3. 通过Stream.of(T... values)创建

    Stream<Integer> stream = Stream.of(1, 2, 3, 4);
    
  4. 通过Stream.iterate() 和 Stream.generate()创建无限流,此处要注意通过limit()限制元素的数量,否则会无限创建元素。

    Stream.generate(new TestSupplier()).limit(10).forEach(System.out::println);
    Stream.iterate(0, new TestUnaryOperator()).limit(10).forEach(System.out::println);
    class TestSupplier implements Supplier<Integer> {
        private int num = 0;
        @Override
        public Integer get() {
            return num++;
        }
    }
    class TestUnaryOperator implements UnaryOperator<Integer> {
        private int num = 1;
        @Override
        public Integer apply(Integer integer) {
            return num++;
        }
    }
    
  5. 通过Stream.builder()创建

    /**
     * 注意:add()就是通过调用accept()方法实现的,只不过返回了当前对象,而accept没有返回值
     * 在调用build()方法以后如果再调用add()()或accept()添加元素会抛出异常,
     * 这是由于调用build方法后会将记录元素个数的count的值变为-count - 1,而调用accept如果count小于0时
     * 会抛出异常
     */
    Stream.Builder<Integer> builder = Stream.builder();
    builder.add(1).add(2);
    builder.accept(3);
    builder.build().forEach(System.out::println);
    

中间操作&终端操作

常用中间操作:

  • skip(n):跳过前面n个元素
  • limit(n):取流中的前n个元素
  • sorted(Comparator):对流进行排序
  • filter():过滤元素
  • distinct():消除重复元素
  • map(Function):将函数操作应用在输入流的元素中,并将返回值传递到输出流中。
  • flatMap(Function):将产生流的函数应用在每个元素上(与 map() 所做的相同),然后将每个流都扁平化为元素,因而最终产生的仅仅是元素。

常用终端操作:

  • toArray():生成数组
  • foreEach():遍历流
  • collect(Collector):使用 Collector 收集流元素到结果集合中
  • count():流中的元素个数。
  • max(Comparator):根据所传入的 Comparator 所决定的“最大”元素。
  • min(Comparator):根据所传入的 Comparator 所决定的“最小”元素。

这里我们通过一个完整的例子来讲解中间操作和终端操作。

问题:假设有ABCD四个一级区域,每个一级区域都包含A1、A2这样的二级区域,同时已知每个区域的面积。

  1. 打印所有的二级区域的面积
  2. 求面积最大的二级区域
  3. 求所有区域的面积之和
  4. 根据一级区域面积按照从小到大的顺序进行排序
  5. 求有多少个面积不一样的二级地区
  6. 二级区域中是否有比一级区域面积大的
  7. 获取面积大于100的一级区域
public class Demo {
    private static final int FIRST_SIZE = 4;
    private static final int SECOND_SIZE = 4;

    public static void main(String[] args) {
        List<District> districts = Demo.createDistrict();
        // 打印所有区域
        districts.forEach(System.out::println);

        // 1.打印所有的二级区域面积
        System.out.print("\n1.所有的二级区域面积:");
        districts.stream()
                .flatMap(district -> district.getDistricts().stream())
                .forEach(district -> System.out.format("%d ", district.getArea()));

        // 2.求面积最大的二级区域
        System.out.print("\n2.面积最大的二级区域:");
        int maxArea = districts.stream()
                .flatMap(district -> district.getDistricts().stream())
                .mapToInt(District::getArea)
                .max().getAsInt();
        System.out.print(maxArea);

        // 3.求所有区域的面积之和
        System.out.print("\n3.所有区域的面积之和:");
        int sumArea = districts.stream().mapToInt(District::getArea).sum();
        System.out.print(sumArea);

        // 4.根据区域面积按照从小到大的顺序进行排序
        System.out.println("\n4.根据一级区域面积按照从小到大的顺序进行排序:");
        List<District> sortDistrict = districts.stream()
                .sorted(Comparator.comparingInt(District::getArea))
                .collect(Collectors.toList());
        sortDistrict.forEach(System.out::println);

        // 5.求有多少个面积不一样的二级地区
        System.out.print("5.求有多少个面积不一样的二级地区:");
        long count = districts.stream()
                .flatMap(district -> district.getDistricts().stream())
                .mapToInt(District::getArea)
                .distinct()
                .count();
        System.out.print(count);

        // 6.二级区域中是否有比一级区域面积大的
        System.out.print("\n6.二级区域中是否有比一级区域面积大的:");
        // 获取一级区域中面积最小的
        int minMax = districts.stream().mapToInt(District::getArea).min().getAsInt();
        boolean result = districts.stream()
                .flatMap(district -> district.getDistricts().stream())
                .mapToInt(District::getArea)
                .anyMatch(area -> area > minMax);
        System.out.println(result ? "存在" : "不存在");

        // 7.获取面积大于100的一级区域
        System.out.println("\n7.获取面积大于100的一级区域:");
        districts.stream()
                .filter(district -> district.getArea() > 100)
                .forEach(district -> System.out.println(
                        "区域名称:" + district.getAreaName() +
                       " 区域大小:" + district.getArea()));
    }

    // 生成所有地区
    private static List<District> createDistrict() {
        Random random = new Random(50);
        List<District> districts = new ArrayList<>(FIRST_SIZE);
        for (int first = 0; first < FIRST_SIZE; first++) {
            District districtFirst = new District();
            districtFirst.setAreaName(String.valueOf((char)((int)'A' + first)));
            List<District> districtsSecond = new ArrayList<>(SECOND_SIZE);
            for (int second = 0; second < SECOND_SIZE; second++) {
                District districtSecond = new District();
                districtSecond.setAreaName(districtFirst.getAreaName() + second);
                districtSecond.setArea(random.nextInt(100));
                districtsSecond.add(districtSecond);
            }
            int sum = districtsSecond.stream().mapToInt(District::getArea).sum();
            districtFirst.setArea(sum);
            districtFirst.setDistricts(districtsSecond);
            districts.add(districtFirst);
        }
        return districts;
    }
}
@Getter
@Setter
class District {
    private String areaName;
    private int area;
    private List<District> districts;

    @Override
    public String toString() {
        return "{" +
                " 区域名称:'" + areaName + '\'' +
                " 区域大小:" + area +
                (districts == null ? "" : " 二级区域:" + districts) +
                '}';
    }
}

关于map()与flatMap()

可以看一下上面例子的第一个问题,通过flatMap我们可以将多个流中的元素合并到一起生成一个集合流,而map只能将子元素合并到一起生成集合流

关于parallel()

使用parallel()以后可实现多处理器并行操作。实现原理为将流分割为多个(通常数目为 CPU 核心数)并在不同处理器上分别执行操作。因为我们采用的是内部迭代,而不是外部迭代,所以这是可能实现的。使用parallel()以后可以通过遍历流看一下结果,使用跟不使用parallel有时候结果是不一样的,可以自己再试一下forEachOrdered()再看一下结果。