java8 list根据元素的某个字段或者根据元素的多个字段去重(多种方式)

本文介绍两种使用Java8进行去重的方法:一是利用Collectors.toMap结合BinaryOperator函数实现;二是利用Collectors.toCollection和TreeSet,通过Comparator比较元素达到去重目的。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本例子使用的实体类如下:

class Person {
    private String name;
    private int age;
    
    //省略getter/setter/toString
}

第一种去重方式:java8利用Collectors.toMap去重


        List<Person> personList = new ArrayList<>();
        personList.add(new Person("one", 10));
        personList.add(new Person("two", 20));
        personList.add(new Person("three", 30));
        //这两的name值重复
        personList.add(new Person("four", 40));
        personList.add(new Person("four", 45));

        System.out.println("利用Collectors.toMap去重:");
        //利用Collectors.toMap去重
        personList.stream()
                .collect(Collectors.toMap(Person::getName, Function.identity(), (oldValue, newValue) -> oldValue))
                .values()
                .stream()
                .forEach(System.out::println); //打印

      输出如下: 

利用Collectors.toMap去重:
Person{name='four', age=40}
Person{name='one', age=10}
Person{name='three', age=30}
Person{name='two', age=20}

     其中,Collectors.toMap需要使用三个参数的版本,前两个参数一个是keyMapper函数一个是valueMapper函数的,用过toMap的都知道,去重的关键在于第三个参数BinaryOperator函数接口。

       这个BinaryOperator函数接收两个参数,如上面代码,一个oldValue,一个newValue,字面意思,第一个旧值,第二个是新值。当stream构造Map时,会先调用Map的get方法获取该key对应节点的旧值,如果该值为null,则不会调用BinaryOperator函数,如果不为null,则会把获取的旧值与新值作为参数传给函数执行,然后把函数的返回值作为新值put到Map中。如果看不懂,请看源码吧:

第二种去重方式:利用Collectors.toCollection和TreeSet去重:

 List<Person> personList = new ArrayList<>();
        personList.add(new Person("one", 10));
        personList.add(new Person("two", 20));
        personList.add(new Person("three", 30));
        //这两的name值重复
        personList.add(new Person("four", 40));
        personList.add(new Person("four", 42));

        System.out.println("利用Collectors.toCollection和TreeSet去重:");
        //利用TreeSet原理去重(TreeSet内部使用的是TreeMap,使用指定Comparator比较元素,如果元素相同,则新元素代替旧元素)
        personList.stream()
                .collect(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Person::getName))))
                .stream()
                .forEach(System.out::println); //打印

注释中也说了,利用TreeSet原理去重,TreeSet内部使用的是TreeMap,使用指定Comparator比较元素,如果元素相同,则新元素代替旧元素,TreeMap的该部分源码如下图:

如果不想要返回TreeSet类型,那也可以使用Collectors.collectingAndThen转换成ArrayList,也可以用new ArrayList(set),原理一样,如下:

List<Person> distinctList = personList.stream()
                .collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Person::getName))), ArrayList::new));

不是java8咋办?还能咋办?上面的源码都给出了。。copy源码自己封装呗。。利用map和treeSet的原理而已

