HashMap 的产生与原理

简介: 数组:一片物理上连续的大小确定的储存空间。好处:根据下标快速的查找和修改里面的内容。缺点:大小确定,无法修改。添加新的元素或者删除元素比较麻烦。

一、HashMap的诞生

1.1 数组

数组:一片物理上连续的大小确定的储存空间。

好处:根据下标快速的查找和修改里面的内容。

缺点:大小确定,无法修改。添加新的元素或者删除元素比较麻烦。
image.png

数组的静态初始化

//数组实现方式一:

    //数据类型 数组名称[] = {值, 值,…}

    String str[] = {"移动端","Android","iOS"};

    System.out.println(str.length);//三个元素

    for (int i = 0; i <str.length ; i++) {

        System.out.println(str[i]);

    }

    //数组实现方式二:

    //数据类型 数组名称[] = new 数据类型[] {值, 值,…}

    String str2[] =  new String[3];

    str2[0] = "移动端";

    str2[1] = "Android";

    str2[2] = "iOS";

    System.out.println(str2.length);//三个元素

    for (int i = 0; i <str2.length ; i++) {

        System.out.println(str2[i]);

    }

运行结果是一样的。

"E:\Android\Android StudioO\jre\bin\java.exe"...

3

移动端

Android

iOS
如果我想再添加两个新元素(Java、Kotlin),那么使用数组就不合适了,这时候顺序表就出现了。

1.2 顺序表

顺序表:以数组的形式保存的线性表,物理上连续、逻辑上连续、大小可以动态增加。(如:ArrayList)

顺序表用的频率远远高于数据。用肯定都会用,咱们看看源码。

public class ArrayList extends AbstractList

    implements List<E>, RandomAccess, Cloneable, java.io.Serializable

{

transient Object[] elementData;//存储 ArrayList 元素的数组缓冲区。

private int size;//ArrayList 的大小

public ArrayList() {

    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;

}    

public E get(int index) {

    if (index >= size)

        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));



    return (E) elementData[index];

}



public E set(int index, E element) {

    if (index >= size)

        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));



    E oldValue = (E) elementData[index];

    elementData[index] = element;

    return oldValue;

}

public boolean add(E e) {

    ensureCapacityInternal(size + 1);  // Increments modCount!!

    elementData[size++] = e;

    return true;

}



public void add(int index, E element) {

    if (index > size || index < 0)

        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));



    ensureCapacityInternal(size + 1);  // Increments modCount!!

    System.arraycopy(elementData, index, elementData, index + 1,

                     size - index);

    elementData[index] = element;

    size++;

}



public void clear() {

    modCount++;



    // clear to let GC do its work

    for (int i = 0; i < size; i++)

        elementData[i] = null;



    size = 0;

}

}
然后你会发现还是用数组进行存储的,只不过把对数组的操作处理了,而不需要我们自己处理。

原来的数组
image.png

新增数组
image.png

而顺序表的新增和删除元素都需要大量移动元素等操作,此时链表就产生了。

1.3 链表

链表:链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针连接次序实现的。

image.png

新增

这里将元素1的指针由指向元素2改为指向新增元素4,然后再将元素4的指针指向元素2即可。

image.png

删除元素

将元素1的指针直接指向元素3即可。
image.png

1.4 ArrayList 和 LinkedList 对比

ArrayList(顺序表):

优点:查找快

缺点:增删慢

LinkedList(链表)

优点:增删快

缺点:查找慢

那么问题来了在什么情况下使用 ArrayList?在什么情况下使用 LinkedList? 为什么?

那么可不可以将 顺序表和链表的优点结合起来呢?-Hash表

1.5 Hash表

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

哈希表其实本质上就是一个数组。可以根据一个key值来直接访问数据,查找速度快。

image.png

二、HashMap

HashMap采用Node(implements Map.Entry)数组来存储key-value对,每一个键值对组成了一个Node实体,Node类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Node实体。

