List集合去重(常规遍历、Set去重、java8 stream去重、重写equals和hashCode方法)

本文详细介绍了多种Java中去除List重复元素的方法,包括使用Set、Stream以及Java 8的新特性。对于对象去重,强调了重写equals()和hashCode()方法的重要性,并展示了如何根据特定属性进行去重。同时,探讨了equals()和hashCode()方法的实现原理及其在对象比较中的作用。

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

1. 常规元素去重

碰到List去重的问题,除了遍历去重,我们常常想到利用Set集合不允许重复元素的特点,通过List和Set互转,来去掉重复元素。

// 遍历后判断赋给另一个List集合,保持原来顺序
    public static void ridRepeat1(List<String> list) {
        System.out.println("list = [" + list + "]");
        List<String> listNew = new ArrayList<String>();
        for (String str : list) {
            if (!listNew.contains(str)) {
                listNew.add(str);
            }
        }
        System.out.println("listNew = [" + listNew + "]");
    }

    // Set集合去重,保持原来顺序
    public static void ridRepeat2(List<String> list) {
        System.out.println("list = [" + list + "]");
        List<String> listNew = new ArrayList<String>();
        Set set = new HashSet();
        for (String str : list) {
            if (set.add(str)) {
                listNew.add(str);
            }
        }
        System.out.println("listNew = [" + listNew + "]");
    }

    // Set去重     由于Set(HashSet)的无序性,不会保持原来顺序
    public static void ridRepeat3(List<String> list) {
        System.out.println("list = [" + list + "]");
        Set set = new HashSet();
        List<String> listNew = new ArrayList<String>();
        set.addAll(list);
        listNew.addAll(set);
        System.out.println("listNew = [" + listNew + "]");
    }

    // Set通过HashSet去重(将ridRepeat3方法缩减为一行) 无序
    public static void ridRepeat4(List<String> list) {
        System.out.println("list = [" + list + "]");
        List<String> listNew = new ArrayList<String>(new HashSet(list));
        System.out.println("listNew = [" + listNew + "]");
    }

    // Set通过TreeSet去重   会按字典顺序重排序
    public static void ridRepeat5(List<String> list) {
        System.out.println("list = [" + list + "]");
        List<String> listNew = new ArrayList<String>(new TreeSet<String>(list));
        System.out.println("listNew = [" + listNew + "]");
    }

    // Set通过LinkedHashSet去重  保持原来顺序
    public static void ridRepeat6(List<String> list) {
        System.out.println("list = [" + list + "]");
        List<String> listNew = new ArrayList<String>(new LinkedHashSet<String>(list));
        System.out.println("listNew = [" + listNew + "]");
    }

除此之外,可以利用java8的stream来实现去重

//利用java8的stream去重
  List uniqueList = list.stream().distinct().collect(Collectors.toList());
  System.out.println(uniqueList.toString());

上面的方法在List元素为基本数据类型及String类型时是可以的,但是如果List集合元素为对象,却不会奏效

public class ObjectRidRepeat {
    
    public static void main(String[] args) {
        List<User> userList = new ArrayList<User>();
        userList.add(new User("小黄",10));
        userList.add(new User("小红",23));
        userList.add(new User("小黄",78));
        userList.add(new User("小黄",10));
        
        //使用HashSet,无序
        Set<User> userSet = new HashSet<User>();
        userSet.addAll(userList);
        System.out.println(userSet);
        
        //使用LinkedHashSet,有序
        List<User> listNew = new ArrayList<User>(new LinkedHashSet(userList));
        System.out.println(listNew.toString());
    }
}

User类结构如下:

输出如下:(没有去重)

2. 对象去重

 解决对象去重,可以利用for循环遍历的方式进行判断去重,但今天我不准备探究这种方法,要使用的是如下两种:

2.1 使用Java8新特性stream去重

//根据name属性去重
        List<User> unique1 = userList.stream().collect(
                collectingAndThen(
                        toCollection(() -> new TreeSet<>(comparing(User::getName))), ArrayList::new));

        System.out.println(unique1.toString());

        //根据name,age属性去重
        List<User> unique2 = userList.stream().collect(
                collectingAndThen(
                        toCollection(() -> new TreeSet<>(comparing(o -> o.getName() + ";" + o.getAge()))), ArrayList::new)
        );

        System.out.println(unique2.toString());

输出如下:

2.2 对象中重写equals()方法和hashCode()方法

 在User类中重写equals()方法和hashCode()方法:

 

//重写equals方法
 @Override
    public boolean equals(Object obj) {
        User user = (User) obj;
        return name.equals(user.getName()) && (age==user.getAge());
    }

//重写hashCode方法
    @Override
    public int hashCode() {
        String str = name + age;
        return str.hashCode();
    }

当再次执行通过Set去重的方法时,输出如下:

3. equals()方法和hashCode()方法探究

通过最具代表的的String中的equals()方法和hashCode()方法源码来探究两个方法的实现

3.1 equals()方法

