java8列表中通过stream流根据对象属性去重的三种方式

 更新时间:2024年08月21日 11:55:53   作者:我认不到你  
这篇文章主要介绍了java8列表中通过stream流根据对象属性去重的三种方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

一、简单去重

public class DistinctTest {
    /**
     * 没有重写 equals 方法
     */
    @Setter
    @Getter
    @ToString
    @AllArgsConstructor
    @NoArgsConstructor
    public static class User {
        private String name;
        private Integer age;
    }

    /**
     * lombok(@Data) 重写了 equals 方法 和 hashCode 方法
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class User2 {
        private String name;
        private Integer age;
    }

    @Test
    public void easyTest() {
        List<Integer> integers = Arrays.asList(1, 1, 2, 3, 4, 4, 5, 6, 77, 77);
        System.out.println("======== 数字去重 =========");
        System.out.print("原数字列表:");
        integers.forEach(x -> System.out.print(x + " "));
        System.out.println();
        System.out.print("去重后数字列表:");
        integers.stream().distinct().collect(Collectors.toList()).forEach(x -> System.out.print(x + " "));

        System.out.println();
        System.out.println();

        List<User> list = Lists.newArrayList();
        User three = new User("张三", 18);
        User three2 = new User("张三", 18);
        User three3 = new User("张三", 24);
        User four = new User("李四", 18);
        list.add(three);
        list.add(three);
        list.add(three2);
        list.add(three3);
        list.add(four);
        System.out.println("======== 没有重写equals方法的话,只能对相同对象(如:three)进行去重,不能做到元素相同就可以去重) =========");
        // 没有重写 equals 方法时,使用的是超类 Object 的 equals 方法
        // 等价于两个对象 == 的比较,只能筛选同一个对象
        System.out.println("初始对象列表:");
        list.forEach(System.out::println);
        System.out.println("简单去重后初始对象列表:");
        list.stream().distinct().collect(Collectors.toList()).forEach(System.out::println);

        System.out.println();
        System.out.println();

        List<User2> list2 = Lists.newArrayList();
        User2 five = new User2("王五", 18);
        User2 five2 = new User2("王五", 18);
        User2 five3 = new User2("王五", 24);
        User2 two = new User2("二蛋", 18);
        list2.add(five);
        list2.add(five);
        list2.add(five2);
        list2.add(five3);
        list2.add(two);
        System.out.println("======== 重写了equals方法的话,可以做到元素相同就可以去重) =========");
        // 所以如果只需要写好 equals 方法 和 hashCode 方法 也能做到指定属性的去重
        System.out.println("初始对象列表:");
        list2.forEach(System.out::println);
        System.out.println("简单去重后初始对象列表:");
        list2.stream().distinct().collect(Collectors.toList()).forEach(System.out::println);
    }
}

二、根据对象某个属性去重

0、User对象

    /**
     * 没有重写 equals 方法
     */
    @Setter
    @Getter
    @ToString
    @AllArgsConstructor
    @NoArgsConstructor
    public static class User {
        private String name;
        private Integer age;
    }

1、使用filter进行去重

    @Test
    public void objectTest() {
        List<User> list = Arrays.asList(
                new User(null, 18),
                new User("张三", null),
                null,
                new User("张三", 24),
                new User("张三5", 24),
                new User("李四", 18)
        );
        System.out.println("初始对象列表:");
        list.forEach(System.out::println);
        System.out.println();
        System.out.println("======== 使用 filter ,根据特定属性进行过滤(重不重写equals方法都不重要) =========");
        System.out.println("根据名字过滤后的对象列表:");
        // 第一个 filter 是用于过滤 第二个 filter 是用于去重
        List<User> collect = list.stream().filter(o -> o != null && o.getName() != null)
                .filter(distinctPredicate(User::getName)).collect(Collectors.toList());
        collect.forEach(System.out::println);
        System.out.println("根据年龄过滤后的对象列表:");
        List<User> collect1 = list.stream().filter(o -> o != null && o.getAge() != null)
                .filter(distinctPredicate(User::getAge)).collect(Collectors.toList());
        collect1.forEach(System.out::println);
    }