注意:jdk1.8 HashMap中的Entry不见了,取而代之的是Node,但是Node也实现了Map.Entry接口,和之前的Entry类似。

Node类

static class Node<K,V> implements Map.Entry<K,V> {

    final int hash;

    final K key;

    V value;

    Node<K,V> next;



    Node(int hash, K key, V value, Node<K,V> next) {

        this.hash = hash;

        this.key = key;

        this.value = value;

        this.next = next;

    }



    public final K getKey()        { return key; }

    public final V getValue()      { return value; }

    public final String toString() { return key + "=" + value; }



    public final int hashCode() {

        return Objects.hashCode(key) ^ Objects.hashCode(value);

    }



    public final V setValue(V newValue) {

        V oldValue = value;

        value = newValue;

        return oldValue;

    }



    public final boolean equals(Object o) {

        ...

    }

}

从上面看出 hash、key、value 代表本节点数据,next表示指向下一个节点的数据。

JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。

JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8) 时,将链表转换为红黑树,这样大大减少了查找时间。

image.png

2.0 小试牛刀
class HashMapTest {

public static void main(String[] args) {

    //创建HashMap

    HashMap<String,String> hashMap = new HashMap<>();

    //添加数据(key-value)

    hashMap.put("name","帅次");

    hashMap.put("age","20");

    hashMap.put("subject","Android");

    hashMap.put(null,"空Key");

    System.out.println("HashMap1:"+hashMap.toString());//{null=空Key, subject=Android, name=帅次, age=20}

    //添加数据(覆盖已有key)

    hashMap.put("age","26");

    System.out.println("HashMap2:"+hashMap.toString());//{null=空Key, subject=Android, name=帅次, age=26}

    //根据key,获取value

    System.out.println("Key-null:"+hashMap.get(null));//空Key

    System.out.println("Key-subject:"+hashMap.get("subject"));//Android



    //根据key删除元素

    System.out.println("删除subject对应:"+hashMap.remove("subject"));//删除subject对应:Android

    //根据key-value删除元素

    System.out.println("删除age:"+hashMap.remove("age","20"));//删除age对应20:false

    System.out.println(hashMap.toString());//{null=空Key, name=帅次, age=26}

    System.out.println("删除age:"+hashMap.remove("age","26"));//删除age对应20:true

    System.out.println("HashMap3:"+hashMap.toString());//{null=空Key, name=帅次}



    //方法一:1、获得key-value的Set集合,2、循环(推荐)

    Set<Map.Entry<String,String>> entrySet= hashMap.entrySet();

    for (Map.Entry<String, String> strEntry : entrySet) {

        System.out.println("遍历Set集合:"+strEntry.getKey()+"-"+strEntry.getValue());

    }

    //方法二:1、获取key的Set集合,2、循环

    Set<String> strKeySet= hashMap.keySet();

    for (String s : strKeySet) {

        //先获取key,通过key获取value

        System.out.println("遍历KeySet集合:"+s+"-"+hashMap.get(s));

    }

    //方法三:1、获得key-value的Set集合,2、获取iterator 3、循环

    Set<Map.Entry<String,String>> itEntrySet= hashMap.entrySet();

    Iterator iterator = itEntrySet.iterator();

    while (iterator.hasNext()){

        Map.Entry map= (Map.Entry)iterator.next();

        System.out.println("遍历Iterator:"+map.getKey()+"-"+map.getValue());

    }

    //其他还有不少方法可以遍历数据

}

}
运行结果

"E:\Android...."

HashMap1:{null=空Key, subject=Android, name=帅次, age=20}

HashMap2:{null=空Key, subject=Android, name=帅次, age=26}

Key-null:空Key

Key-subject:Android

删除subject对应:Android

删除age:false

{null=空Key, name=帅次, age=26}

删除age:true

HashMap3:{null=空Key, name=帅次}

遍历Set集合:null-空Key

遍历Set集合:name-帅次

遍历KeySet集合:null-空Key

