Java集合中基于hash的容器

本文通过一个具体的例子探讨了HashSet如何保证元素的唯一性,并详细解释了hashCode()和equals()方法在这一过程中的作用。

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

一个奇怪的例子

package indi.demo.container.hash;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    //setter and getter

    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}


public class OddExample {
    public static void main(String[] args) {
        Set<Person> set = new HashSet<Person>();
        set.add(new Person("Jhon", 23));
        set.add(new Person("Jhon", 23));
        set.add(new Person("Li Gang", 24));

        Iterator<Person> it = set.iterator();
        while(it.hasNext()) {
            System.out.println(it.next());
        }
    }
}

程序运行的结果如下:

Person [name=Li Gang, age=24]
Person [name=Jhon, age=23]
Person [name=Jhon, age=23]

刚学Java的时候,这样的代码是信手拈来,很显然这样写是错的,因为Set中是不允许元素重复的,这是集合里面有两个Person对象他们拥有相同的名字和年龄,它们应该被视为同一个对象,那么为什么HashSet没有发现它们是同一对象呢?HashSet到底是如何保证元素的唯一性呢?要弄清楚这个问题,我们必须要去Java源码一探究竟。(很明显,维护元素唯一性的代码肯定实在add方法中),下面是HashSet的部分源码:

private transient HashMap<E,Object> map;

// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
}

可以看到HashSet是用HashMap来实现的,因此我们还得到HashMap的put方法中看看:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

从上面的代码可以看到,在判断两个Key对象是否相同时,进行了两个主要操作:
1. 先比较两个Key的哈希值(用hash函数计算出)是否一样
2. 在上面的基础上再调用其中一个key的equals方法判断两个对象是否相等
因此,我们定义的类要在基于hash的集合中使用时,需要覆写equals()和hashCode()这两个方法,来确保元素的唯一性
所以,上面的类正确的实现方式应该是:

package indi.demo.container.hash;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class CorrectDemo {
    private static class Person {
        private String name;
        private int age;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + age;
            result = prime * result + ((name == null) ? 0 : name.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null || getClass() != obj.getClass()) {
                return false;
            }
            if (this == obj) {
                return true;
            }
            Person other = (Person) obj;
            if (name.equals(other.getName()) && age == other.getAge()) {
                return true;
            }
            return false;
        }

        @Override
        public String toString() {
            return "Person [name=" + name + ", age=" + age + "]";
        }
    }

    public static void main(String[] args) {
        Set<Person> set = new HashSet<Person>();
        set.add(new Person("Jhon", 23));
        set.add(new Person("Jhon", 23));
        set.add(new Person("Li Gang", 24));

        Iterator<Person> it = set.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}

运行结果如下:

Person [name=Jhon, age=23]
Person [name=Li Gang, age=24]

hashCode()和equals()方法的疑问

  1. 为什么需要两个方法才能确定两个对象是否相同?
    确定两个对象是否相同用equals()方法就可以了,这里的hashCode()方法的主要目的不是用来确定对象是否相等的,hash机制是为了提高查询效率,因此这里调用hashCode()方法是为了快速确定元素的位置(或者说,元素应该存放的位置,前提是元素还没有放入到集合中),因为hash函数可能存在冲突(也就是说由两个不同的对象计算出的hash值可能相等),所以hashCode()函数是无法唯一确定一个对象的,必须借助equals()方法来判断两个对象是否相等。
### JavaHashList排序方法及实现方式 在Java中,`HashList` 并不是一个标准的数据结构名称。通常情况下,可能是指 `ArrayList<HashMap<K,V>>` 或者类似的复合数据结构。如果要对这种类型的集合进行排序,则需要明确以下几个方面: 1. **确定排序依据**:是按照 `HashMap` 的 key 还是 value 来排序? 2. **选择合适的工具类**:可以使用 `Collections.sort()` 配合自定义的 Comparator 实现排序[^3]。 3. **Lambda表达式的应用**:通过 Lambda 表达式简化 Comparator 的编写过程[^1]。 以下是针对不同场景的具体实现方式: #### 场景一:按 HashMap 的 Key 排序 当 `HashList` 是指一个包含多个 `HashMap<String, Integer>` 的列表时,可以通过以下代码实现按键排序: ```java import java.util.*; public class Main { public static void main(String[] args) { List<Map<String, Integer>> hashList = new ArrayList<>(); Map<String, Integer> map1 = new HashMap<>(); map1.put("bc", 10); map1.put("ad", 5); Map<String, Integer> map2 = new HashMap<>(); map2.put("cb", 8); map2.put("ad", 7); hashList.add(map1); hashList.add(map2); // 使用流操作提取所有的键并去重后再排序 List<String> sortedKeys = hashList.stream() .flatMap(m -> m.keySet().stream()) .distinct() .sorted() // 默认字典顺序 .toList(); System.out.println(sortedKeys); // 输出 ["ad", "bc", "cb"] } } ``` #### 场景二:按 HashMap 的 Value 排序 对于同样的 `HashList` 数据结构,也可以根据值来进行排序。下面是一个例子展示如何完成此任务: ```java import java.util.*; import java.util.stream.Collectors; public class Main { public static void main(String[] args) { List<Map<String, Integer>> hashList = new ArrayList<>(); Map<String, Integer> map1 = new HashMap<>(); map1.put("bc", 10); map1.put("ad", 5); Map<String, Integer> map2 = new HashMap<>(); map2.put("cb", 8); map2.put("ad", 7); hashList.add(map1); hashList.add(map2); // 提取所有条目并将它们合并到单个列表中以便于后续处理 List<Map.Entry<String, Integer>> entries = hashList.stream() .flatMap(m -> m.entrySet().stream()) // 将每个Map转成Entry Stream再拼接起来 .collect(Collectors.toList()); // 按照Value降序排列这些entries Collections.sort(entries, (entry1, entry2) -> entry2.getValue().compareTo(entry1.getValue())); // 打印结果 for (var entry : entries){ System.out.println(entry.getKey()+":"+entry.getValue()); } /* 可能输出如下(具体取决于输入数据) bc:10 cb:8 ad:7 ad:5 */ } } ``` 上述代码片段展示了两种不同的排序策略——一种基于Key另一种基于Value,并且都利用到了现代Java特性如Stream API 和 lambda expressions来提高可读性和效率[^4][^5]。 ### 注意事项 - 如果存在重复项而希望保留唯一性,在收集阶段就应该考虑去除冗余记录或者调整比较逻辑以适应业务需求。 - 当涉及到复杂对象比如 Student 类型作为元素存在于 list 中时候,记得提供清晰无误的 equals/hashCode 定义以及定制化 comparator 函数[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值