    /**
     * 列表对象去重
     */
    public <K, T> Predicate<K> distinctPredicate(Function<K, T> function) {
        // 因为stream流是多线程操作所以需要使用线程安全的ConcurrentHashMap
        ConcurrentHashMap<T, Boolean> map = new ConcurrentHashMap<>();
        return t -> null == map.putIfAbsent(function.apply(t), true);
    }

测试

①、疑惑

  • 既然 filter 里面调用的是 distinctPredicate 方法,而该方法每次都 new 一个新的 map 对象,那么 map 就是新的,怎么能做到可以过滤呢

②、解惑

  • 先看一下 filter 的部分实现逻辑,他使用了函数式接口 Predicate ,每次调用filter时,会使用 predicate 对象的 test 方法,这个对象的test 方法就是 null == map.putIfAbsent(function.apply(t), true)
  • 而 distinctPredicate 方法作用就是生成了一个线程安全的 Map 集合,和一个 predicate 对象,且该对象的 test 方法为 null == map.putIfAbsent(function.apply(t), true)
  • 之后 stream 流的 filter 方法每次都只会使用 predicate 对象的 test 方法,而该 test 方法中的 map 对象在该流中是唯一的,并不会重新初始化
    @Override
    public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) {
        Objects.requireNonNull(predicate);
        return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE,
                                     StreamOpFlag.NOT_SIZED) {
            @Override
            Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) {
                return new Sink.ChainedReference<P_OUT, P_OUT>(sink) {
                    @Override
                    public void begin(long size) {
                        downstream.begin(-1);
                    }

                    @Override
                    public void accept(P_OUT u) {
                        if (predicate.test(u))
                            downstream.accept(u);
                    }
                };
            }
        };
    }

2、使用Collectors.toMap() 实现根据某一属性去重(这个可以实现保留前一个还是后一个)

要注意 Collectors.toMap(key,value) 中 value 不能为空,会报错,key 可以为 null,但会被转换为字符串的 “null”

    @Test
    public void objectTest() {
        List<User> list = Arrays.asList(
                new User(null, 18),
                new User("张三", null),
                null,
                new User("张三", 24),
                new User("张三5", 24),
                new User("李四", 18)
        );

        System.out.println("初始对象列表:");
        list.forEach(System.out::println);
        System.out.println();
        System.out.println("======== 使用 Collectors.toMap() 实现根据某一属性去重 =========");
        System.out.println("根据名字过滤后的对象列表 写法1:");
        // (v1, v2) -> v1 的意思 两个名字一样的话(key一样),存前一个 value 值
        Map<String, User> collect = list.stream().filter(Objects::nonNull).collect(Collectors.toMap(User::getName, o -> o, (v1, v2) -> v1));
        // o -> o 也可以写为 Function.identity() ,两个是一样的,但后者可能比较优雅,但阅读性不高,如下
        // Map<String, User> collect = list.stream().filter(Objects::nonNull).collect(Collectors.toMap(User::getName, Function.identity(), (v1, v2) -> v1));
        List<User> list2 = new ArrayList<>(collect.values());
        list2.forEach(System.out::println);
        System.out.println("根据名字过滤后的对象列表 写法2:");
        Map<String, User> map2 = list.stream().filter(o -> o != null && o.getName() != null)
                .collect(HashMap::new, (m, o) -> m.put(o.getName(), o), HashMap::putAll);
        list2 = new ArrayList<>(map2.values());
        list2.forEach(System.out::println);
        
        System.out.println("根据年龄过滤后的对象列表:");
        // (v1, k2) -> v2 的意思 两个年龄一样的话(key一样),存后一个 value 值
        Map<Integer, User> collect2 = list.stream().filter(Objects::nonNull).collect(Collectors.toMap(User::getAge, o -> o, (v1, v2) -> v2));
        list2 = new ArrayList<>(collect2.values());
        list2.forEach(System.out::println);

    }

测试

3、Collectors.toMap() 的变种 使用 Collectors.collectingAndThen()

Collectors.collectingAndThen() 函数 它可接受两个参数,第一个参数用于 reduce操作,而第二参数用于 map操作。

