Java面试题精选:Hashcode以及HashMap

  • Java面试题精选,持续更新中......

    提示:Java面试题精选持续更新:Hashcode以及HashMap相关应用

    现在的面试题大多散而乱,本专栏旨在整理精品Java面试题,对不同Java专题进行介绍。

  • 目录

    Hashcode

    Hash的定义

    HashCode是什么

    hash code(hash值|hash码)是什么

    HashCode规范

    HashMap

    HashMap的类继承关系

    put插入数据流程

    流程图(详细)

    内部实现:存储结构-字段

    功能实现-方法

    线程安全性

    面试题精选

    介绍下 HashMap 的底层数据结构

    两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?

    HashMap 的默认初始容量是多少?HashMap 的容量有什么限制吗?

    HashMap 和Hashtable 的区别?

    HashMap 与 ConcurrentHashMap 的异同

    HashMap 中的 key 我们可以使用任何类作为 key 吗?

    HashMap 的长度为什么是 2 的 N 次方呢?

  • Hashcode

    • Hash的定义

      • 散列(哈希)函数:把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出, 该输出就是散列值,是一种压缩映射。或者说一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
    • HashCode是什么

      • HashCode是Object的一个方法,hashCode方法返回一个hash code值,且这个方法是为了更好的支持hash表,比如String、 Set、 HashTable、HashMap等
    • hash code(hash值|hash码)是什么

      • 哈希码是按照某种规则生成的int类型的数值。
      • 哈希码并不是完全唯一的。
      • 让同一个类的对象按照自己不同的特征尽量的有不同的哈希码,但不是说不同的对象哈希码就一定不同,也有相同的情况。
    • HashCode规范

      • 若重写了某个类的equals方法,请一定重写hashCode方法,要能保证通过equals方法判断为true的两个对象,其hashCode方法的返回值相等, 换句话说,就是要保证”两个对象相等,其hashCode一定相同”始终成立;
      • 若equals方法返回false,即两个对象不相等,并不要求这两个对象的hashCode得到不同的数;
      • 改写equals时总是要改写hashCode
  • HashMap

    • HashMap的类继承关系

      • Java为数据结构中的映射定义了一个接口java.util.Map,此接口主要有四个常用的实现类 ,分别是HashMap、Hashtable、LinkedHashMap和TreeMap
        • 类继承关系如下图所示:

        • HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
    • put插入数据流程

      • 流程图(详细)

        • 首先,将要插入的键值对作为参数传递给put方法。
        • HashMap会根据键的哈希码(hashCode)计算出对应的桶(bucket)索引,用于确定键值对在HashMap内部数组中的位置。
        • 如果该桶位置上没有其他键值对,直接将键值对插入到该位置,并增加HashMap的size计数器。
        • 如果该桶位置上已经存在一个或多个键值对,则需要进行链式处理(使用链表或红黑树)。
          • 如果链表长度小于8(JDK 8之前)或6(JDK 8及以后),则将键值对添加到链表的末尾。
          • 如果链表长度达到阈值,而且HashMap的容量达到了一个较大的阈值(一般为数组长度的0.75),则会进行链表转换为红黑树的操作,以提高查找效率。
          • 如果链表已经被转换为红黑树,则会使用红黑树的插入操作来插入键值对。
        • 在插入键值对后,检查HashMap的size是否达到了一个较大的阈值(一般为数组长度的0.75),如果达到,则触发扩容操作。
          • 扩容操作会创建一个新的数组,将原有的键值对重新分布到新的数组中,以减少碰撞。
          • 扩容过程会涉及重新计算键的哈希码,确定新的桶位置,并将键值对插入到新的桶位置上。
        • 插入完成后,put方法结束。
      • 内部实现:存储结构-字段

        • HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的

      • 功能实现-方法

        • 根据key获取哈希桶数组索引位置
        • 分析HashMap的put方法

          • ①判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容
          • ②根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③
          • ③判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals
          • ④判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤
          • ⑤遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可
          • ⑥插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容
        • 扩容机制
          • 扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。
    • 线程安全性

      • 在多线程使用场景中,应该尽量避免使用线程不安全的HashMap,而使用线程安全的ConcurrentHashMap。

