侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 285 篇文章
  • 累计创建 125 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

java8新特性Stream流操作详解及实战1

孔子说JAVA
2022-07-12 / 0 评论 / 0 点赞 / 111 阅读 / 7,844 字 / 正在检测是否收录...

java8 是一个非常成功的版本,这个版本新增的Stream,配合同版本出现的 Lambda,给我们操作集合(Collection)提供了极大的便利。Stream操作就是将要处理的元素集合看作一种流,在流的过程中,借助Stream API对流中的元素进行操作,比如:筛选、排序、聚合等。Stream可以由数组或集合创建,对流的操作分为两种:中间操作和终止操作。

  • 中间操作/函数拼接,返回值类型仍然是 Stream 类型的方法,支持链式调用。(筛选filter、映射map、排序sorted、去重组合skip—limit)

  • 终止/终端操作,返回值类型不再是 Stream 类型的方法,不再支持链式调用。终止/终端操作会产生一个新的集合或值。(遍历foreach、匹配find–match、规约reduce、聚合max–min–count、收集collect)

  • 相关文章:java8新特性Stream流操作详解及实战2

  • 相关文章:java8新特性Stream流操作详解及实战3

注意:Stream和IO流(InputStream/OutputStream)没有任何关系,请暂时忘记对传统IO流的固有印象!

61e79134c33d40c2b34b3911118b71d4

1、Stream常用方法及注意事项

Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工 处理。Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品。

image-1657587100200

image-1657587108984

1.1 常用方法

Stream流模型的操作很丰富,Stream API能让我们快速完成许多复杂的操作,如筛选、切片、映射、查找、去除重复,统计,匹配和归约。下图列出一些常用的API。可以分成两类:中间操作/函数拼接,终止/终端操作。

image-1657585458453

中间操作符

对于数据流来说,中间操作符在执行制定处理程序后,数据流依然可以传递给下一级的操作符。

中间操作符包含8种(排除了parallel,sequential,这两个操作并不涉及到对数据流的加工操作):

  1. map(mapToInt,mapToLong,mapToDouble) 转换操作符,把比如A->B,这里默认提供了转int,long,double的操作符。
  2. flatmap(flatmapToInt,flatmapToLong,flatmapToDouble) 拍平操作比如把 int[]{2,3,4} 拍平 变成 2,3,4 也就是从原来的一个数据变成了3个数据,这里默认提供了拍平成int,long,double的操作符。
  3. limit 限流操作,比如数据流中有10个 我只要出前3个就可以使用。
  4. distint 去重操作,对重复元素去重,底层使用了equals方法。
  5. filter 过滤操作,把不想要的数据过滤。
  6. peek 挑出操作,如果想对数据进行某些操作,如:读取、编辑修改等。
  7. skip 跳过操作,跳过某些元素。
  8. sorted(unordered) 排序操作,对元素排序,前提是实现Comparable接口,当然也可以自定义比较器。

终止操作符

数据经过中间加工操作,就轮到终止操作符上场了;终止操作符就是用来对数据进行收集或者消费的,数据到了终止操作这里就不会向下流动了,终止操作符只能使用一次。

  1. collect 收集操作,将所有数据收集起来,这个操作非常重要,官方的提供的Collectors 提供了非常多收集器,可以说Stream 的核心在于Collectors。
  2. count 统计操作,统计最终的数据个数。
  3. findFirst、findAny 查找操作,查找第一个、查找任何一个 返回的类型为Optional。
  4. noneMatch、allMatch、anyMatch 匹配操作,数据流中是否存在符合条件的元素 返回值为bool 值。
  5. min、max 最值操作,需要自定义比较器,返回数据流中最大最小的值。
  6. reduce 规约操作,将整个数据流的值规约为一个值,count、min、max底层就是使用reduce。
  7. forEach、forEachOrdered 遍历操作,这里就是对最终的数据进行消费了。
  8. toArray 数组操作,将数据流的元素转换成数组。

1.2 注意事项

在使用Stream时有以下几个注意事项:

  1. stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果。
  2. stream不会改变数据源,通常情况下会产生一个新的集合或一个值。(返回的是新的流)
  3. stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行。(不调用终结方法,中间的操作不会执行,即Stream只能执行一次)

2、Stream与传统遍历对比

几乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或间接的遍历操作。当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。接下来我们就从集合遍历方面比较一下传统遍历方式及Stream方式。

在一个ArrayList集合中存储有以下数据: 张无忌、周芷若、赵敏、小昭、殷离、张三、张三丰。

