由HashSet实现延伸出去

68 阅读4分钟

HashSet能确保集合中的元素不重复。那看看是否能回答出以下问题:

  1. HashSet底层是如何解决重复的?

  2. HashMap的底层实现原理?

  3. ==与equals的区别?

  4. 重写equals方法的同时为什么要重写hashCode()方法?

  5. HashMap与HashTable的区别?

首先来回答第一个问题。

1. HashSet底层是如何解决重复的?

1. HashSet的实现

  • HashSet底层结构是通过HashMap来实现的。所有存入HashSet的值实际上是作为HashMap的键值,而HashMap的键值实际上一个固定的Object对象,名为PRESENT。

  • 去重原理,HashMap的键是唯一的,因此HashSet依赖HashMap的的机制来保证元素的唯一性。

public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
    private transient HashMap<E,Object> map;
    private static final Object PRESENT = new Object();

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

2. 重复元素的检测过程

  1. 计算Hash值 通过hashCode方法计算元素的哈希值,用于定位到HashMap的某个桶
  2. 定位桶 通过哈希值和数组的长度取模,找到存储该元素的目标桶
  3. 检查是否重复
  • 如果目标桶为空,则直接存入
  • 如果目标桶的已有元素
    • 逐一比较已有元素的哈希值与当前元素的哈希值
      • 如果哈希值不相同,则存入链表或树结构的下一位置
      • 如果哈希值相同,进一步调用equals方法比较
        • 如果equals返回true,说明重复,不存入
        • 如果equals返回false,说明不重复,存入

3. 核心依赖方法

为了保证唯一性,HashSet依赖如下两个方法:

  1. hashCode()
  • 决定元素的Hash值,用于快速定位到某个桶
  • 不同的对象尽量具有不同的Hash值,以减少冲突
  1. equals()
  • 用于比较两个元素是否逻辑上相等
  • 当Hash值相等时,HashSet会用equals用来确认对象是否重复

注意:如果自定义HashSet的元素,需要重写hashCode()和equals()方法,保证逻辑上的一致性:相等的对象必须具有相同的哈希值。

代码实例:

import java.util.HashSet;
import java.util.Objects;

class Person {
    private String name;
    private int age;

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

    @Override
    public int hashCode() {
        // 所有对象都返回相同的哈希值,模拟哈希冲突
        return 1;
    }

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

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

public class HashSetExample {
    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<>();
        set.add(new Person("Alice", 25));
        set.add(new Person("Bob", 30));
        set.add(new Person("Alice", 25)); // 与第一个对象内容相同
        
        System.out.println("Set contents: " + set);
    }
}

2. HashMap的底层实现原理

HashMap底层是使用数组加链表或者红黑树来实现的。数组中的元素称为桶(bucket),每个桶实际上是一个链表。当存在哈希冲突时,会将值存入桶中对应的链表中,当链表长度大于阈值时,会转换成红黑树。

当我们在HashMap中查找元素时,它首先使用键的哈希码来计算出元素所在的桶的位置,然后遍历该桶对应的链表,查找键所对应的值。由于每个桶可能包含多个元素,因此查找的时间复杂度通常为O(1)。

需要注意的是,在添加元素时,如果哈希码相同但键不同,则会在同一桶中创建一个新的链表节点,这被称为哈希冲突。为了减少哈希冲突的数量,HashMap在每个桶上设置了一个阈值,当链表长度超过阈值时,会将链表转换为红黑树,以提高查找效率。

另外,为了保持HashMap的性能,当桶的数量达到一定程度时,HashMap会自动扩容。在扩容过程中,HashMap会重新计算每个元素在新数组中的位置,这个过程是比较耗时的,因此需要在实际使用时留足够的空间以减少扩容的频率。

3. ==与equals的区别?

  • ==比较的是两个对象的内存地址是否相同,如果用于比较基本类型,则比较的值是否相等。

  • equals默认也是比较引用地址是否相同,若类重写了equals方法,则比较两个对象的内容是否相等。常见的如String、Integer类已经重写了equals方法