关于HashMap的所有

一篇搞懂HashMap,手写HashMap 手撕HashMap 阅读详情

HashMap

HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

1.HashMap的数据结构

(1)JDK 8 之前

将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可

在这里插入图片描述

(2)JDK 8 之后

相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。

在这里插入图片描述

====================JDK8 HashMap底层结构=============================
// HashMap中的Node数组
transient Node<K,V>[] table;

// HashMap中的静态内部类Node节点,组成单向链表
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
// 省略部分代码
}

// HashMap中的TreeNode节点,构成树模型
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
// 省略部分代码
}

JDK1.7 VS JDK1.8 比较

JDK1.8主要解决或优化了一下问题:

  1. resize 扩容优化
  2. 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
  3. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
不同JDK 1.7JDK 1.8
存储结构数组 + 链表数组 + 链表 + 红黑树
初始化方式单独函数:inflateTable()直接集成到了扩容函数resize()中
hash值计算方式扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
存放数据的规则无冲突时,存放数组;冲突时,存放链表无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
插入数据方式头插法(先讲原位置的数据移到后1位,再插入数据到该位置)尾插法(直接插入到链表尾部/红黑树)
扩容后存储位置的计算方式全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1))按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)

2.底层原理

  • 当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
  • 存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
  • 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
  • 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。

(1)HashMap 各常量、成员变量作用

//创建 HashMap 时未指定初始容量情况下的默认容量   
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

//HashMap 的最大容量     
	static final int MAXIMUM_CAPACITY = 1 << 30;

//HashMap 默认的装载因子,当 HashMap 中元素数量超过 容量*装载因子 时,进行 resize() 操作
	static final float DEFAULT_LOAD_FACTOR = 0.75f;

//用来确定何时将解决 hash 冲突的链表转变为红黑树
	static final int TREEIFY_THRESHOLD = 8;

// 用来确定何时将解决 hash 冲突的红黑树转变为链表
	static final int UNTREEIFY_THRESHOLD = 6;

/* 当需要将解决 hash 冲突的链表转变为红黑树时,需要判断下此时数组容量,若是由于数组容量太小(小于 MIN_TREEIFY_CAPACITY )导致的 hash 冲突太多,则不进行链表转变为红黑树操作,转为利用 resize() 函数对 hashMap 扩容 */
	static final int MIN_TREEIFY_CAPACITY = 64;
//保存Node<K,V>节点的数组
	transient Node<K,V>[] table;

//由 hashMap 中 Node<K,V> 节点构成的 set
	transient Set<Map.Entry<K,V>> entrySet;

//记录 hashMap 当前存储的元素的数量
    transient int size;

//记录 hashMap 发生结构性变化的次数(注意 value 的覆盖不属于结构性变化)
	transient int modCount;
 
//threshold的值应等于 table.length * loadFactor, size 超过这个值时进行 resize()扩容
	int threshold;

//记录 hashMap 装载因子
	final float loadFactor;

(2)HashMap的构造器

====================空参构造器============================
public HashMap() {
    // 所有参数均采用默认值,threshold=0
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
====================带参构造器============================
public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
====================带参构造器============================
public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
    //注意此种方法创建的 hashMap 初始容量的值存在 threshold 中
        this.threshold = tableSizeFor(initialCapacity);
}
 // 该方法方法返回的值是最接近 initialCapacity 的2的幂作为threshold
static final int tableSizeFor(int cap) {
        int n = cap - 1;
    // >>> 代表无符号右移
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

(3)先从put说起

不同于JDK 8 之前,HashMap在实例化的时候并没有在底层初始化map,只有在第一次put元素时,才会在底层初始化一个容量为16的map

当我们put的时候,首先计算 key的hash值,这里调用了 hash方法,hash方法实际是让key.hashCode()与key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length - 1) & hash,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。

流程图

在这里插入图片描述