遍历KeySet集合:name-帅次

遍历Iterator:null-空Key

遍历Iterator:name-帅次

Process finished with exit code 0

2.1 构造方法

/**

 * 默认加载因子值
 */

static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 哈希表的加载因子。
 */

final float loadFactor;



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;

    this.threshold = tableSizeFor(initialCapacity);

}

public HashMap(int initialCapacity) {

    this(initialCapacity, DEFAULT_LOAD_FACTOR);

}

public HashMap(Map<? extends K, ? extends V> m) {

    this.loadFactor = DEFAULT_LOAD_FACTOR;

    putMapEntries(m, false);

}

/**
 * 构造一个具有默认加载因子 (0.75) 的空 HashMap。
 */

public HashMap() {

    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted

}

HashMap有4个构造方法,主要初始化了三个参数:

initialCapacity:初始容量(默认16)。

loadFactor 加载因子(默认0.75)。

threshold 阈值:hashMap所能容纳的最大价值对数量,如果超过则需要扩容,计算方式:threshold=initialCapacity*loadFactor。

加载因子:在默认情况下,数组大小为16,那么当HashMap中元素个数超过 160.75=12 的时候,就把数组的大小扩展为 162=32 ,即扩大一倍,然后重新计算每个元素在数组中的位置。当然这个值是可以自己设定的,但是不推荐修改这个值。

咱们比较常用的还是无参构造方法。HashMap 创建好了,那么咱们开始存储数据吧。

2.2 put()

添加数据:

key可以为null

key唯一,如果key存在则覆盖value内容

public V put(K key, V value) {

    return putVal(hash(key), key, value, false, true);

}

这里在调用 putVal() 方法前根据key进行了hash计算。

image.png

2.2.1 hash(key)

/**
  * 计算 key.hashCode() 并将散列的高位散列(XOR)到低位。由于该表使用二次幂掩码,因此仅在当前掩码之上位变化的散列集将始终发生冲突。
  * 使用树来处理 bin 中的大量冲突,我们只是以最便宜的方式对一些移位的位进行异或,以减少系统损失,以及合并最高位的影响,否则由于表边界,这些最高位将永远不会用于索引计算(摘自源码)。
*/