需求1: 获取所有姓张的人员名单
需求2: 获取名字长度为3个字的
需求3: 将上述条件下获取到的数据打印出来

2.1 传统遍历

import java.util.ArrayList;
import java.util.List;
 
public class Demo1List {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("小昭");
        list.add("殷离");
        list.add("张三");
        list.add("张三丰");
 
        List<String> listA = new ArrayList<>();
        for ( String s  : list) {
            if (s.startsWith("张") && s.length() == 3) {
                listA.add(s);
            }    
        }
 
        for (String s: listA) {
            System.out.println(s);
        }
    }
}

传统循环遍历的弊端

这段代码中含有2个循环,每一个作用不同:

  1. 首先筛选所有姓张的且名字有三个字的人;(有些写法会将这2个条件分别写2个循环)

  2. 最后对结果进行打印输出。

可以看出每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。每个需求都要循环一次,还要搞一个新集合来装数据,如果希望再次遍历,只能再使用另一个循环从头开始。

Java 8的Lambda更加专注于做什么(What),而不是怎么做(How)。

循环 是做事情的方式,而不是目的。for循环的语法是“怎么做”,for循环的循环体才是“做什么”。

为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。

有没有更好的方式呢,答案是肯定的,可以参考对比下面的Stream方式。

2.2 Stream

import java.util.ArrayList;
import java.util.List;
 
public class Demo2Steam {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("小昭");
        list.add("殷离");
        list.add("张三");
        list.add("张三丰");
        list.stream()
                .filter(name -> name.startsWith("张"))
                .filter(name -> name.length() == 3)
                .forEach(name -> System.out.println(name));
    }
}

效果显而易见,首先代码更加简洁,其次代码更易理解,直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:

  1. 获取流
  2. 过滤姓张
  3. 过滤长度为3
  4. 逐一打印。

我们真正要做事情的内容被更好地体现在代码中。

2.3 原始集合操作与Stream集合操作对比

再举一个比较全面的对比例子。原始集合操作与Stream集合操作在 “过滤/映射/扁平化/遍历/排序/去重/跳过/截断” 的应用区别。

举例: 打印所有商品、图书类过滤掉、排序、要前面两个、求两件商品的总价、获取两件商品的名称、打印输入结果

1)以原始集合操作实现需求

public void oldCartHandle() {

        //可以理解为获取数组对象为cartSkuList
        List<Sku> cartSkuList = CartService.getCartSkuList();

        /**
         * 1 打印所有商品
         */
        for (Sku sku : cartSkuList) {
            System.out.println(JSON.toJSONString(sku, true));
        }

        /**
         * 2 图书类过滤掉
         */
        List<Sku> notBooksSkuList = new ArrayList<Sku>();
        for (Sku sku : cartSkuList) {
            if (!SkuCategoryEnum.BOOKS.equals(sku.getSkuCategory())) {
                notBooksSkuList.add(sku);
            }
        }

        /**
         * 排序
         */
        notBooksSkuList.sort(new Comparator<Sku>() {
            @Override
            public int compare(Sku sku1, Sku sku2) {
                if (sku1.getTotalPrice() > sku2.getTotalPrice()) {
                    return -1;
                } else if (sku1.getTotalPrice() < sku2.getTotalPrice()) {
                    return 1;
                } else {
                    return 0;
                }
            }
        });
        
        /**
         * 前面两个
         */
        List<Sku> top2SkuList = new ArrayList<Sku>();
        for (int i = 0; i < 2; i++) {
            top2SkuList.add(notBooksSkuList.get(i));
        }

        /**
         * 求两件商品的总价
         */
        Double money = 0.0;
        for (Sku sku : top2SkuList) {
            // money = money + sku.getTotalPrice();
            money += sku.getTotalPrice();
        }

        /**
         * 获取两件商品的名称
         */
        List<String> resultSkuNameList = new ArrayList<String>();
        for (Sku sku : top2SkuList) {
            resultSkuNameList.add(sku.getSkuName());
        }
        
        /**
         * 打印输入结果
         */
        System.out.println(JSON.toJSONString(resultSkuNameList, true));
        System.out.println("商品总价:" + money);
}

2)以Stream流方式实现需求