源码分析

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.判断是否数组是否为空
        if ((tab = table) == null || (n = tab.length) == 0)
            // 首次添加,初始化tab为容量为16的map成功,n=16
            n = (tab = resize()).length; 
    // 2.计算index,判断数组的该位置是否为null
 //(n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
    // 3.数组的该index处已经有元素
        else {
            Node<K,V> e; K k;
          // 3.1 只有一个结点
            // p就是该index上的node结点,比较其hash值是否与待添加的node结点的hash相同
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 将第一个元素赋值给e,用e来记录
                e = p;
          // 3.2 存在树结构
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
          // 3.3 存在链表结构
            else {
                for (int binCount = 0; ; ++binCount) {
                  // 3.3.1 是否到达链表尾部
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 链表长度是否超过规定的阈值8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 当tab.length >=64(MIN_TREEIFY_CAPACITY)时,才转换为树结构
                            // 否则进行对数组进行扩容
                            treeifyBin(tab, hash);
                        break;
                    }
                  // 3.3.2 判断链表中的每个结点是否与待插入结点相同
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    // 后移
                    p = e;
                }
            }
          //判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回之前的value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
    // 记录 hashMap 发生结构性变化的次数(注意 value 的覆盖不属于结构性变化)
        ++modCount;
    // 实际大小大于阈值则扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
  • 判断键值对数组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,如果超过,进行扩容。

(4)再说resize

在putval()中,我们曾调用过两次resize()方法

  1. 在初始化时会对其进行扩容

  2. 当该数组的size大于其threshold值(第一次为12),而在JDK 7 中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发;在JDK 8 中,扩容的同时也会伴随的原数组上面的元素进行重新分配。具体是根据在数组的某个结点的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

注意点:

  • 在JDK 8 中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;

  • 每次扩展的时候,都是扩展2倍;

  • 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。

源码分析

=================当数组的size超过阈值时=================
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    // oldCap : 默认容量为16
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // oldThr:16 * 0.75 = 12
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 1.如果oldCap大于0的话,说明hash桶数组不为空
    if (oldCap > 0) {
        // 1.1如果大于最大容量了,就赋值为整数最大的阀值
        	// MAXIMUM_CAPACITY:1073741824
        if (oldCap >= MAXIMUM_CAPACITY) {
            // MAX_VALUE:2147483647
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 1.2如果当前数组的容量在扩容2倍后仍然小于最大容量 并且oldCap大于默认值16
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // threshold扩大为原来的2倍,24
            newThr = oldThr << 1; // double threshold
    }
    // 2.当oldCap为0时,但threshold大于0,代表有参构造有cap传入,threshold已经被初始化成最小2的n次幂
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 3.当用无参构造创建map时,给出默认容量和threshold 16, 16*0.75
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 新的threshold = 新的cap * 0.75
    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;
    if (oldTab != null) {
        // 遍历原来的数组
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 1.当该位置的结点为单个结点时(非链表),重新计算该节点在新数组中的位置,并加入
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 2.当该位置的结点为红黑树结构时,并且e.next!=null,那么处理树中元素的重排
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 3.当该位置结点为链表结构时,并且e.next!=null,那么处理链表中元素重排
                else { // preserve order
                    // loHead,loTail 代表扩容后不用变换下标
                    Node<K,V> loHead = null, loTail = null;
                    // hiHead,hiTail 代表扩容后变换下标
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                // 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead
                // 代表下标保持不变的链表的头元素
                                loHead = e;
                            else
				// loTail.next指向当前e
                                loTail.next = e;
                // loTail指向当前的元素e
                // 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素时,
                // 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next.....
                // 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
				// 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
               	// 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

(5)关于get的分析

实际上是根据输入节点的 hash 值和 key 值利用getNode 方法进行查找

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

=========================getNode()方法====================================
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // tab不为空,且根据hash值计算出的索引位置不为null时
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
        	// 判断该索引位置的结点是否为要输入结点
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 判断first.next 是否为null,也就是判断是否为树结构或者链表结构
            if ((e = first.next) != null) {
                // 当该结点为树结构时
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 当为链表结构时,进行遍历查找
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

(6)谈谈红黑树

final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                       int h, K k, V v) {
            Class<?> kc = null;
            boolean searched = false;
            TreeNode<K,V> root = (parent != null) ? root() : this;
    //从根节点开始查找合适的插入位置(与二叉搜索树查找过程相同)
            for (TreeNode<K,V> p = root;;) {
                int dir, ph; K pk;
                if ((ph = p.hash) > h)
                    // dir < 0,查找当前结点的左结点
                    dir = -1;
                else if (ph < h)
                    // dir > 0,查找当前结点的右结点
                    dir = 1;
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    //进入这个else if 代表 hash 值相同,key 相同
                    return p;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    // 进入这个else if代表:
                    // 1.当前结点与待插入结点hash相同,key不同
                    // 2.k是不可比较的,即k并未实现 comparable<K> 接口
                    if (!searched) {
                        //在以当前节点为根的整个树上搜索是否存在待插入节点(只会搜索一次)
                        TreeNode<K,V> q, ch;
                        searched = true;
                        if (((ch = p.left) != null &&
                             (q = ch.find(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.find(h, k, kc)) != null))
                            //若树中存在待插入节点,直接返回
                            return q;
                    }
                    dir = tieBreakOrder(k, pk);
                }

                TreeNode<K,V> xp = p;
                // 找到了待插入的位置,xp 为待插入节点的父节点
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    Node<K,V> xpn = xp.next;
                    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    xp.next = x;
                    x.parent = x.prev = xp;
                    if (xpn != null)
                        ((TreeNode<K,V>)xpn).prev = x;
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }
        }

3.解决哈希冲突

Hash,一般翻译为“散列”,就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值),简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数

哈希冲突:当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)

(1)HashMap的数据结构

在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突

**链地址法:**将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下

(2)HashMap中的hash()

这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动)

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // 与自己右移16位进行异或运算(高低位异或)
}

