HashMap源码(JDK1.7)解读

目录

一、构造函数

二、添加元素put()

1.链表

2.存放数据put() 

二、取元素get()

三、其他方法和知识点

四、与HashTable的区别


HashMap其实就是存储一系列的链表数组,用链表来解决哈希冲突。

一、构造函数

有四种构造函数,最终调用到可以设置初始化容量initialCapacity和负载因子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;
        threshold = initialCapacity;
        init();
    }
变量默认值备注

loadFactor

(负载因子)

0.75用来控制数组的元素的稀疏程度。值越接近1,说明该数组结构中的存放的数据越多,查找效率会越低

initialCapacity

(初始化容量)

16 

threshold

(实际容量)

16允许HashMap存放的最多元素的个数,当超过该值的时候,就会进行扩容

二、添加元素put()

在研究添加元素之前,先了解下存放key和value的Entry<K,V>[] table数组,其实这就是一个链表数组,用来存放添加的key和value。

1.链表

该链表结构的单个节点含有存放的数据的key、value、key对应的hash值以及指向下一个节点。下面是单个节点的含有的属性

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
//省略代码
}

2.存放数据put() 

看下put()对应的源码

 public V put(K key, V value) {
//1)如果是空table,则进行初始化,创建一个最接近的2的n次方的数组
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
//2)如果key为null,则单独处理。所以HashMap是支持key为null
        if (key == null)
            return putForNullKey(value);
//3)将key通过哈希函数转换成hash值
        int hash = hash(key);
//4)找到该hash值对应的数组的索引值
        int i = indexFor(hash, table.length);
//采用链地址法来处理哈希冲突,将所有索引一致的节点构成一个单链表
//5)循环链表节点,若该HashMap中已经存在了该key的hash值,直到找到该数组中的节点的hash值和当前的key的hash值相同的时候,则更新alue
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
//6)如果没有找到该key对应的hash值,则进行插入该值
        addEntry(hash, key, value, i);
        return null;
    }

1)判断table是否为空,如果仍为空,则通过inflateTable()进行初始化,创建 一个最接近接近且>=2的N次方的数组的大小。

2)判断传入的key是null,则将该值存放到table[0]对应的链表元素的节点位置。从源码中可以看出,null对应的hash值为0,存放在table[0]的链表节点中。所以HashMap支持key为null

    private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
//若存在key为null的节点,则直接替换原值
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
//否则插入该值。key为null的hash值为0
        addEntry(0, null, value, 0);
        return null;
    }

3)将非null的key值转换成hash值

4)找到该hash值在对应的table数组的索引值

5)在该索引值的位置上的链表元素从头节点开始查找是否存在该key值,若存在该key值,则直接替换里面的value对应的值

6)若不存在该key值,则将该元素插入到该链表的对应位置。从源码看下该value是怎么插入到链表中的

/**
* @param hash 哈希值
* @param key 插入的key
* @param value 插入的value
* @param bucketIndex table数组的索引值
*/   
 void addEntry(int hash, K key, V value, int bucketIndex) {
//(6.1)若现在插入元素的已经超过了实际容量threshold,则进入扩容为目前长度的2倍
        if ((size >= threshold) && (null != table[bucketIndex])) {
//(6.2)扩容2倍,并进行对插入的链表进行重排
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
//(6.3)得到新的table数组的索引值
            bucketIndex = indexFor(hash, table.length);
        }
//(6.4)插入该key和value
        createEntry(hash, key, value, bucketIndex);
    }

(6.2)进行扩容创建原容量2倍大小的数组,并通过transfer()来调整对应的位置的链表元素

    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
//对原有元素在table中的位置进行重排
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

(6.3)得到对应的新的table中对应的索引值

(6.4)插入对应的key和value的节点

    void createEntry(int hash, K key, V value, int bucketIndex) {
//找到对应的索引值的链表元素
        Entry<K,V> e = table[bucketIndex];
//并将该元素作为当前链表的下一个节点
        table[bucketIndex] = new Entry<>(hash, key, value, e);
//统计此时HashMap中元素的个数
        size++;
    }
  • 插播——创建链表 

(1)这里采用的链表节点的插入方式为头插法,是采用以下的这种插入方式

 

每个节点为

public class Node {

    public int data;
    public Node next;

    public Node(int data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Node() {
    }
}

1为第一个节点,往后在加入节点的时候,要取出当前链表,然后作为当前要插入节点的next节点,header指针每次都向前移动,一行代码就可以实现链表结构:

    private Node head1;   
    public void insert1(int data){
        head1 = new Node(data,head1);
    }

打印该链表的数据会依次为4,3,2,1

(2)还有一种插入方式为尾插法,如下图

 

1为第一个节点,往后在插入节点的时候依次作为前一个插入的节点的next节点。就是header不变化,每加入的节点为尾节点。

      public void insert(int data) {
        if (head == null) {
            head = new Node();
            head.data = data;
            head.next = null;
            return;
        }

        Node node = new Node();
        node.data = data;
        Node p = head;
        while (p.next != null) {
            p = p.next;
        }
        p.next = node;

打印该链表的数据依次为1,2,3,4 

二、取元素get()

有了存的过程,取的过程其实也就简单了,就是找到key对应的table数组中的位置,然后找到在位置的链表结构,然后找到该元素。同样key为null的时候,对应的hash值为0

    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        //如果key为null,则直接hash值为0,否则得到对应key的hash值
        int hash = (key == null) ? 0 : hash(key);
//根据hash值找到对应table数组中的索引值,取出对应的链表元素
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
//直到找到对应的key值对应的value返回
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

三、其他方法和知识点

像判断HashMap的长度相关的size()、判断是不是有key相关的containsKey()等,其实都是从创建的链表数组中进行判断的。

我们看到对于HashMap中一些变量使用transient进行修饰。一旦变量被transient修饰,则变量修饰的内容在序列化后无法获得。transient只能修饰变量,不能修饰类和方法。并且该变量所在的类要实现Serializable接口。

JDK 1.7之前的HashMap利用链表寻址法来解决哈希冲突,其实在一种非常极端的情况下,就是创建的table的链表数组中其中一个元素的链表很长,其他的空间却没有利用,所以在1.8的时候进行了调整,后续还会继续去研究下源码。

 线程同步key/value是否允许nullkey/value是否允许重复
HashMap不支持允许key不允许重复/value允许重复

内部维持一个单链表来解决哈希冲突和记录的插入没有任何顺序

四、与HashTable的区别

 线程安全父类contains()key/valuetable数组初始值扩容
HashMapAbstractMap否,只有containValue()/containKey()都允许默认为16,若设置初始值需要调整为2的n次幂2n
HashtableDictionary是,三者都有都不允许默认为11,若设置初始值无需调整2n+1

当然两者的哈希函数也不相同。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值