前言

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

思路:

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

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

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

要点:

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

一、集合的比较

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

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

1.1需要得到一个新的流

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

        @Test
    public void testFilter(){
    //第一个数组
    List<ListData> list1 = new ArrayList<>();
    list1.add(new ListData("测测名字11",11,"email@11"));
    list1.add(new ListData("测测名字22",22,"email@22"));
    list1.add(new ListData("测测名字33",33,"email@33"));
    log.info("第一个数组为:{}", list1);
    //第二个数组
    List<ListData> list2 = new ArrayList<>();
    list2.add(new ListData("测测名字111",111,"email@11"));
    list2.add(new ListData("测测名字22",22,"email@22"));
    list2.add(new ListData("测测名字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结果
    boolean flag = 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 集合接口自带的方法
    boolean isEqual = list1.containsAll(list2) && list2.containsAll(list1);
    //与上述方法效果一致
    boolean isEqual = list1.stream().anyMatch(p1 -> list2.contains(p1));
    //下面的是上述语句的 lambda 表达式写法
    //boolean isEqual = list1.stream().anyMatch(list2::contains);

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

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class ListData {
    private String name;
    private Integer age;
    private String email;
    @Override
    public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    ListData listData = (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() 比较
boolean isEqual = 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 接口
boolean flag = 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 = new ArrayList<>();
list1.add(new ListData("泛型为引用类型", 666, "abc"));
List<ListData> list2 = new ArrayList<>();
list2.add(new ListData("泛型为引用类型", 888, "def"));
//这里想要收集成为集合进行比较,需要先根据特定的元素排序(年龄),然后再按顺序比较
boolean flag = 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 也可以获取数组流

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

    public static<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“按钮,可以追踪当前流中的链式变化。

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

参考文档:

【进阶篇】使用 Stream 流对比两个集合的常用操作分享的更多相关文章

  1. 使用传统的方式遍历集合对集合中的数据进行过滤和使用Stream流的方式遍历集合对集合中的数据进行过滤

    使用传统的方式,遍历集合,对集合中的数据进行过滤 class Test{ public static void main(String[] args){ ArrayList<String> ...

  2. 【Java SE进阶】Day13 Stream流、方法引用

    〇.总结 Stream流的方法:forEach.filter.map.count.limit.skip.concat(结合之前的Collectors接口) 方法引用:Lambda的其他类方法体相同,如 ...

  3. Java之IO流进阶篇:内存流,打印流,对象流

    Java中的IO流,即为输入输出流.所谓输入输出流,都是相对于程序而言,程序就是这个参照物.一张图看懂输入输出流: 输入流抽象基类:InputStream,Reader 输出流抽象基类:OutputS ...

  4. 《手把手教你》系列进阶篇之4-python+ selenium自动化测试 - python几种超神操作你都知道吗?(详细教程)

    1. 简介 今天分享和讲解的超神操作,对于菜鸟来说是超神的操作,对于大佬来说也就是几个简单方法的封装和调用.这里讲解和分享这部分主要是为了培养小伙伴们和童鞋们的面向对象的开发思维,对比这样做的好处让你 ...

  5. C#自定义规则对比两个集合的对象是否相等

    IList<获取的类> ret = 类的结果集; return ret.Except(另一个相同类型的对象列表集, new AClassComPare()): public class A ...

  6. Java进阶篇之十五 ----- JDK1.8的Lambda、Stream和日期的使用详解(很详细)

    前言 本篇主要讲述是Java中JDK1.8的一些新语法特性使用,主要是Lambda.Stream和LocalDate日期的一些使用讲解. Lambda Lambda介绍 Lambda 表达式(lamb ...

  7. Java Stream API进阶篇

    本文github地址 上一节介绍了部分Stream常见接口方法,理解起来并不困难,但Stream的用法不止于此,本节我们将仍然以Stream为例,介绍流的规约操作. 规约操作(reduction op ...

  8. 流式思想概述和两种获取Stream流的方式

    流式思想概述 整体来看,流式思想类似于工厂车间的生产流水线 当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个模型步骤方案,然后再按照方法去执行他 这张图中展示 ...

  9. 流思想概述-两种获取Stream流的方式

    流思想概述 注意:请暂时忘记对传统IO流的固有印象 ! 整体来看,流式思想类似与工厂车间的 '生产流水线'. 当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个 ...

  10. 还看不懂同事的代码?超强的 Stream 流操作姿势还不学习一下

    Java 8 新特性系列文章索引. Jdk14都要出了,还不能使用 Optional优雅的处理空指针? Jdk14 都要出了,Jdk8 的时间处理姿势还不了解一下? 还看不懂同事的代码?Lambda ...

随机推荐

  1. java 校验同一张表某个字段值不能重复

    例如 一个实体 user 校验name名字不能重复 思路 1.新增:时比较容易做 直接根据传来的参数 查询实体如果不为空 则查询到了重复值 2.修改:修改需要考虑较多  2.1.既然是不重复 必然是必 ...

  2. 在python中通过面向对象方式,实现烤地瓜案例

    例子:烤地瓜,不同时间,反馈不同状态,并给不同状态地瓜加入不同味道 烤地瓜时间 0-3分钟,生的 4-7分钟,半生不熟的 8-12分钟,熟了 12分钟以上,已烤熟,糊了 用户可以按自己的意思添加调料 ...

  3. 定了!12支队伍进入HarmonyOS极客马拉松2023决赛

      12支队伍将在8月初,华为开发者大会(HDC.Togerther)上展开巅峰对决!

  4. 基于tapd的git commit规范

    现状 开发团队中,总是有人提交代码时的commit内容乱写一通,或者不明确不完整.当回溯代码的时候,很难通过commit内容定位历史记录,只能一条一条查看,找不到就要去问历史参与开发的其他同事,沟通成 ...

  5. 美团二面:如何保证Redis与Mysql双写一致性?连续两个面试问到了!

    引言 Redis作为一款高效的内存数据存储系统,凭借其优异的读写性能和丰富的数据结构支持,被广泛应用于缓存层以提升整个系统的响应速度和吞吐量.尤其是在与关系型数据库(如MySQL.PostgreSQL ...

  6. 力扣441(java&python)-排列硬币(简单)

    题目: 你总共有 n 枚硬币,并计划将它们按阶梯状排列.对于一个由 k 行组成的阶梯,其第 i 行必须正好有 i 枚硬币.阶梯的最后一行 可能 是不完整的. 给你一个数字 n ,计算并返回可形成 完整 ...

  7. 重度使用Flutter研发模式下的页面性能优化实践

    简介: 淘宝特价版是集团内应用Flutter技术场景比较多,且用户量一亿人以上的应用了.目前我们首页.详情.店铺.我的,看看短视频,及评价,设置等二级页面都在用Flutter技术搭建.一旦Flutte ...

  8. [FE] yarn, npm 切换镜像源

    yarn 设置命令如下,会修改 ~/.yarnrc 内容. $ yarn config set registry https://registry.yarnpkg.com npm 设置命令如下,会修改 ...

  9. dotnet C# 反射扫描程序集所有类型会不会触发类型静态构造函数

    在 dotnet 里面,有很多框架都喜欢扫描程序集进行初始化逻辑,在扫描程序集的所有类型的时候,相当于碰到所有类型.而某个类型的静态构造函数将会在某个类型被使用之前被 CLR 调用,那么扫描类型是否会 ...

  10. Anaconda环境下GPT2-Chinese的基本使用记录

    偶然在看到了这个项目,感觉很厉害,于是就折腾了下,跑了一跑 项目地址:https://github.com/Morizeyao/GPT2-Chinese 如果Github下载太慢的可以用这个代下载:h ...