也就是,先把流中的所有元素传递给第一个参数,然后把生成的集合传递给第二个参数来处理。

    @Test
    public void objectTest() {
        List<User> list = Arrays.asList(
                new User(null, 18),
                new User("张三", null),
                null,
                new User("张三", 24),
                new User("张三5", 24),
                new User("李四", 18)
        );
        System.out.println("初始对象列表:");
        list.forEach(System.out::println);
        System.out.println();
        System.out.println("======== 使用 Collectors.toMap() 实现根据某一属性去重 =========");
        System.out.println("根据名字过滤后的对象列表:");
        ArrayList<User> collect1 = list.stream().filter(o -> o != null && o.getName() != null).collect(
                Collectors.collectingAndThen(Collectors.toMap(User::getName, o -> o, (k1, k2) -> k2), x-> new ArrayList<>(x.values())));
        collect1.forEach(System.out::println);
        System.out.println("======== 或者 ==========");
        List<User> collect = list.stream().filter(o -> o != null && o.getName() != null).collect(
                Collectors.collectingAndThen(Collectors.toCollection(
                        () -> new TreeSet<>(Comparator.comparing(User::getName))), ArrayList<User>::new));
        collect.forEach(System.out::println);
    }

测试

三、测试哪个方法比较快

    @Test
    public void objectTest() {
        List<User> list = new ArrayList<>(Arrays.asList(
                new User(null, 18),
                new User("张三", null),
                null,
                new User("张三", 24),
                new User("张三5", 24),
                new User("李四", 18)
        ));
        for (int i = 0; i < 100000; i++) {
            list.add(new User((Math.random() * 10) + "", (int) (Math.random() * 10)));
        }
        System.out.println("======== 测试速度 =========");
        long startTime = System.currentTimeMillis();
        List<User> list1 = list.stream().filter(o -> o != null && o.getName() != null)
                .filter(distinctPredicate(User::getName)).collect(Collectors.toList());
        long endTime = System.currentTimeMillis();
        System.out.println("filter 用时 :" + (endTime - startTime));

        System.out.println();
        startTime = System.currentTimeMillis();
        Map<String, User> map1 = list.stream().filter(o -> o != null && o.getName() != null)
                .collect(Collectors.toMap(User::getName, o -> o, (v1, v2) -> v1));
        List<User> list2 = new ArrayList<>(map1.values());
        endTime = System.currentTimeMillis();
        System.out.println("map1 用时 :" + (endTime - startTime));

        System.out.println();
        startTime = System.currentTimeMillis();
        ArrayList<User> list3 = list.stream().filter(o -> o != null && o.getName() != null).collect(
                Collectors.collectingAndThen(Collectors.toMap(User::getName, o -> o, (k1, k2) -> k2), x -> new ArrayList<>(x.values())));
        endTime = System.currentTimeMillis();
        System.out.println("map2 用时 :" + (endTime - startTime));

        System.out.println();
        startTime = System.currentTimeMillis();
        List<User> list4 = list.stream().filter(o -> o != null && o.getName() != null).collect(
                Collectors.collectingAndThen(Collectors.toCollection(
                        () -> new TreeSet<>(Comparator.comparing(User::getName))), ArrayList<User>::new));
        endTime = System.currentTimeMillis();
        System.out.println("map3 用时 :" + (endTime - startTime));

        System.out.println();
        startTime = System.currentTimeMillis();
        Map<String, User> map2 = list.stream().filter(o -> o != null && o.getName() != null)
                .collect(HashMap::new, (m, o) -> m.put(o.getName(), o), HashMap::putAll);
        List<User> list5 = new ArrayList<>(map2.values());
        endTime = System.currentTimeMillis();
        System.out.println("map4 用时 :" + (endTime - startTime));
    }

测试:

总结

1、去重最快

	ArrayList<User> list3 = list.stream().filter(o -> o != null && o.getName() != null).collect(
                Collectors.collectingAndThen(Collectors.toMap(User::getName, o -> o, (k1, k2) -> k2), x -> new ArrayList<>(x.values())));
	// 或者
	Map<String, User> map2 = list.stream().filter(o -> o != null && o.getName() != null)
                .collect(HashMap::new, (m, o) -> m.put(o.getName(), o), HashMap::putAll);
	List<User> list5 = new ArrayList<>(map2.values());

