⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 www.cnblogs.com/CodeBlogMan/p/18156081 「CodeBlogMan」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前言

在之前的开发中,遇到了这样的需求:记录某个更新操作之前的数据作为日志内容,之后可以供管理员在页面上查看该日志。

思路:

  1. 更新接口拿入参与现在数据库该条数据逐一对比,将不同的部分取出;
  2. 在更新操作前取出现在数据库的该条数据,更新操作后再取出同一条数据,比较两者的异同。

经过短暂对比后,我选择方案2,理由如下:

  • 前端入参未经过后端真实性校验,即万一进来的不是同一条数据呢?这样是不可靠的。
  • 后端先拿参数去数据库找,如果有这条数据,那么拿出来做对比可以保证更新的是同一条数据。

要点:

  1. 从数据库里拿出来的一条数据其实是个实体类对象,那是否可以两个对象逐一比较属性值是否相等呢?这个不现实,因为引用类型的对象在内存中的地址肯定不同,所以对象 .equals() 的结果永远是 false;
  2. 既然对象不能直接比较,那么就将其先转换为一个集合后再进行 Stream 操作;
  3. 这里需要比较的两个集合的元素属性名相同,但是值不一定相同;

一、集合的比较

具体情况可以分为:1、是否需要得到一个新的流?2、是否只需要一个简单 boolean 结果?

我开发需求是要得到具体哪些数据不一样,所以选择返回一个新的流,只是得到一个 boolean 来判断是否相同是不够的。

1.1需要得到一个新的流

  • 如果是得到一个新的流,那么推荐使用.filter() + .collect()

    @Test
    publicvoidtestFilter(){
    //第一个数组
    List<ListData> list1 = newArrayList<>();
    list1.add(newListData("测测名字11",11,"email@11"));
    list1.add(newListData("测测名字22",22,"email@22"));
    list1.add(newListData("测测名字33",33,"email@33"));
    log.info("第一个数组为:{}", list1);
    //第二个数组
    List<ListData> list2 = newArrayList<>();
    list2.add(newListData("测测名字111",111,"email@11"));
    list2.add(newListData("测测名字22",22,"email@22"));
    list2.add(newListData("测测名字33",33,"email@33"));
    log.info("第二个数组为:{}", list2);
    //返回一个新的结果数组
    List<ListData> resultList = list1.stream()
    //最外层的filter里是条件,这个条件需要返回一个boolean:符合条件返回true,不符合条件返回false
    .filter(p1 -> list2.stream()
    //这个filter也是条件:判断两个数组里名字和年龄是否都相等,符合条件返回true,不符合条件返回false
    .filter(p2 -> p2.getName().equals(p1.getName()) && p2.getAge().equals(p1.getAge()))
    //如有内容则返回流中的第一条记录,其它情况都返回空
    .findFirst().orElse(null)
    //这个是最外层的filter的断言
    == null)
    //将上一步流处理的的结果,收集成一个新的集合
    .collect(Collectors.toList());
    log.info("经过 Stream 流处理后输出的结果数组为: {}", resultList);
    }

    结合.filter() + noneMatch() 其实也与上面的语句效果相同:

           List<ListData> resultList = list1.stream()
    .filter(p1 -> list2.stream()
    //这个 noneMatch 也是条件:判断两个数组里名字和年龄是否都相等,符合条件返回true,不符合条件返回false
    .noneMatch(p2 -> p2.getName().equals(p1.getName()) && p2.getAge().equals(p1.getAge())))
    .collect(Collectors.toList());
    log.info("经过 Stream 流处理后输出的结果数组为: {}", resultList);

    结合 filter() + contains() 方法( 其中 contains() 方法的使用详见 1.2 小节的注意事项),与以上的效果也一样:

    List<ListData> resultList = list1.stream().filter(p1 -> !list2.contains(p1)).collect(Collectors.toList());
    log.info("经过 Stream 流处理后输出的结果数组为: {}", resultList);

    下面是以上代码的运行结果如图 1 所示:

图1

