hashmap应用场景_面试需要了解的HashMap

HashMap是Java中常见的数据结构,常用于面试。本文介绍了HashMap的哈希表概念,包括其四个主要实现类HashMap、Hashtable、LinkedHashMap和TreeMap的特点。HashMap是非线程安全的,而Hashtable则是线程安全的。JDK 1.8中HashMap的优化体现在使用了数组+链表+红黑树的结构,当链表达到一定长度时会转为红黑树。此外,文章还详细阐述了HashMap的存储结构、冲突解决方式、put操作流程以及扩容机制。

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

java面试中,HashMap集合的面试基本是都会问到的。

哈希表(hash table)
也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表。

Map实现类

Java为数据结构中的映射定义了一个接口Map,主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap和TreeMap。

针对这4个实现类做个简单的说明:

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

(2) Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的(方法上使用悲观锁synchronized),任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。

(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

(4) TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会抛出ClassCastException类型的异常。

存储结构:jdk1.7是数组+链表,jdk1.8是数组+链表+红黑树。

0e290dc62e452522d5f3bd23304e8254.png

hashmap底层结构

链表主要为了解决哈希冲突(也叫哈希碰撞)而存在的。主要是因为使用hashmap存储数据的时候,两个不同的元素(键值),通过哈希函数得出的实际存储地址相同。哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap就是采用了链地址法。

JDK 1.8 对 HashMap 进行了比较大的优化,底层实现由之前的 “数组+链表” 改为 “数组+链表+红黑树”,当链表节点较少时仍然是以链表存在,当链表节点较多时(等于8,将会调用内部的treeifyBin方法)转为红黑树。

JDK 1.8数组使用了Node,它实现了Map.Entry接口,本质是就是一个映射(键值对)。

初始化几个重要的参数:

int threshold;             // 所能容纳的key-value对极限 
final float loadFactor;    // 负载因子
int modCount;  //修改的次数,用于fail-fast容错
int size; //数组大小

Node[] table的初始化数组大小(length )默认值是16,负载因子(loadFactor)默认值是0.75,threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * loadFactor。得出负载因子越大,所能容纳的键值对个数就越多。

put描述过程(查看源码更清晰)

1、判断数组是否为空,为空进行初始化,不为空,计算 k 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index。

2、查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中。存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据(onlyIfAbsent为false),如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;

3、如果不是树型节点,创建普通Node加入链表中;判断链表长度是否大于 8,大于的话链表转换为红黑树;

4、插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍

主要代码实现:

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;
    // 1.校验table是否为空或者length等于0,如果是则调用resize方法进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2.通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增一个节点即可
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // table表该索引位置不为空,则进行查找
        Node<K,V> e; K k;
        // 3.判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 4.判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 5.走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数
            for (int binCount = 0; ; ++binCount) {
                // 6.如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 7.校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点,
                    // 减一是因为循环是从p节点的下一个节点开始的
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                // 8.如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;  // 将p指向下一个节点
            }
        }
        // 9.如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); // 用于LinkedHashMap
            return oldValue;
        }
    }
    ++modCount;
    // 10.如果插入节点后节点数超过阈值,则调用resize方法进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);  // 用于LinkedHashMap
    return null;
}

resize扩容机制

两个重要属性:

Capacity:HashMap当前长度。
LoadFactor:负载因子,默认值0.75f。

主要两个步骤:

1、创建一个新的Entry空数组,长度是原数组的2倍。

2、遍历原Entry数组,把所有的Entry重新Hash到新数组。

为啥要重新Hash!!!卧槽这个问题!有点知识盲区呀!有没有很懵逼!直接复制过去不香么

这里我们回想下计算index的公式:index = HashCode(Key) & (Length - 1),原来长度(Length)是8你位运算出来的值是2 ,新的长度是16你位运算出来的值明显不一样了。

代码全部贴出来着实有点恶心,就分几个步骤说了:

一、扩容:如果超过了数组的最大容量,那么就直接将阈值设置为整数最大值,然后如果没有超过,那就扩容为原来的2倍。

if (oldCap > 0) {
      if (oldCap >= MAXIMUM_CAPACITY) {
           threshold = Integer.MAX_VALUE;
           return oldTab;
      }
      else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
          oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; 
}

二、设置阈值:如果阈值(oldThr)已经初始化过了,新阈值(newCap)直接使用旧的阈值。如果没有初始化,那就初始化一个新的数组容量和新的阈值。最后为当前的容量阈值赋值。

//第二部分:设置阈值
else if (oldThr > 0)
      newCap = oldThr;
else {    // 没有初始化阈值那就初始化一个默认的容量和阈值
      newCap = DEFAULT_INITIAL_CAPACITY;
      newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
      float ft = (float)newCap * loadFactor;
      newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
}
//为当前的容量阈值赋值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;

三、复制数组到新数组(这个代码就不贴了,大家有兴趣自己去研究下)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值