Hashmap

Posted by lily's blog on October 17, 2024

equals方法和hashCode方法

常见问题

  1. equals()方法中,如果有两个引用类型的对象,例如两个hashMap,但是里面存储的键值对内容都相等,调用equals()方法返回得到true还是false呢? 在 Java 中,equals() 方法用于比较两个对象的内容是否相等。对于 HashMap 这样的引用类型,如果两个 HashMap 对象存储的键值对内容相等(即它们的键和值都相同),调用 equals() 方法将返回 true。 具体来说,HashMapequals() 方法会比较两个映射的大小以及它们的键值对。如果两个 HashMap 的大小相同,并且所有的键值对都相等,那么 equals() 方法返回 true;否则返回 false

  2. equals方法重写比较的规则是编码者自定义的是吗?也就是说不同的编码规则决定两个引用类型对象是否相等? equals() 方法的重写确实是由编码者自定义的。不同的编码规则可以决定两个引用类型对象是否相等。重写 equals() 方法时,通常需要遵循以下几个基本原则:
    1. 自反性:对于任何非空引用值 xx.equals(x) 应返回 true
    2. 对称性:对于任何非空引用值 xy,如果 x.equals(y) 返回 true,那么 y.equals(x) 也应返回 true
    3. 传递性:对于任何非空引用值 xyz,如果 x.equals(y) 返回 true,且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true
    4. 一致性:如果 xy 的比较条件没有改变,多次调用 x.equals(y) 应返回相同的结果。
    5. null 的处理:对于任何非空引用值 xx.equals(null) 应返回 false。 重写 equals() 方法时,编码者可以根据对象的特定属性来定义相等的条件。例如,对于一个 Person 类,可能会根据 nameage 属性来判断两个 Person 对象是否相等。 因此,编码者的选择确实决定了两个引用类型对象是否被认为相等。
  3. 为什么不重写hashcode可能会导致hashcode返回false? 不重写 hashCode 的后果
    1. 相等对象的哈希码不同
      • 如果 hashCode 方法没有被重写,Java 默认的 hashCode 实现是基于对象的内存地址(hashCode()在Object中是一个native方法,注释上说是对象的内存地址转换的一个值)。这意味着即使两个对象的内容相等(通过 equals 方法判断),它们的哈希码仍然可能不同,因为它们是不同的对象实例。
    2. HashMap 中的存储问题
      • 在 HashMap 中,键的存储是基于哈希码的。如果两个对象的 equals 方法返回 true,但它们的 hashCode 方法返回不同的值,那么它们会被存储在 HashMap 中的不同位置。
      • 这将导致 HashMap 中存在多个“相同”的键(根据 equals 判断),但实际上它们是不同的条目,从而导致数据覆盖和存储不一致的问题。
  4. 为什么头插法会导致循环链表问题? 头插法的工作原理
    1. 插入方式:在头插法中,新元素被插入到链表的头部,原有的链表节点则成为新节点的下一个节点。
    2. 扩容过程:当 HashMap 需要扩容时,所有现有的元素会被重新哈希并放入一个新的、更大的数组中。在扩容的过程中如果要使用头插法继续链接出一个新的链表,是先将所有的元素依次按照头插顺序链接,最后再修改头节点指针指向链好的数组的头节点元素。 死循环的形成
      • 如果在扩容过程中,线程 A 正在将元素插入新的链表,而线程 B 同时插入了元素,且在插入时没有适当的同步控制,那么可能导致链表的指针关系发生混乱。
      • 例如,假设线程 B 在插入 C 时,链表的头指针被修改为 C,而此时线程 A 的链表结构尚未完成。如果线程 A 的插入操作在此时也试图访问链表,可能会导致某些节点的 next 指针指向了前面的节点,形成环形链表。
      • 示例 1:环形链表的形成 ```java
    3. 插入元素: HashMap<Person, String> map = new HashMap<>(2, 0.75f); map.put(new Person(“Alice”), “Person 1”); map.put(new Person(“Bob”), “Person 2”);
    4. 扩容触发: 当插入第三个元素时,负载因子超过阈值,触发扩容。 map.put(new Person(“Charlie”), “Person 3”); // 触发扩容
    5. 多线程操作
      • 线程 A 正在执行扩容操作,将 Alice 和 Bob 移动到新的数组中。
      • 线程 B 同时插入一个新元素 David
    6. 可能的执行顺序
      • 线程 A 开始扩容,并准备将链表中的元素(Alice 和 Bob)插入新的数组。
      • 线程 B 在此时插入 David,由于是头插法,David 被插入到链表的头部。
      • B线程插入结束后A线程才修改链表的头节点指针为Alice
    7. 最终,链表可能变成如下结构(假设环形链表): Alice -> Bob -> David -> (指向 Alice,形成环形) ```
      • 结果
      • 当链表形成环形结构后,任何尝试遍历该链表的操作都将导致死循环,程序将无法正常执行,甚至可能导致内存泄漏或栈溢出。