static final int hash(Object key) {

    int h;

    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

public int hashCode() {

    return identityHashCode(this);

}



static int identityHashCode(Object obj) {

    int lockWord = obj.shadow$_monitor_;

    final int lockWordStateMask = 0xC0000000;  // Top 2 bits.

    final int lockWordStateHash = 0x80000000;  // Top 2 bits are value 2 (kStateHash).

    final int lockWordHashMask = 0x0FFFFFFF;  // Low 28 bits.

    if ((lockWord & lockWordStateMask) == lockWordStateHash) {

        return lockWord & lockWordHashMask;

    }

    return identityHashCodeNative(obj);

}

当 key == null 时返回0,也就是说 key 可以设置为 null。

从identityHashCode()方法看出key可以是任意类型,都可以变成int类型的hashCode。

h >>> 16 是什么?

h是hashCode。h >>> 16是用来取出h的高16,(>>>是无符号右移)

0000 0100 1011 0011 1101 1111 1110 0001

4

0000 0000 0100 1011 0011 1101 1111 1110

16

0000 0000 0000 0000 0000 0100 1011 0011
上面两个例子 一个>>>4 一个>>>16,可用于对比。

这里根据Key返回了一个Hash值。拿到哈希值咱们继续看 putVal() 方法。

2.2.2 putVal()

/**
 * table 在首次使用时初始化,存储数据的Node类型 数组,并根据需要调整大小。
 * 长度 =  2 的幂。(特殊情况下可为0),数组的每个元素 = 1个单链表
 */

transient Node<K,V>[] table;



/**
  * @param hash 键的哈希值
  * @param key 键
  * @param value 要放置的值
  * @param onlyIfAbsent 如果为真,则不要更改现有值
  * @param evict 如果为 false,则表处于创建模式。
  * @return 前一个值,如果没有则返回 null
 */

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

               boolean evict) {

    Node<K,V>[] tab; Node<K,V> p; int n, i;

    //判断table是否初始化

    if ((tab = table) == null || (n = tab.length) == 0)

        //调用 resize() 方法,进行初始化并赋值

        n = (tab = resize()).length;

    //通过hash获取下标,如果数据为null

    if ((p = tab[i = (n - 1) & hash]) == null)

        //tab[i]下标没有值,创建新的Node并赋值。

        tab[i] = newNode(hash, key, value, null);

    else {

         //tab[i] 下标的有数据,发生碰撞

        Node<K,V> e; K k;

        //判断tab[i]的hash值传入的hash值相当,tab[i]的的key值和传入的key值相同

        if (p.hash == hash &&

            ((k = p.key) == key || (key != null && key.equals(k))))

            //如果是原有数据直接替换即可

            e = p;

        else if (p instanceof TreeNode)//判断数据结构为红黑树

            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

        else {//数据结构是链表

            for (int binCount = 0; ; ++binCount) {

            

                //p的下一个节点为null,表示p就是最后一个节点

                if ((e = p.next) == null) {

                    //创建Node并插入链表的尾部

                    p.next = newNode(hash, key, value, null);

                    //当元素>=8-1,链表转为树(红黑树)结构

                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

                        treeifyBin(tab, hash);

                    break;

                }

                //如果key在链表中已经存在,则退出循环

                if (e.hash == hash &&

                    ((k = e.key) == key || (key != null && key.equals(k))))

                    break;

                //更新p指向下一个节点,继续遍历

                p = e;

            }

        }

        //如果key在链表中已经存在,则修改其原先key的value值,并且返回老的value值

        if (e != null) {

            V oldValue = e.value;

            if (!onlyIfAbsent || oldValue == null)

                e.value = value;

            afterNodeAccess(e);//替换旧值时会调用的方法(默认实现为空)

            return oldValue;

        }

    }

    ++modCount;//修改次数

    //根据map值判断是否要对map的大小扩容

    if (++size > threshold)

        resize();

    afterNodeInsertion(evict);//插入成功时会调用的方法(默认实现为空)

    return null;

}

2.2.3 resize() 扩容

/**
 * 默认初始容量 - 必须是 2 的幂。
 */

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
  * 初始化或加倍 table 大小。 如果为空,则按照字段阈值中保存的初始容量目标进行分配。
  * 否则,因为我们使用二次幂扩展
  */