1.2只需要一个简单 boolean 结果

  • 如果只需要一个简单的 boolean 结果,那么推荐使用.anyMatch() 或者 allMatch()

    //返回一个boolean结果
    booleanflag= list1.stream()
    //只要流中任意一个元素符合条件则返回true,否则返回false
    .anyMatch(p1 -> list2.stream()
    //如果流中全部元素都符合条件,就返回true,否则返回false;当流为空时总是返回true
    .allMatch(p2 -> p2.getName().equals(p1.getName()) && p2.getAge().equals(p1.getAge())));
    log.info("经过 Stream 流对比是否相等: {}", flag);

    下面是以上代码的运行结果如图 2 所示:

    图2

  • 除了 Stream 流之外,还可以使用 JDK 自带的.contains() 相关方法来判断

    //List 集合接口自带的方法 
    booleanisEqual= list1.containsAll(list2) && list2.containsAll(list1);
    //与上述方法效果一致
    booleanisEqual= list1.stream().anyMatch(p1 -> list2.contains(p1));
    //下面的是上述语句的 lambda 表达式写法
    //boolean isEqual = list1.stream().anyMatch(list2::contains);

    注意事项:.contains() 相关方法底层是迭代器 Iterator 以及 .equals() 方法,需要为 List 集合包含的泛型 中重写.equals() 方法才能使用,举例如下所示:

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    publicclassListData {
    private String name;
    private Integer age;
    private String email;
    @Override
    publicbooleanequals(Object o) {
    if (this == o) returntrue;
    if (o == null || getClass() != o.getClass()) returnfalse;
    ListDatalistData= (ListData) o;
    return Objects.equals(name, listData.name) && Objects.equals(age, listData.age) && Objects.equals(email, listData.email);
    }
    }

    下面是以上代码的运行结果如图 3 所示:

    图3

  • 理论上可以用 for 循环或者迭代器来做,效果与使用 .containsAll() 方法差不多,但是自己手写的话可能会比较复杂,数据量稍大些的话效率较低,一般不考虑采用,这里我就不演示了。

二、简单集合的对比

上述的集合都是泛型为自定义引用类型的集合,下面分享一些简单集合,如整形、字符串类型集合的 Stream 流对比操作。

2.1整型元素集合

        List<Integer> list1 = Arrays.asList(1, 6,);
List<Integer> list2 = Arrays.asList(3, 2, 1);
//Java 本身提供的 Integer 类已经实现了 Comparable 接口,可以直接.sort() 比较
booleanisEqual= list1.stream().sorted().collect(Collectors.toList())
.equals(list2.stream().sorted().collect(Collectors.toList()));
log.info("是否相等:{}", isEqual);

2.2字符串元素集合

// 先排序然后转成 String 逗号分隔,joining()拼接
List<String> list3 = Arrays.asList("语文","数学","英语");
List<String> list4 = Arrays.asList("数学","英语","语文");
//Java 本身提供的 String 类也已经实现了 Comparable 接口
booleanflag= list3.stream().sorted().collect(Collectors.toList())
.equals(list4.stream().sorted().collect(Collectors.toList()));
log.info("是否相等:{}", flag);

下面是简单集合比较的运行结果,如图 4 所示:

图4

2.3其它比较

不知道大家有没有发现,上述简单类型的类可以直接比较,而自己写的类就不能,会报”cannot be cast to java.lang.Comparable“。

举个例子,对于自定义的引用类型 ListData , Java 不知道应该怎样为 ListData 的对象排序,是应该按名字排序? 还是按年龄来排序?

注意:.sort() 方法底层实现需要依赖 Comparator 接口,那么这个引用类型 ListData 类要自己手动去实现 Comparator() 接口并重写 compare() 方法才能这样做比较。

        List<ListData> list1 = newArrayList<>();
list1.add(newListData("泛型为引用类型", 666, "abc"));
List<ListData> list2 = newArrayList<>();
list2.add(newListData("泛型为引用类型", 888, "def"));
//这里想要收集成为集合进行比较,需要先根据特定的元素排序(年龄),然后再按顺序比较
booleanflag= list1.stream().sorted(Comparator.comparing(ListData::getAge)).collect(Collectors.toList())
.equals(list2.stream().sorted(Comparator.comparing(ListData::getAge)).collect(Collectors.toList()));
log.info("是否相等: {}", flag);

三、Stream 基础回顾

Stream API 是 Java 8 中最为重要的更新之一,是处理集合的关键抽象概念,也是每个 Java 后端开发人员都必须无条件掌握的内容。

Stream 和 Collection 集合的主要区别:Collection 是内存数据结构,重在数据的存储;而 Stream 是集合的操作计算,重在一系列的流式操作。

3.1基本概念

  • Stream 不会自己存储元素,会返回一个持有结果的新的流;
  • Stream 操作是延迟执行的,即一旦执行终止操作,就执行中间操作链,并产生结果;
  • Stream 一旦执行了终止操作,那么就不能再执行中间操作或者其它终止操作。

3.2 Stream 操作的三个步骤

3.2.1创建 Stream

一个数据源(如:集合、数组)来获取一个流,具体有 3 种方式来创建:

  • 通过集合直接创建(最常用)

    //Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:
    //返回一个顺序流
    default Stream<E> stream(){}
    //返回一个并行流
    default Stream<E> parallelStream{}
  • Arrays 也可以获取数组流

    //返回一个流
    publicstatic <T> Stream<T> stream(T[] array){}
  • 调用 Stream 类静态方法 of() 来创建流

    publicstatic<T> Stream<T> of(T... values){}