(3)红黑树

判断链表长度是否大于8,并且数组容量大于64的话把链表转换为红黑树

我们的HashMap中存在大量数据时,加入我们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn);

总结:

1. 使用链地址法(使用散列表)来链接拥有相同hash值的数据;
2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;

4.需要注意的地方

(1)关于HashMap的key

使用任何类都可以作为 Map 的 key,必须重写hashCode()和equals()方法

  • 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;

  • 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性

HashMap中String、Integer这样的包装类最适合作为Key

String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率

  • 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
  • 内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况

(2)HashMap非线程安全

HashTable线程安全

  • 线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);

  • 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;

  • 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。

  • **初始容量大小和每次扩充容量大小的不同 **: ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小。

  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 采用 数组+链表 的形式。

ConcurrentHashMap线程安全

  • 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树;
  • 实现线程安全的方式:① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。

==推荐使用:==在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。

RH850从0搭建Autosar开发环境【17】- Davinci Configurator之DCM模块配置详解(下) 阅读详情

相关推荐

FDTD Script命令学习-farfield3d/farfieldux/farfielduy

介绍了farfield3d/farfieldux/farfielduy命令功能,及不同应用语法和参数介绍,以及对官网示例进行介绍。

jxxl_1314的博客 3098

HashMap全解

疫情肆虐,时间空余,将HashMap源码看了看,加上之前的知识,总结了一篇针对hashMap的常见问题,放在这里分享,如理解有误,请留言指正。

weixin_44236420的博客 755

pytorch转onnx转paddlepaddle

1.pt转onnx 可查看我之前的文章:https://blog.youkuaiyun.com/oyjwin/article/details/117778583?spm=1001.2014.3001.5501

oyjwin的博客 1461

Java HashMap底层实现和原理分析(四)