final Node<K,V>[] resize() {

    Node<K,V>[] oldTab = table;

    int oldCap = (oldTab == null) ? 0 : oldTab.length;

    int oldThr = threshold;

    int newCap, newThr = 0;

    //table已经初始化,且容量 > 0

    if (oldCap > 0) {

        //MAXIMUM_CAPACITY=1<<30=2的30次幂=1073741824

        if (oldCap >= MAXIMUM_CAPACITY) {

            //如果旧的容量已近达到最大值,则不再扩容,阈值直接设置为最大值

            //Integer.MAX_VALUE=(1 << 31)-1=2的31次幂-1=2147483648-1

            threshold = Integer.MAX_VALUE;

            return oldTab;

        }

        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

                 oldCap >= DEFAULT_INITIAL_CAPACITY)

            newThr = oldThr << 1; // 扩大两倍

    }

    else if (oldThr > 0) // 初始化容器=threshold

        newCap = oldThr;

    else { 

        //threshold 和 table 皆未初始化情况,此处即为首次进行初始化

        //第一次进入什么都没有所以要初始化容器(16)

        newCap = DEFAULT_INITIAL_CAPACITY;

        //12 = 0.75*16

        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

    }

    //newThr 为 0 时,计算阈值

    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数据

    table = newTab;

    //如果之前的数组桶里面已经存在数据,由于table容量发生变化,hash值也会发生变化,需要重新计算下标

    if (oldTab != null) {

        for (int j = 0; j < oldCap; ++j) {

            Node<K,V> e;

            //将指定下标数据不为null

            if ((e = oldTab[j]) != null) {

                //将指定下标数据置空

                oldTab[j] = null;

                //指定下标数据只有一个

                if (e.next == null)

                    //直接将数据存放到新计算的hash值下标

                    newTab[e.hash & (newCap - 1)] = e;

                else if (e instanceof TreeNode)//红黑树

                    //将树箱中的节点拆分为下树箱和上树箱,如果现在太小(<=6),则数据结构取消红黑树改为链表。

                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

                else { //链表

                    //重新计算hash值,根据新的下标重新分组

                    Node<K,V> loHead = null, loTail = null;

                    Node<K,V> hiHead = null, hiTail = null;

                    Node<K,V> next;

                    do {

                        next = e.next;

                        if ((e.hash & oldCap) == 0) {

                            if (loTail == null)

                                loHead = e;

                            else

                                loTail.next = e;

                            loTail = e;

                        }

                        else {

                            if (hiTail == null)

                                hiHead = e;

                            else

                                hiTail.next = e;

                            hiTail = e;

                        }

                    } while ((e = next) != null);

                    if (loTail != null) {

                        loTail.next = null;

                        newTab[j] = loHead;

                    }

                    if (hiTail != null) {

                        hiTail.next = null;

                        newTab[j + oldCap] = hiHead;

                    }

                }

            }

        }          

    }

    return newTab;

}

因此,resize()方法 初始化了容器,并给 table 赋值,返回Node<K,V>[]。

2.2.4 putTreeVal()

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;

        //循环所有的节点,如果key冲突,返回原key对应的对象,如果不存在key冲突,则直接存放对象,并返回null对象

        for (TreeNode<K,V> p = root;;) {

            int dir, ph; K pk;

            //dir:决定节点的位置

            if ((ph = p.hash) > h)//当前节点的哈希值大于存放对象的哈希值

                dir = -1;

            else if (ph < h)//当前节点的哈希值小于存放对象的哈希值

                dir = 1;

            else if ((pk = p.key) == k || (k != null && k.equals(pk)))//当前节点等于存放对象的哈希值,且key相同,则返回当前节点

                return p;

            else if ((kc == null &&

                      (kc = comparableClassFor(k)) == null) ||

                     (dir = compareComparables(kc, k, pk)) == 0) {

                //如果不按哈希值排序,而是按照比较器排序,则通过比较器返回值决定进入左右结点

                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;

             //如果p的左节点或右节点为空,证明已经找到存放位置

            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);

                //根据dir设置存放位置

                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;

                //balanceInsertion树化结构,设置红黑节点,是否需要旋转

                //moveRootToFront重置根节点                    

                moveRootToFront(tab, balanceInsertion(root, x));

                return null;

            }

        }

    }

2.3 get()

获取数据

这方法返回结果:

节点(Node);

null

该 key 对应的数据就是 null;

HashMap 中不存在该 key

public V get(Object key) {

    Node<K,V> e;

    //hash(key):根据key的hashCode计算hash值,这个跟put中的的一样。

    return (e = getNode(hash(key), key)) == null ? null : e.value;

}

这个方法就是通过 getNode() 方法来获取节点,如果节点为null则返回null,如果节点存在则返回key对应的value。

2.3.1 getNode()

/**
 * Implements Map.get and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @return 节点(node),如果没有则返回 null
 */