2、其次

        Map<String, User> map1 = list.stream().filter(o -> o != null && o.getName() != null)
                .collect(Collectors.toMap(User::getName, o -> o, (v1, v2) -> v1));
        List<User> list2 = new ArrayList<>(map1.values());

		// distinctPredicate 是一个方法 本文中有 ,可以 ctrl + f 查找
        List<User> list1 = list.stream().filter(o -> o != null && o.getName() != null)
                .filter(distinctPredicate(User::getName)).collect(Collectors.toList());

3、最慢

	List<User> list4 = list.stream().filter(o -> o != null && o.getName() != null).collect(
                Collectors.collectingAndThen(Collectors.toCollection(
                        () -> new TreeSet<>(Comparator.comparing(User::getName))), ArrayList<User>::new));

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Java实现在线编辑预览office文档详解

    Java实现在线编辑预览office文档详解

    PageOffice是一款在线的office编辑软件,帮助Web应用系统或Web网站实现用户在线编辑Word、Excel、PowerPoint文档,下面我们就来看看如何使用Java实现在线预览office吧
    2024-01-01
  • idea如何自动添加版权许可证信息

    idea如何自动添加版权许可证信息

    这篇文章主要介绍了idea如何自动添加版权许可证信息问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-07-07
  • Springboot设置windows和Linux临时上传路径方式

    Springboot设置windows和Linux临时上传路径方式

    文章介绍了在Windows和Linux系统中设置临时上传路径的方法,对于Windows系统,可以通过修改注册表或者环境变量来实现,而对于Windows和Linux,可以在应用程序中注入Bean来设置临时上传路径
    2026-03-03
  • 彻底卸载、更换jdk全过程

    彻底卸载、更换jdk全过程

    本文介绍了卸载旧版JDK并安装新版JDK的详细步骤,包括卸载JDK、删除注册表中的JavaSoft文件夹、删除环境变量、删除exe文件以及安装新版JDK
    2026-02-02
  • Spring Boot分离配置文件的多种方式总结

    Spring Boot分离配置文件的多种方式总结

    Spring Boot可以外部化程序配置,以便可以在不同环境中使用相同的应用程序代码;当然Spring Boot可以将配置文件进行拆分,以便于激活不同的运行环境,下面这篇文章主要给大家总结介绍了关于Spring Boot分离配置文件的多种方式,需要的朋友可以参考下
    2022-11-11
  • Spring实现定时任务的几种方式总结

    Spring实现定时任务的几种方式总结

    Spring Task 是 Spring 框架提供的一种任务调度和异步处理的解决方案,可以按照约定的时间自动执行某个代码逻辑它可以帮助开发者在 Spring 应用中轻松地实现定时任务、异步任务等功能,提高应用的效率和可维护性,需要的朋友可以参考下本文
    2024-07-07
  • Java中Calendar类用法实例详解

    Java中Calendar类用法实例详解

    这篇文章主要给大家介绍了关于Java中Calendar类用法的相关资料,Calendar类是Java.util包中提供的一个抽象类,该类从JDK1.1开始出现,作为Date类的替代方案,Calendar类中包含了对不同国家地区日历的处理,需要的朋友可以参考下
    2023-09-09
  • java 多态与抽象类详解总结

    java 多态与抽象类详解总结

    在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类,而多态是同一个行为具有多个不同表现形式或形态的能力
    2021-11-11
  • Java中的弗洛伊德(Floyd)算法

    Java中的弗洛伊德(Floyd)算法

    这篇文章主要介绍了Java中的弗洛伊德(Floyd)算法,Floyd算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与Dijkstra算法类似,需要的朋友可以参考下
    2024-01-01
  • Java 并发编程中如何创建线程

    Java 并发编程中如何创建线程

    这篇文章主要介绍了Java 并发编程中如何创建线程,帮助大家更好的理解和学习使用Java,感兴趣的朋友可以了解下
    2021-03-03

最新评论