/**
     * Compares this string to the specified object.  The result is {@code
     * true} if and only if the argument is not {@code null} and is a {@code
     * String} object that represents the same sequence of characters as this
     * object.
     *
     * @param  anObject
     *         The object to compare this {@code String} against
     *
     * @return  {@code true} if the given object represents a {@code String}
     *          equivalent to this string, {@code false} otherwise
     *
     * @see  #compareTo(String)
     * @see  #equalsIgnoreCase(String)
     */
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

比较两个对象时,首先先去判断两个对象是否具有相同的地址,如果是同一个对象的引用,则直接放回true;如果地址不一样,则证明不是引用同一个对象,接下来就是挨个去比较两个字符串对象的内容是否一致,完全相等返回true,否则false。

3.2 hashCode()方法

/**
     * Returns a hash code for this string. The hash code for a
     * {@code String} object is computed as
     * <blockquote><pre>
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * </pre></blockquote>
     * using {@code int} arithmetic, where {@code s[i]} is the
     * <i>i</i>th character of the string, {@code n} is the length of
     * the string, and {@code ^} indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。

根据《Effective Java》第二版的第九条:覆盖equals时总要覆盖hashCode 中的内容,总结如下:

  • 在程序执行期间,只要equals方法的比较操作用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法必须始终如一地返回同一个整数。
  • 如果两个对象根据equals方法比较是相等的,那么调用两个对象的hashCode方法必须返回相同的整数结果。
  • 如果两个对象根据equals方法比较是不等的,则hashCode方法不一定得返回不同的整数。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。

《Java编程思想》中也有类似总结:

  设计hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该产生同样的值。如果在讲一个对象用put()添加进HashMap时产生一个hashCdoe值,而用get()取出时却产生了另一个hashCode值,那么就无法获取该对象了。所以如果你的hashCode方法依赖于对象中易变的数据,用户就要当心了,因为此数据发生变化时,hashCode()方法就会生成一个不同的散列码。

<think>嗯,用户问的是Java中对List方法有哪些。我得仔细想想,确保涵盖常用的方法,并且解释清楚每种方法的优缺点。首先,我应该回忆一下自己学过的知识,或者工作中用到的方法。 首先,最基础的方法可能就是使用HashSet了吧。因为HashSet本身不允许复元素,所以把List转成HashSet,再转回来,就能。不过这种方法可能会打乱原来的顺序,因为HashSet不保证顺序。不过Java 8以后有LinkedHashSet,可以保持插入顺序,所以如果用户在意顺序的话,应该用LinkedHashSet。这点要记得提到。 然后,Java 8Stream API也是一个常用的方法。用stream的distinct()方法,可以很方便地,而且代码简洁。不过要注意,distinct()保持的是元素的出现顺序,所以对于有序列表来说,这个方法比较合适。不过需要提醒用户,这种方法返回的是一个新的List,原List不会被修改。 第三种方法可能是遍历List,逐个检查元素是否已经存在于一个新List中,不存在的话就加入。这种方法虽然简单,但效率比较低,尤其是当List很大的时候,因为每次都要线性搜索。不过对于小数据量或者不关心性能的情况,可能还是适用的。不过这种方法的时间复杂度是O(),所以需要指出它的缺点。 还有一种方法是利用TreeSet,因为TreeSet可以排序并且。不过同样,这也会改变元素的顺序,或者按照自然顺序排序。如果用户需要自定义排序的话,可以在构造TreeSet时传入Comparator。不过这可能不常用,除非用户同时需要排序。 另外,可能还有其他方法,比如使用Apache Commons Collections里的工具类,或者Guava库中的方法。例如,Guava的Lists.newArrayList(Sets.newHashSet(list)),不过这可能依赖于第三方库,需要用户项目中有引入这些库才行。所以可能需要提到,但说明这是第三方库的方法,如果用户的项目已经用了这些库,可以考虑。 还有一种情况是,如果用户需要保持原List的顺序,除了LinkedHashSet之外,可能还可以用Java 8Stream配合LinkedHashSet的收集器,比如.collect(Collectors.toCollection(LinkedHashSet::new)),然后再转成ArrayList。不过其实直接用Stream的distinct()方法已经保持了顺序,所以可能不需要这么麻烦。不过可能需要比较这两种方法。 另外,可能要考虑Java版本的问题。比如,如果用户还在用Java 7或更早版本,就不能用Stream API,只能用传统的方法,比如HashSet遍历的方式。所以可能需要分情况讨论,或者指出不同方法适用的Java版本。 此外,需要提到每种方法的时间复杂度空间复杂度。比如HashSet方法时间复杂度是O(n),空间也是O(n),而遍历方法时间复杂度是O(),空间O(n)。这样用户可以根据数据量性能需求选择合适的方法。 还有要注意的是,如果List中的元素是自定义对象,必须正确重写equalshashCode方法,否则可能无法正确工作。这一点很要,特别是对于新手来说,他们可能没有意识到这个问题,导致失败。所以需要提醒用户这一点。 总结下来,主要的方法可能有: 1. 使用HashSet(无序)或LinkedHashSet(有序) 2. 使用Java 8Stream.distinct() 3. 遍历并检查新List 4. 使用TreeSet(排序) 5. 第三方库方法 需要逐一解释每种方法的实现步骤,代码示例,以及优缺点。确保用户能够理解并选择合适的方法。 比如,对于HashSet方法,代码可能是: List<String> newList = new ArrayList<>(new HashSet<>(originalList)); 但这样会丢失顺序,所以用LinkedHashSet的话: List<String> newList = new ArrayList<>(new LinkedHashSet<>(originalList)); 对于Stream方法List<String> newList = originalList.stream().distinct().collect(Collectors.toList()); 遍历方法List<String> newList = new ArrayList<>(); for (String item : originalList) { if (!newList.contains(item)) { newList.add(item); } } 这些代码示例需要准确无误,并且用中文注释可能更好,但用户可能希望看到实际的Java代码。所以在回答中要正确写出这些示例,并解释每段代码的作用。 还要注意,如果原List允许包含null值,这些方法是否有效。比如HashSet可以处理null,所以不用担心。但需要确认每种方法是否兼容null的情况。 另外,可能还有其他方法,比如使用Java 8的并行流来,但实际效果可能不如串流,因为distinct()在并行流中的性能可能不佳,所以可能不建议这样做。 最后,总结一下各个方法的适用场景,帮助用户根据具体需求选择最合适的方法。例如,如果保持顺序很要,就用LinkedHashSetStream.distinct();如果需要排序,用TreeSet;如果不想用额外空间,可能只能用遍历方法但效率低;如果追求代码简洁,用Stream API等。</think>Java 中对 `List` 的常用方法如下(附实现代码及优缺点分析): --- ### 方法 1:通过 `HashSet` (**无序**) ```java List<String> originalList = Arrays.asList("A", "B", "A", "C"); List<String> newList = new ArrayList<>(new HashSet<>(originalList)); // 结果可能为 ["A", "B", "C"](顺序不保证) ``` **特点**: - 简单高效,时间复杂度为 $O(n)$。 - 会丢失原始顺序。 - 依赖元素的 `hashCode()` `equals()` 方法。 --- ### 方法 2:通过 `LinkedHashSet` (**保留顺序**) ```java List<String> newList = new ArrayList<>(new LinkedHashSet<>(originalList)); // 结果为原始顺序的首次出现元素,如 ["A", "B", "C"] ``` **特点**: - 保留元素首次出现的顺序。 - 时间复杂度 $O(n)$,空间复杂度略高于 `HashSet`。 --- ### 方法 3:使用 Java 8+ 的 `Stream.distinct()` ```java List<String> newList = originalList.stream() .distinct() .collect(Collectors.toList()); // 保留顺序且代码简洁 ``` **特点**: - 适合 Java 8 及以上版本。 - 保持原始顺序,依赖 `equals()` 方法。 - 返回新集合,原集合不变。 --- ### 方法 4:遍历手动(**低效但兼容性高**) ```java List<String> newList = new ArrayList<>(); for (String item : originalList) { if (!newList.contains(item)) { // 依赖 equals() 方法 newList.add(item); } } // 保留原始顺序,但时间复杂度为 $O(n^2)$ ``` **特点**: - 兼容所有 Java 版本。 - 效率低,仅适用于小规模数据。 --- ### 方法 5:使用 `TreeSet`(**排序**) ```java Set<String> set = new TreeSet<>(Comparator.naturalOrder()); set.addAll(originalList); List<String> newList = new ArrayList<>(set); // 结果为 ["A", "B", "C"](按自然顺序排序) ``` **特点**: - 后按自然顺序(或自定义 `Comparator`)排序。 - 时间复杂度 $O(n \log n)$。 --- ### 方法 6:第三方库(如 Apache Commons 或 Guava) ```java // Guava 示例 List<String> newList = Lists.newArrayList(Sets.newLinkedHashSet(originalList)); ``` **特点**: - 需引入依赖库(如 `Guava`)。 - 实现方式类似原生方法。 --- ### **关键注意事项**: 1. **自定义对象**: - 必须正确重写 `equals()` `hashCode()`,否则逻辑可能失效。 2. **空值处理**: - 上述方法均支持 `null` 值,但需确保集合允许存储 `null`。 3. **性能选择**: - 大数据量优先选择 `HashSet` 或 `Stream.distinct()`,小数据量可手动遍历。 --- ### **总结对比表** | 方法 | 保留顺序 | 时间复杂度 | 空间复杂度 | 适用场景 | |--------------------|----------|------------|------------|------------------------| | `HashSet` | ❌ | $O(n)$ | $O(n)$ | 无需顺序,快速 | | `LinkedHashSet` | ✅ | $O(n)$ | $O(n)$ | 需保留顺序 | | `Stream.distinct()`| ✅ | $O(n)$ | $O(n)$ | Java 8+,代码简洁 | | 遍历 | ✅ | $O(n^2)$ | $O(n)$ | 兼容旧版本,小数据量 | | `TreeSet` | ❌ | $O(n \log n)$ | $O(n)$ | 需排序 | 根据具体需求(如顺序要求、Java 版本、数据规模)选择最合适的方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值