final Node<K,V> getNode(int hash, Object key) {

    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

    //根据hash值获取table中节点存放位置,并获取第一个元素。

    if ((tab = table) != null && (n = tab.length) > 0 &&

        (first = tab[(n - 1) & hash]) != null) {

        //如果第一个节点是我们要找的Key,则直接返回

        if (first.hash == hash && // 

            ((k = first.key) == key || (key != null && key.equals(k))))

            return first;

        //获取下一个节点的信息

        if ((e = first.next) != null) {

            //判断数据结构是红黑树,则通过getTreeNode()获取节点并返回

            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;

}

2.3.2 getTreeNode()

static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> {

    TreeNode<K,V> parent;  // 红黑树节点

    //调用树的find()函数

    final TreeNode<K,V> getTreeNode(int h, Object k) {

        return ((parent != null) ? root() : this).find(h, k, null);

    }

2.3.3 find()

final TreeNode<K,V> find(int h, Object k, Class<?> kc) {

        TreeNode<K,V> p = this;

        do {

            int ph, dir; K pk;

            TreeNode<K,V> pl = p.left, pr = p.right, q;

            if ((ph = p.hash) > h)//当前节点的哈希值大于给定哈希值,进入左节点

                p = pl;

            else if (ph < h)//当前节点小于给定哈希值,进入右节点

                p = pr;

            else if ((pk = p.key) == k || (k != null && k.equals(pk)))//当前节点等于给定哈希值,且key相同,则返回当前节点

                return p;

            else if (pl == null)//如果左节点为null,则进入右节点

                p = pr;

            else if (pr == null)//如果右节点为null,则进入左节点

                p = pl;

            else if ((kc != null ||

                      (kc = comparableClassFor(k)) != null) &&

                     (dir = compareComparables(kc, k, pk)) != 0)//如果不按哈希值排序,而是按照比较器排序,则通过比较器返回值决定进入左右结点

                p = (dir < 0) ? pl : pr;

            else if ((q = pr.find(h, k, kc)) != null)//如果在右结点中找到该关键字,直接返回当前节点

                return q;

            else//进入左节点

                p = pl;

        } while (p != null);

        return null;

    }

2.4 remove()

public V remove(Object key) {

    Node<K,V> e;

    return (e = removeNode(hash(key), key, null, false, true)) == null ?

        null : e.value;

}



final Node<K,V> removeNode(int hash, Object key, Object value,

                           boolean matchValue, boolean movable) {

    Node<K,V>[] tab; Node<K,V> p; int n, index;

    //判断table是否存在、数据元素大于>0、index位置元素是否存在

    if ((tab = table) != null && (n = tab.length) > 0 &&

        (p = tab[index = (n - 1) & hash]) != null) {

        Node<K,V> node = null, e; K k; V v;

        //判断p和传入的数据一致

        if (p.hash == hash &&

            ((k = p.key) == key || (key != null && key.equals(k))))

            //获取p的数据

            node = p;

        else if ((e = p.next) != null) {//和p数据不一致,获取p下一个节点数据

            //判断p的数据结构是否为红黑树

            if (p instanceof TreeNode)

                //红黑树,调用find找到node节点

                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);

            else {

                //链表

                do {

                    //循环找到跟e相同的数据

                    if (e.hash == hash &&

                        ((k = e.key) == key ||

                         (key != null && key.equals(k)))) {

                        node = e;

                        break;

                    }

                    p = e;

                } while ((e = e.next) != null);

            }

        }

        //判断获取到的数据

        if (node != null && (!matchValue || (v = node.value) == value ||

                             (value != null && value.equals(v)))) {

            //根据红黑树删除数据

            if (node instanceof TreeNode)

                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);

            else if (node == p)//node=p,然后指针指向p节点后一个节点,达到删除node的目的

                tab[index] = node.next;

            else//此时node=p.next,所以node就被干掉了

                p.next = node.next;

            ++modCount;

            --size;

            afterNodeRemoval(node);

            return node;

        }

    }

    return null;

}

这个方法其实跟上面的 put/get 方法类似,都是先获取哈希值,然后根据哈希值和Key找到这个节点,进行删除,自己探索乐趣多多。

2.5 clear()

public void clear() {

    Node<K,V>[] tab;

    modCount++;

    if ((tab = table) != null && size > 0) {

        size = 0;

        for (int i = 0; i < tab.length; ++i)

            tab[i] = null;

    }

}

这个简单粗暴,直接遍历table,然后将table的值设置为null。

其实HashMap常用的方法也就这么几个,你了解了吗?

目录
相关文章
|
6月前
|
存储 缓存 安全
面试题-HashMap底层原理与HashTable的区别
字节跳动面试题-HashMap底层原理与HashTable的区别
60 0
|
6月前
|
存储 算法 Java
【深入挖掘Java技术】「源码原理体系」盲点问题解析之HashMap工作原理全揭秘(下)
在阅读了上篇文章《【深入挖掘Java技术】「源码原理体系」盲点问题解析之HashMap工作原理全揭秘(上)》之后,相信您对HashMap的基本原理和基础结构已经有了初步的认识。接下来,我们将进一步深入探索HashMap的源码,揭示其深层次的技术细节。通过这次解析,您将更深入地理解HashMap的工作原理,掌握其核心实现。
60 0
【深入挖掘Java技术】「源码原理体系」盲点问题解析之HashMap工作原理全揭秘(下)
|
存储 算法 安全
HashMap底层实现原理
HashMap底层实现原理
164 0
|
24天前
|
机器学习/深度学习 算法
让星星⭐月亮告诉你,HashMap之tableSizeFor(int cap)方法原理详解(分2的n次幂和非2的n次幂两种情况讨论)
`HashMap` 的 `tableSizeFor(int cap)` 方法用于计算一个大于或等于给定容量 `cap` 的最小的 2 的幂次方值。该方法通过一系列的无符号右移和按位或运算,逐步将二进制数的高位全部置为 1,最后加 1 得到所需的 2 的幂次方值。具体步骤包括: 1. 将 `cap` 减 1,确保已经是 2 的幂次方的值直接返回。 2. 通过多次无符号右移和按位或运算,将最高位 1 后面的所有位都置为 1。 3. 最终加 1,确保返回值为 2 的幂次方。 该方法保证了 `HashMap` 的数组容量始终是 2 的幂次方,从而优化了哈希表的性能。
31 1
|
2月前
|
设计模式 安全 Java
HashMap底层原理:数据结构+put()流程+2的n次方+死循环+数据覆盖问题
假如有T1、T2两个线程同时对某链表扩容,他们都标记头结点和第二个结点,此时T2阻塞,T1执行完扩容后链表结点顺序反过来,此时T2恢复运行再进行翻转就会产生环形链表,即B.next=A;采用2的指数进行扩容,是为了利用位运算,提高扩容运算的效率。JDK8中,HashMap采用尾插法,扩容时链表节点位置不会翻转,解决了扩容死循环问题,但是性能差了一点,因为要遍历链表再查到尾部。例如15(即2^4-1)的二进制为1111,31的二进制为11111,63的二进制为111111,127的二进制为1111111。
HashMap底层原理:数据结构+put()流程+2的n次方+死循环+数据覆盖问题
|
6月前
|
Java
HashMap原理解析
HashMap原理解析
161 47
|
存储 安全 Java
java学会这些,我就入门啦!(基础篇六)HashMap、Hashtable、ConcurrentHashMap的原理与区别
java学会这些,我就入门啦!(基础篇六)HashMap、Hashtable、ConcurrentHashMap的原理与区别
|
6月前
|
存储 缓存 安全
Java HashMap:哈希表原理、性能与优化
Java HashMap:哈希表原理、性能与优化
258 1
|
6月前
|
存储 安全 Java
【深入挖掘Java技术】「源码原理体系」盲点问题解析之HashMap工作原理全揭秘(上)
HashMap是基于Map接口构建的数据结构,它以键值对的形式存储元素,允许键和值都为null。由于键的唯一性,HashMap中只能有一个键为null。HashMap的特点是元素的无序性和不重复性。
65 1
【深入挖掘Java技术】「源码原理体系」盲点问题解析之HashMap工作原理全揭秘(上)