HashMap

HashMap

HashMap类中有一个非常重要的属性,就是 Node[ ] table,即哈希桶数组。

transient Node<K,V>[] table;

Node是HashMap的一个内部类,本质是就是一个映射(键值对),除了K,V,还包含hash和next。

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

 

1、HashMap由数组和链表来实现对数据的存储

HashMap采用Node数组 来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,以此来解决Hash冲突的问题。

数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:查找快,插入和删除困难;

链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。

 

 

 

img

 

img

 

HashMap里面实现一个静态内部类Node,其重要的属性有 hash,key,value,next。

HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Node。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Node[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?当冲突时,在冲突的地址上生成一个链表,将冲突的元素的key,通过equals进行比较,相同即覆盖,不同则添加到链表上,

{

此段感觉有问题

HashMap会这样做:B.next = A,Node[0] = B,如果又进来C,index也等于0,那么C.next = B,Node[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素

}

到这里为止,HashMap的大致实现,我们应该已经清楚了。

 

 

hash碰撞:
hash是指,两个元素通过hash函数计算出的值是一样的,是同一个存储地址。当后面的元素要插入到这个地址时,发现已经被占用了,这时候就产生了hash冲突

hash冲突的解决方法:
开放定址法(查询产生冲突的地址的下一个地址是否被占用,直到寻找到空的地址),再散列法,链地址法等。hashmap采用的就是链地址法,jdk1.7中,当冲突时,在冲突的地址上生成一个链表,将冲突的元素的key,通过equals进行比较,相同即覆盖,不同则添加到链表上,此时如果链表过长,效率就会大大降低,查找和添加操作的时间复杂度都为O(n);但是在jdk1.8中如果链表长度大于8,链表就会转化为红黑树,时间复杂度也降为了O(logn),性能得到了很大的优化。
 

下面通过源码分析一下,HashMap的底层实现

 

首先,hashMap的主干是一个Node数组(jdk1.7及之前为Entry数组)每一个Node包含一个key与value的键值对,与一个next指向下一个node,hashMap由多个Node对象组成。

再看下hashMap中几个重要的字段:

//默认初始容量为16,0000 0001 右移4位 0001 0000为16,主干数组的初始容量为16,而且这个数组
//必须是2的倍数(后面说为什么是2的倍数)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
 
//最大容量为int的最大值除2
static final int MAXIMUM_CAPACITY = 1 << 30;
 
//默认加载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
 
//阈值,如果主干数组上的链表的长度大于8,链表转化为红黑树
 static final int TREEIFY_THRESHOLD = 8;
 
//hash表扩容后,如果发现某一个红黑树的长度小于6,则会重新退化为链表
 static final int UNTREEIFY_THRESHOLD = 6;
 
//当hashmap容量大于64时,链表才能转成红黑树
 static final int MIN_TREEIFY_CAPACITY = 64;
 
//临界值=主干数组容量*负载因子
int threshold;

hashMap的构造方法:

 //initialCapacity为初始容量,loadFactor为负载因子
public HashMap(int initialCapacity, float loadFactor) {
        //初始容量小于0,抛出非法数据异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //初始容量最大为MAXIMUM_CAPACITY
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //负载因子必须大于0,并且是合法数字
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        
        this.loadFactor = loadFactor;
        //将初始容量转成2次幂
        this.threshold = tableSizeFor(initialCapacity);
    }
 
    //tableSizeFor的作用就是,如果传入A,当A大于0,小于定义的最大容量时,
  //  如果A是2次幂则返回A,否则将A转化为一个比A大且差距最小的2次幂。  
    //例如传入7返回8,传入8返回8,传入9返回16
  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;
    }
 
 
    //调用上面的构造方法,自定义初始容量,负载因子为默认的0.75
 public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
 
 
    //默认构造方法,负载因子为0.75,初始容量为DEFAULT_INITIAL_CAPACITY=16,初始容量在第一次put时才会初始化
 public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
 
 
    //传入一个MAP集合的构造方法
 public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
 

一次put经历的过程:

首先执行put方法:

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

然后将key传入hash()方法,计算其对应的hash值:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

此处如果传入的int类型的值:①向一个Object类型赋值一个int的值时,会将int值自动封箱为Integer。②integer类型的hashcode都是他自身的值,即h=key;

h >>> 16为无符号右移16位,低位挤走,高位补0;

^ 为按位异或,即转成二进制后,相异为1,相同为0,

由此可发现,当传入的值小于  2的16次方-1 时,调用这个方法返回的值,都是自身的值。
 

解释:

>>> 运算符

描述: 在表达式中执行无符号右移。

语法:result = expression1 >>> expression2

>>> 运算符的语法组成部分如下:

部分描述
result任何变量
expression1任何表达式
expression2任何表达式。

说明:

>>> 运算符把 expression1 的各个位向右移 expression2 指定的位数。右移后左边空出的位用零来填充。移出右边的位被丢弃。例如:

var temp
temp = -14 >>> 2

变量 temp 的值为 -14 (即二进制的 11111111 11111111 11111111 11110010),向右移两位后等于 1073741820 (即二进制的 00111111 11111111 11111111 11111100)

 

然后再执行putVal方法:

//onlyIfAbsent是true的话,不要改变现有的值
//evict为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;
//如果主干上的table为空,长度为0,调用resize方法,调整table的长度(resize方法在下图中)
        if ((tab = table) == null || (n = tab.length) == 0)
            /* 这里调用resize,其实就是第一次put时,对数组进行初始化。
               如果是默认构造方法会执行resize中的这几句话:
               newCap = DEFAULT_INITIAL_CAPACITY;  新的容量等于默认值16
               newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);            
               threshold = newThr;   临界值等于16*0.75
               Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 
               table = newTab; 将新的node数组赋值给table,然后return newTab
                
                如果是自定义的构造方法则会执行resize中的: 
                int oldThr = threshold;   
                newCap = oldThr;   新的容量等于threshold,这里的threshold都是2的倍数,原因在    
                于传入的数都经过tableSizeFor方法,返回了一个新值,上面解释过
                float ft = (float)newCap * loadFactor; 
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                (int)ft : Integer.MAX_VALUE); 
                 threshold = newThr; 新的临界值等于 (int)(新的容量*负载因子)
                Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
                table = newTab; return newTab;
            */
            n = (tab = resize()).length;  //将调用resize后构造的数组的长度赋值给n
        // 按位与计算 &: 只有对应的两个二进位均为1时,结果位才为1 ,否则为0
//如果数组最大下标为15,那么不管Hash是多少,i都不会大于15,也就不会数组越界
        if ((p = tab[i = (n - 1) & hash]) == null) //将数组长度减1再和hash值进行按位与&运算
            tab[i] = newNode(hash, key, value, null);//位置为空,将i位置上赋值一个node对象
        else {  //位置不为空
            Node<K,V> e; K k;
            if (p.hash == hash &&  // 如果这个位置的old节点与new节点的key完全相同
                ((k = p.key) == key || (key != null && key.equals(k)))) 
                e = p;             // 则e=p
            else if (p instanceof TreeNode) // 如果p已经是树节点的一个实例,既这里已经是树了
               // 红黑树对象操作
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {  //p与新节点既不完全相同,p也不是treenode的实例
                for (int binCount = 0; ; ++binCount) {  //一个死循环
                    if ((e = p.next) == null) {   //e=p.next,如果p的next指向为null
                        p.next = newNode(hash, key, value, null);  //指向一个新的节点
                        if (binCount >= TREEIFY_THRESHOLD - 1) // 如果链表长度大于等于8
                            treeifyBin(tab, hash);  //将链表转为红黑树
                        break;
                    }
                 //如果遍历过程中链表中的元素与新添加的元素完全相同,则跳出循环
                   if (e.hash == hash &&                
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                 //将p指向下一个节点 将p中的next赋值给p,即将链表中的下一个node赋值给p,
                    p = e; 
                           //继续循环遍历链表中的元素
                }
            }
            if (e != null) { //这个判断中代码作用为:如果添加的元素产生了hash冲突,那么调用                
                             //put方法时,会将他在链表中他的上一个元素的值返回
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)  //判断条件成立的话,将oldvalue替换        
                //为newvalue,返回oldvalue;不成立则不替换,然后返回oldvalue
                    e.value = value;
                afterNodeAccess(e);  //这个方法在后面说
                return oldValue;
            }
        }
        ++modCount;  //记录修改次数
        if (++size > threshold)   //如果元素数量大于临界值,则进行扩容
            resize();   //下面说
        afterNodeInsertion(evict);  
        return null;
    }

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值