JDK8流式编程Stream

例子

先看个例子,用户购买了三件商品,价格分别为[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()再看一下结果。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,110评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,443评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,474评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,881评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,902评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,698评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,418评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,332评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,796评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,968评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,110评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,792评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,455评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,003评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,130评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,348评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,047评论 2 355