public void newCartHandle() {
        //多线程安全,防止多线程计数出现冲突,用于计算金额而声明的
        AtomicReference<Double> money =
                new AtomicReference<>(0.0);
        //CartService.getCartSkuList()可以理解为获取数组对象随后进入流操作
        List<String> resultSkuNameList =
                CartService.getCartSkuList()
                        .stream()
                        /**
                         * 1 打印商品信息
                         */
                        .peek(sku -> System.out.println(
                                JSON.toJSONString(sku, true)))
                        /**
                         * 2 过滤掉所有图书类商品
                         */
                        .filter(sku -> !SkuCategoryEnum.BOOKS.equals(
                                sku.getSkuCategory()))
                        /**
                         * 排序
                         */
                        .sorted(Comparator.
                                comparing(Sku::getTotalPrice).reversed())
                        /**
                         * TOP2
                         */
                        .limit(2)

                        /**
                         * 累加商品总金额
                         */
                        .peek(sku -> money.set(money.get() + sku.getTotalPrice()))

                        /**
                         * 获取商品名称
                         */
                        .map(sku -> sku.getSkuName())

                        /**
                         * 收集结果
                         */
                        .collect(Collectors.toList());

        /**
         * 打印输入结果
         */
        System.out.println(JSON.toJSONString(resultSkuNameList, true));
        System.out.println("商品总价:" + money.get());
}

3、Stream的创建

Stream可以通过集合数组创建。

3.1 Stream的3种创建方式

1)通过 java.util.Collection.stream() 方法用集合创建流

List<String> list = Arrays.asList("a", "b", "c");

// 创建一个顺序流
Stream<String> stream = list.stream();

// 创建一个并行流
Stream<String> parallelStream = list.parallelStream();

2)使用 java.util.Arrays.stream(T[] array) 方法用数组创建流

int[] array={1,3,5,6,8};

IntStream stream = Arrays.stream(array);

3)使用Stream的静态方法:of()、iterate()、generate()

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);

Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 3).limit(4);
stream2.forEach(System.out::println);

Stream<Double> stream3 = Stream.generate(Math::random).limit(3);
stream3.forEach(System.out::println);

3.2 stream和parallelStream的区分

stream是顺序流,由主线程按顺序对流执行操作,而parallelStream是并行流,内部以多线程并行执行的方式对流进行操作,但前提是流中的数据处理没有顺序要求。例如筛选集合中的奇数,两者的处理不同之处:

ecd67d8d0f444d0db63d15e40062a48d

  • 如果流中的数据量足够大,并行流可以加快处速度。
  • 除了直接创建并行流,还可以通过parallel()把顺序流转换成并行流。
Optional<Integer> findFirst = list.stream().parallel().filter(x->x>6).findFirst();
List<Integer> list = Arrays.asList(1, 3, 6, 8, 12, 4);
       Optional<Integer> findFirst = list.stream().parallel().filter(x->x>6).findFirst();
       System.out.println("使用Stream的静态方法generate:" + findFirst.get());

4、Stream常用操作使用示例

4.1 stream、collect方法

//将List转化成stream流;
List.stream();

//将List按照转化规则转化成stream流;
Stream.collect(转化规则);

4.2 转换成其他容器

1. 转化成新的List

List<> new = old.stream().collect(Collectors.toList());

2. 转化成Map

Map<T,E> new= old.stream().collect(Collectors.toMap(T,E));

3. 转换成set集合

Set<> new =old.stream().Collectors.toSet();

4. 转换成特定的set集合

TreeSet<> new =old.stream().Collectors.toCollection(TreeSet::new);

4.3 常用方法

1. 过滤

保留Stream流中满足filter()中的条件的元素转换成新List,例中条件为id=5的元素。

old.stream().filter(f->f.id==5);

2. 映射

将Stream流中的元素映射成新的Stream流,例中为从f映射到f的id字段。

old.stream().map(f.getid());

3. 计算

下例中为求所有元素的和。可以理解为:

  • sum = stream流中第一个元素
  • sum = reduce中方法(sum,下一个元素)
  • return sum
old.stream().reduce((a, b) -> a + b);

下例中为求所有元素的和并加上1。可以理解为:

  • sum = reduce中第一个参数
  • sum = reduce中方法(sum,下一个元素)
  • return sum
old.stream().reduce(1,(a, b) -> a + b);

4. 取前几个元素

例中为取前四个元素

old.stream().limit(4);

5. 排序

按照sorted中方法对元素排序,例中为按照元素的weight排序。

old.stream().sorted((f1, f2) -> Integer.compare(f2.getWeight(), f1.getWeight()));

6. 去重

old.stream().distinct();

7. 查找

判断是否存在满足条件的元素

old.stream().anyMatch(f -> f.getid() == 5);

返回第一个满足条件的元素

old.stream().findFirst(f -> f.getid() == 5);
0

评论区