3.2.2中间操作

每次处理都会返回一个持有结果的新 Stream,即中间操作的方法返回值仍然是 Stream 类型的对象。因此中间操作可以是链式的,可对数据源的数据进行 n 次处理,但是在终止操作前,并不会真正执行;

中间操作可谓是最重要也最常使用的操作,具体分为3种:筛选与切片、映射、排序,如以下表格所示:

  • 筛选与切片

    | 方法 | 描 述 |
    | :——————————————–: | :———————————————————-: |
    | Stream filter(Predicate<? super T> predicate); | 筛选,接收 Predicate 的条件,从流中排除某些元素,返回一个符合该条件的流 |
    | Stream limit(long maxSize); | 截断,使其元素的数量不超过给定数量 |
    | Stream skip(long n); | 跳过,返回一个扔掉了前 n 个元素的流,若流中元素不足 n 个则返回一个空流,可与 limit() 形成互补 |
    | Stream distinct(); | 去重,利用流所生成元素的 hashCode() 和 equals() 去除流中的重复元素 |

  • 映射

    这里只介绍常见的映射方法,flatMap() 的系列方法并不常用。

    | 方法 | 描述 |
    | :—————————————————–: | :———————————————————-: |
    | Stream map(Function<? super T, ? extends R> mapper); | 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。 |
    | LongStream mapToLong(ToLongFunction<? super T> mapper); | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 Long 类型的Stream 流。 |

  • 排序

    | 方法 | 描述 |
    | :———————————————-: | :——————————————-: |
    | Stream sorted(); | 产生一个新流,其中按自然顺序(如Integer)排序 |
    | Stream sorted(Comparator<? super T> comparator); | 产生一个新流,其中按比较器指定的顺序排序 |

3.2.3终止操作

终止操作的方法返回值类型不再是 Stream,而可以是任何不为流的值,如List、Integer 甚至是 void ,因此一旦执行终止操作就会结束整个 Stream操作且不能再次使用。终止操作也很常见,下面就不做具体的分类,都写在一起了,按需使用即可:

方法 描述
boolean anyMatch(Predicate<? super T> predicate); 检查是否所有元素都符合条件,符合就返回 true,不符合则返回 false
boolean allMatch(Predicate<? super T> predicate); 检查是否至少有一个元素符合条件,有则返回 true,无则返回false
boolean noneMatch(Predicate<? super T> predicate); 检查是否所有元素都不匹配条件,都不符合则返回 true,其它情况返回false
Optional findFirst(); 返回流中第一个元素并放置到 Optional 容器中
Optional findAny(); 返回流中任意一个元素并放置到 Optional 容器中
long count(); 返回流中元素的总个数
Optional max(Comparator<? super T> comparator); 经比较器按顺序比较后,返回流中最大值
Optional min(Comparator<? super T> comparator); 经比较器按顺序比较后,返回流中最小值
void forEach(Consumer<? super T> action); 内部迭代,如果要对集合迭代可以直接使用.foreach(),不必经过 Stream
<R, A> R collect(Collector<? super T, A, R> collector); 将流转换为其他形式,如:将 Stream 中元素收集成.toList()、.toSet() 等

这里有个特殊的方法,.groupingBy() 不属于 Stream 而是属于 Collectors:

方法 返回类型 描述
.stream().collect(Collectors.groupingBy()); public static <T, K> Collector<T, ?, Map<K, List>> 根据流中的某属性值对流进行分组,属性为 K,结果为指定的泛型,如 List

四、文章小结

文章到这里就结束了,关于 Stream 流 API 是日常开发中经常会遇到的,熟练运用可以提高我们的开发效率,让我们写出简洁易懂的代码,我们作为后端开发必须重视起来。总有人说它的调试 debug 是个缺点,不妨试试”Trace Current Stream Chain“按钮,可以追踪当前流中的链式变化。

那么今天的分享到这里就结束了,如有不足和错误,还请大家指正。或者你有其它想说的,也欢迎大家在评论区交流!

参考文档:

文章目录
  1. 1. 前言
  2. 2. 一、集合的比较
    1. 2.1. 1.1需要得到一个新的流
    2. 2.2. 1.2只需要一个简单 boolean 结果
  3. 3. 二、简单集合的对比
    1. 3.1. 2.1整型元素集合
    2. 3.2. 2.2字符串元素集合
    3. 3.3. 2.3其它比较
  4. 4. 三、Stream 基础回顾
    1. 4.1. 3.1基本概念
    2. 4.2. 3.2 Stream 操作的三个步骤
      1. 4.2.1. 3.2.1创建 Stream
      2. 4.2.2. 3.2.2中间操作
      3. 4.2.3. 3.2.3终止操作
  5. 5. 四、文章小结