<think>我们有一个需求:使用JavaStream根据对象的某个字段List进行操作。在JavaStream中,`distinct()`方法默认使用对象的`equals`和`hashCode`方法来判断复。如果我们想根据对象的某个特定字段,就需要自定义逻辑。一种常见的方法是使用`filter`和`ConcurrentHashMap`(或利用HashSet)来实现基于字段。另一种方法是使用`Collectors.collectingAndThen`与`Collectors.toCollection`结合TreeSet或自定义收集器。这里提供几种方法:方法1:使用`filter`和`HashSet`(或`ConcurrentHashMap`)来记录已经出现过的字段值。方法2:使用`Collectors.toMap`或`Collectors.toCollection`来构造一个以该字段为key的Map,然后取Map的values。方法3:使用StreamEx库(第三方)的`distinct`方法,它提供了根据字段的功能。由于问题要求使用JavaStream,并且可能不希望引入第三方库,所以我们主要介绍前两种方法。示例类:```javaclassPerson{privateStringname;privateintage;//构造方法、getter和setter省略}```假设我们有一个`List<Person>`,我们想根据`name`字段。方法1:使用`filter`和`HashSet````javaList<Person>distinctList=list.stream().filter(distinctByKey(Person::getName)).collect(Collectors.toList());//辅助方法publicstatic<T>Predicate<T>distinctByKey(Function<?superT,?>keyExtractor){Set<Object>seen=ConcurrentHashMap.newKeySet();returnt->seen.add(keyExtractor.apply(t));}```这里利用了`ConcurrentHashMap.newKeySet()`创建一个线程安全的Set(如果是在并行流中使用,需要线程安全)。也可以使用`HashSet`,但要注意在并行流中可能会有问题。方法2:使用`Collectors.toMap````javaList<Person>distinctList=list.stream().collect(Collectors.toMap(Person::getName,p->p,(existing,replacement)->existing//如果遇到复的,保留已有的)).values().stream().collect(Collectors.toList());```这种方法会收集成一个Map,然后取value集合。注意,这里我们指定了当遇到复key时,保留已有的值(也可以选择保留新值,根据需求)。方法3:使用`Collectors.collectingAndThen`和`TreeSet`(需要实现Comparable或提供Comparator)```javaList<Person>distinctList=list.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(()->newTreeSet<>(Comparator.comparing(Person::getName))),ArrayList::new));```这种方法利用了TreeSet的特性(根据比较器),但是注意,TreeSet会排序,所以会改变顺序。如果不希望改变顺序,这种方法就不适用。根据需求选择合适的方法。如果希望保持顺序,方法1和方法2(在收集Map时,如果使用HashMap则不能保证顺序,但可以使用LinkedHashMap来保持顺序)可以做到。方法2改进(保持顺序):```javaList<Person>distinctList=list.stream().collect(Collectors.toMap(Person::getName,p->p,(existing,replacement)->existing,LinkedHashMap::new//使用LinkedHashMap以保持插入顺序)).values().stream().collect(Collectors.toList());```总结:推荐使用方法1(使用辅助方法)或方法2(使用LinkedHashMap)来保持顺序并。相关问题:1.在JavaStream中,如何根据多个字段进行?2.使用`distinctByKey`方法在并行流中是否安全?3.使用`toMap`方法时,如果遇到复键,如何选择保留哪一个对象?4.除了,如何同时进行其他操作(如过滤、排序)?</think>### 使用 Java Stream 根据对象字段List Java Stream 中,`distinct()` 默认基于对象的 `equals()` 和 `hashCode()` 方法。若需根据**特定字段**,需自定义逻辑。以下是三种常用方法: --- #### 方法 1:使用 `Collectors.toMap`(推荐) 通过 Map 的键唯一性实现字段: ```java List<Person> distinctList = persons.stream() .collect(Collectors.toMap( Person::getName, // 以姓名字段作为依据 p -> p, // 对象本身作为值 (oldVal, newVal) -> oldVal // 复时保留旧值 )) .values() // 获取后的值集合 .stream() .collect(Collectors.toList()); ``` **特点**: ✅ 保持原始顺序(使用 `LinkedHashMap` 时) ✅ 高效处理大列表 ⚠️ 需显式指定复处理策略(如保留首次出现的数据) --- #### 方法 2:自定义 `filter` + `ConcurrentHashMap` 利用 `ConcurrentHashMap` 记录已出现字段值: ```java List<Person> distinctList = persons.stream() .filter(distinctByKey(Person::getName)) // 字段提取函数 .collect(Collectors.toList()); // 辅助方法 public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) { Set<Object> seen = ConcurrentHashMap.newKeySet(); // 线程安全集合 return t -> seen.add(keyExtractor.apply(t)); // 首次出现返回true } ``` **特点**: ✅ 并行流安全 ✅ 无需收集到中间集合 ✅ 代码复用性强 --- #### 方法 3:`TreeSet` + 自定义比较器 通过 `TreeSet` 的排序特性: ```java List<Person> distinctList = persons.stream() .collect(Collectors.collectingAndThen( Collectors.toCollection( () -> new TreeSet<>(Comparator.comparing(Person::getName)) // 按姓名排序 ), ArrayList::new )); ``` **特点**: ✅ 简洁的一行式操作 ⚠️ 会改变原始顺序(按字段排序) ⚠️ 需注意字段的 `Comparable` 实现 --- ### 关键注意事项 1. **字段选择**:确保作为依据的字段具有唯一性语义(如用户 ID、邮箱等)。 2. **空值处理**:若字段可能为 `null`,需在比较器中处理(如 `Comparator.nullsFirst`)。 3. **性能考量**:大数据集优先选择 `Collectors.toMap` 或 `ConcurrentHashMap` 方案。 4. **对象完整性**:若字段相同但其他属性不同,需明确业务需求(保留首次/最后一次出现)。 > 示例引用:Java Stream 的 `distinct()` 默认基于 `hashCode()` 和 `equals()` ,但无法按字段 [^1]。通过自定义比较器或中间集合可实现字段 [^2]。 --- ### 相关问题 1. 如何根据多个字段组合进行(如姓名+出生日期)? 2. 时如何保留最后一次出现的对象而非首次? 3. 并行流处理时有哪些线程安全问题需要注意? 4. 除了 Stream,还有哪些高效的 Java 集合方法?
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值