文章目录JDK1.8 HashMap介绍JDK1.8 HashMap源码分析成员变量构造方法`put(K key, V value)`方法`hash(Object key)`方法`putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)`方法`treeifyBin(NodeJDK1.8 HashMap介绍 在JDK1.8 之前 HashMap 由 数组+链表 数据结构组成的。 在JDK1.8 之后 HashMap 由 数组+

C. 1228

蓝桥杯单片机省一经验分享1:基础篇.含详细代码与应试技巧

含详细代码、驱动修改与应试技巧

qq_53529450的博客 1万+

Windows 10下用vcpkg一键搞定libcurl开发环境(Code::Blocks 20.03配置指南)

本文详细介绍了在Windows 10系统下使用vcpkg工具快速配置libcurl开发环境的完整指南,特别针对Code::Blocks 20.03 IDE进行优化设置。通过vcpkg自动化管理依赖项,简化libcurl的安装与配置过程,帮助开发者高效搭建网络通信功能开发环境,并提供了常见问题解决方案和实际项目中的最佳实践。

weixin_28040125的博客 250

迭代HashMap里的所有元素

private Map attributes = new HashMap(); //变量定义。 假设已经给该变量赋了N对值 那么接下来可以按下面的方式迭代: Iterator> it =this.attributes.entrySet().iterator(); while(it.hasNext()) { Map.Entry me = it.next();

Java高手真经 979

【Codex安装使用手册】

Codex安装部署使用教程

qq_43392230的博客 4976

遍历HashMap中元素的三种方法

遍历HashMap中的元素一共有三种方式: 1、以Map.Entry 的方式 2、通过map.keySet() 的方式 3、通过map.getValues() 的方式 以下是自己的学习总结代码,希望对大家有用! package demo; import java.util.Collection; import java.util.HashMap; import java.util.I...

相由心生的博客 5689

LVS 负载均衡面试题及参考答案

什么是 LVS 负载均衡?它的主要作用是什么? LVS(Linux Virtual Server)是一个基于 Linux 内核的开源负载均衡项目。它通过将请求流量按照一定的算法分发到后端的多个真实服务器(Real Server)上,从而实现负载均衡的功能。 从原理上来说,LVS 工作在网络层和传输层之间。当客户端发起请求时,请求首先到达 LVS 服务器,LVS 会根据预先设定的规则和算法,比如轮询(Round - Robin)、加权轮询(Weighted Round - Robin)、最少连

大模型大数据攻城狮的专栏 1469

LangChain4j + MCP:从工具协议到生产级 Agent

这两年,很多团队都已经把「大模型问答」接入到系统里了:前端一个输入框,后端调一下 OpenAI / DeepSeek / 本地模型,返回一段回答——这类 「简单问答型」AI 功能它只能说,不会做:不会主动查数据库,不会调用你的接口,不会帮你真正执行脚本;面对复杂一点的需求,比如「帮我生成这个报表的 SQL 并执行一下」,普通 ChatGPT 式的问答就开始力不从心了。而Agent 智能体想解决的,就是这件事:不只是回答你「怎么做」,而是能自己思考、自己决定要不要用工具、再用工具把事做完。

OFFTime_we的博客 1152

vuex的基本属性以及使用方法详解

意义:实现组件之间的数据共享 vuex中的属性: 1.state提供唯一的数据源,获取的方法,this.$store.state.索要获取的数据。 第二中获取state中的数据的方法,导入{mapState}映射到computed中,使用展开运算符。 2.mutation;作用是用于变更store中的数据,通过这种方式,可以集中监控store中的数据变化,方便后期中维护,mutation中写的方法,moutation(state){},第一个参数永远代表state数据,外部调用mutation中函数的

zyxczyf123的博客 597

【Mode】FDE 求解器工作流程

Lumerical---FDE(Finite Difference Eigenmode) Solver

u011699626的博客 4942

什么是HashMap?什么情况下使用HashMap?

这里是修真院后端小课堂,每篇分享文从 【背景介绍】【知识剖析】【常见问题】【解决方案】【编码实战】【扩展思考】【更多讨论】【参考文献】 八个方面深度解析后端知识/技能,本篇分享的是: 【什么是HashMap?什么情况下使用HashMap?】 大家好,我是IT修真院深圳分院第十一期学员,一枚正直纯洁善良的JAVA程序员。 今天给大家分享一下,修真院官网JAVA任务十的一个知识点:什么是...

IT修真院:初学者转行到互联网的聚集地 7636

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

HashMap相关面试题(哈希表、HashMap的实现原理、HashMap的put方法的具体流程、HashMap的扩容机制、HashMap的寻址算法)

HashMap相关面试题(哈希表、HashMap的实现原理、HashMap的put方法的具体流程、HashMap的扩容机制、HashMap的寻址算法)

聂可以的博客 2245

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

【HashMap】

HashMap简单概括

weixin_52553065的博客 2087

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

详解 HashMap扩容机制,HashMap的底层原理以及HashSet在底层原理

利用HashCode方法和equals方法HashCode方法来计算出哈希值,根据哈希值计算机出要元素要存入的位置,然后再调用equals方法比较对象内部的属性值是否一样利用什么机制保证去重?**利用HashCode方法和equals方法HashCode方法来计算出哈希值,根据哈希值计算机出要元素要存入的位置,然后再调用equals方法比较对象内部的属性值是否一样。

Java技术加油站的博客 2112

HashMap详解

本文主要介绍了Java集合框架中HashMap的相关知识,包括源码分析、hash 原理、扩容机制、加载因子、线程不安全等内容。

专注Java和MySQL领域知识分享 1685

HashMap深入讲解

摘要: HashMap是Java中基于哈希表的Map实现类,允许null键值且不保证顺序。其底层采用数组+链表/红黑树结构,默认容量16,负载因子0.75(平衡空间与时间效率)。HashSet实际通过HashMap实现。关键特性包括:key需重写equals()和hashCode(),value需重写equals();当链表长度>8且数组容量≥64时转为红黑树。put操作先计算key的hash值(通过hashCode()高16位异或),再处理哈希冲突。构造方法可自定义初始容量和负载因子。

1356

JAVA之HashMap详解

HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全的。它的 key、value 都可以为 null,此外,HashMap 中的映射不是有序的。jdk1.8 之前 HashMap 由数组 + 链表组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突(两个对象调用的 hashCode 方法计算的哈希值经哈希函数算出来的地址被别的元素占用)而存在的(“拉链法”解决冲突)。

励志成为一名优秀的全能型软件开发工程师。 2328

面试手写HashMap,手撕HashMap

前言: 现在面试时,面试官经常会问到HashMap,简单点就会问下HashMap的一些关键知识点,困难些的可能会当场让你手写一个HashMap,考察下你对HashMap底层原理的了解深度;所以,今天特别手写了一个简单的HashMap,只实现了 put、get、containsKey、keySet 方法的 HashMap,来帮助我们理解HashMap的底层设计原理。 本文...

木子雷的博客 2220

HashMap原理

HashMap是基于哈希表的键值对存储结构,采用数组+链表+红黑树解决哈希冲突。核心通过键的hashCode计算哈希值,使用(n-1)&hash确定数组索引位置。默认容量16,负载因子0.75,超过阈值会扩容2倍。Java8优化了哈希扰动函数,将高16位与低16位异或,使分布更均匀。当链表长度超过8且数组容量≥64时,链表转为红黑树;节点数低于6时转回链表。put方法先检查桶是否为空,为空则直接插入;否则处理冲突,匹配则更新,不匹配则链式存储或树化。

猫狗鼠鱼的储物间 1096

HashMap 详解

本文深入解析Java中HashMap的核心特性、底层数据结构及哈希冲突处理机制,涵盖自定义类型作为key的处理、手写HashMap实现及Java8后的优化,通过源码分析揭示哈希表扩容与性能优化策略,助你掌握高效数据管理与操作技巧。

m0_73941339的博客 1423

HashMap

如果这和个位置上有多个元素,还需要用key的equals方法在对应位置的链表中找到需要的元素,通常equals方法一般都会根据实际的业务内容来定义。这里适用位运算,所以上面的capacity要是2的次方,因为2的次方减1得到一个全1的二进制数,做and操作就会保证索引的均匀性,否则加入某一位是0,那么这一位就永远不会在索引中出现。如果key为null,会存放在索引为0的entry,但是这个entry不一定只有key为null的value,所以需要遍历所有的entry,返回key为null的value。

weixin_43991255的博客 862

Java HashMap

HashMap 的 key 与 value 类型可以相同也可以不同,可以是字符串(String)类型的 key 和 value,也可以是整型(Integer)的 key 和字符串(String)类型的 value。如果你只想获取 key,可以使用 keySet() 方法,然后可以通过 get(key) 获取对应的 value,如果你只想获取 value,可以使用 values() 方法。对 hashMap 中指定 key 的值进行重新计算,如果不存在这个 key,则添加到 hashMap 中。

2401_88984449的博客 1101

HashMap、创建并遍历HashMap集合、LinkedHashMap

HashMap是基于哈希表的Map实现。键是哈希表结构,可以保证键的唯一性。 HashMap 继承于AbstractMap,实现了Map接口。 HashMap的设计初衷主要是为了解决键值(key-value)对应的关联的,HashMap的优势是可以很快的根据键(key)找到该键对应的值(value)。注意一下,HashMap是一种无序的存储结构。 遍历 1.案例1 键String类型,值S...

元气满满晨 1万+
上一篇: Redis基础入门及实战案例
下一篇: chapter 1 操作系统基本概念
小李同学呦
博客等级 码龄6年 4粉丝 21原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值