面试题精选

  • 介绍下 HashMap 的底层数据结构

    • 我们现在用的都是 JDK 1.8,底层是由“数组+链表+红黑树”组成,而在 JDK 1.8 之前是由“数组+链表”组成。

  • 两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?

    • 不对。hashCode() 和 equals() 之间的关系如下:当有 a.equals(b) == true 时,则 a.hashCode() == b.hashCode() 必然成立,反过来,当 a.hashCode() == b.hashCode() 时,a.equals(b) 不一定为 true。

  • HashMap 的默认初始容量是多少?HashMap 的容量有什么限制吗?

    • 默认初始容量是16。HashMap 的容量必须是2的N次方,HashMap 会根据我们传入的容量计算一个大于等于该容量的最小的2的N次方,例如传 9,容量为16。

  • HashMap 和Hashtable 的区别?

    • HashMap 允许 key 和 value 为 null,Hashtable 不允许。
    • HashMap 的默认初始容量为 16,Hashtable 为 11。
    • HashMap 的扩容为原来的 2 倍,Hashtable 的扩容为原来的 2 倍加 1。
    • HashMap 是非线程安全的,Hashtable是线程安全的。
    • HashMap 的 hash 值重新计算过,Hashtable 直接使用 hashCode。
    • HashMap 去掉了 Hashtable 中的 contains 方法。
    • HashMap 继承自 AbstractMap 类,Hashtable 继承自 Dictionary 类。

  • HashMap 与 ConcurrentHashMap 的异同

    • 都是 key-value 形式的存储数据

    • ConcurrentHashMap 在 JDK 1.8 之前是采用分段锁来现实的 Segment + HashEntry,Segment 数组大小默认是 16,2 的 n 次方;JDK 1.8 之后,采用 Node + CAS + Synchronized
    • HashMap 初始数组大小为 16(默认),当出现扩容的时候,以 0.75 * 数组大小的方式进行扩容;
    • HashMap 底层数据结构是数组 + 链表(JDK 1.8 之前)。JDK 1.8 之后是数组 + 链表 + 红黑树。当链表中元素个数达到 8 的时候,链表的查询速度不如红黑树快,链表会转为红黑树,红黑树查询速度快;
    • HashMap 是线程不安全的,ConcurrentHashMap 是 JUC 下的线程安全的;

  • HashMap 中的 key 我们可以使用任何类作为 key 吗?

  • 平时可能大家使用的最多的就是使用 String 作为 HashMap 的 key,但是现在我们想使用某个自定义类作为 HashMap 的 key,那就需要注意以下几点:
    • 如果类重写了 equals 方法,它也应该重写 hashCode 方法。
    • 类的所有实例需要遵循与 equals 和 hashCode 相关的规则。
    • 如果一个类没有使用 equals,你不应该在 hashCode 中使用它。
    • 自定义 key 类的最佳实践是使之为不可变的,这样,hashCode 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode 和 equals 在未来不会改变,这样就会解决与可变相关的问题了。

  • HashMap 的长度为什么是 2 的 N 次方呢?

  • 为了能让 HashMap 存数据和取数据的效率高,尽可能地减少 hash 值的碰撞,也就是说尽量把数据能均匀的分配,每个链表或者红黑树长度尽量相等。
  • 我们首先可能会想到 % 取模的操作来实现。
  • 下面是回答的重点哟:
    • 取余(%)操作中如果除数是 2 的幂次,则等价于与其除数减一的与(&)操作(也就是说hash % length == hash &(length - 1) 的前提是 length 是 2 的 n 次方)。并且,采用二进制位操作 & ,相对于 % 能够提高运算效率。

猜你喜欢

转载自blog.csdn.net/ikkkp/article/details/130873955