HashMap

Posted ericguoxiaofeng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap相关的知识,希望对你有一定的参考价值。

一、关于哈希表:

在讨论哈希表之前,我们先大概了解下其他数据结构在新增,查找等基础操作执行性能

  数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)

  线性链表:对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)

  二叉树:对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。

  哈希表:相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1),接下来我们就来看看哈希表是如何实现达到惊艳的常数阶O(1)的。

  我们知道,数据结构的物理存储结构只有两种:顺序存储结构链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组

  比如我们要新增或查找某个元素,我们通过把当前元素的关键字 通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。

        存储位置 = f(关键字)

  其中,这个函数f一般称为哈希函数,这个函数的设计好坏会直接影响到哈希表的优劣。举个例子,比如我们要在哈希表中执行插入操作:

技术分享图片

查找操作同理,先通过哈希函数计算出实际存储地址,然后从数组中对应地址取出即可。

  哈希冲突

  然而万事无完美,如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。

 

二、关于hashmap:

技术分享图片

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

技术分享图片

 

技术分享图片

三、源码

备注(一个是乘2;一个是次方):

4 << 1:表示4*2=81 << 4:表示2的4次方=16;

基本指标:

技术分享图片
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始化容量为16,must be a power of two,详见下面                                                                                    
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量1G                                                                                                                           
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子                                                                                                                        
static final int TREEIFY_THRESHOLD = 8;// 从链表变为红黑树的阈值,当链表长度大于等于8时,由链表转换成红黑树                                                                                                      
static final int UNTREEIFY_THRESHOLD = 6; // 从红黑树变为链表的阈值                                                                                                                         
static final int MIN_TREEIFY_CAPACITY = 64; // 当需要将解决 hash 冲突的链表转变为红黑树时,需要判断下此时数组容量,若是由于数组容量太小(小于 MIN_TREEIFY_CAPACITY )导致的 hash 冲突太多,则不进行链表转变为红黑树操作,转为利用 resize() 函数对 hashMap 扩容
View Code

成员变量:

技术分享图片
//比如说,在初始化时,默认的容量是16,那么table的length就是16,其threshold=容量×负载因子=16×0.75=12,这就代表着,当size大于12时,就会进行扩容(容量会×2,threshold会根据新容量重新计算)的操作!                                         
//这样做的目的很明确,就是为了减少哈希冲突!有效元素的个数少于哈希表的总大小时,其产生哈希冲突的可能性一定是小于相等情况的!                                                                                                      
                                                                                                                                                                     
transient Node<K,V>[] table; // 真正开辟的空间,其length就是真正的容量大小;真正占用空间(用不用是一回事,先占用先);map.size()就是指这个; When allocated, length is always a power of two.                      
transient int size; // 真正使用的空间;有效的结点个数;总的键值对的个数;    The number of key-value mappings contained in this map.                                                          
int threshold; // 阈值,大于这个值,扩容;The next size value at which to resize (capacity * load factor);用来记录当前容量下,最适合存放多少键值对(容量*负载因子)                                          
final float loadFactor; // 负载因子,默认0.75                                                                                                                               
transient int modCount; //用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException                   
transient Set<Map.Entry<K,V>> entrySet; // //由 hashMap 中 Node<K,V> 节点构成的 set         
View Code

 静态工具:

技术分享图片
/**                                                                                     
 * Node<K, V>是一个静态内部类,封装了这个结点的所有信息                                                      
 */                                                                                     
static class Node<K,V> implements Map.Entry<K,V>{                                       
    final int hash;  // 相对应的hash值,其方法见下                                                 
    final K key;                                                                        
    V value;                                                                            
    Node<K,V> next;  // 链表中指向下一处的指针;为了解决哈希冲突,当产生哈希冲突时,next就可以指向一张链表,或者一棵黑树!             
    ......                                                                              
}                                                                                       
                                                                                        
/**                                                                                     
 *  计算其hash值的方法,看不懂                                                                     
 */                                                                                     
static final int hash(Object key) {                                                     
    int h;                                                                              
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);                       
}                                                                                       
                                                                                        
/**                                                                                     
 *  求下标,也就是求:                                                                           
 *  当 table.length 满足2的整数次幂时,以下条件成立:                                                    
 *  hash & (table.length - 1) == hash % table.length                                    
 */                                                                                     
hash & (table.length - 1)                                                               
                                                                                        
/**                                                                                     
 * 方法返回的值是最接近 initialCapacity 的2的幂,若指定初始容量为9,则实际 hashMap 容量为16(还不是很懂)                  
 * @param cap                                                                           
 * @return                                                                              
 */                                                                                     
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;            
}    
View Code

 构造方法(构造器下Capacity都没有初始化进去好似):

技术分享图片
// 赋值阈值以及负载因子初始化                                                          
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);  // 阈值初始化                                        
}                                                                                          
                                                                                           
// 默认负载因子为0.75了                                                                     
public HashMap(int initialCapacity) {                                                      
    this(initialCapacity, DEFAULT_LOAD_FACTOR);                                            
}                                                                                          
                                                                                           
// 初始化负载因子
// 默认构造器阈值不初始化,没有容量哪里来阈值??
public HashMap() {                                                                         
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted                   
}
View Code

 

 put方法:如果存在Hash碰撞就会以链表的形式保存,把当前传进来的参数生成一个新的节点保存在链表的尾部(JDK1.7保存在首部)。而如果链表的长度大于8那么就会以红黑树的形式进行保存。

技术分享图片
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; // 这里的tab就是指这个table,只不过不用table罢了                                                                    
    Node<K,V> p; //p就是相对应下标的node值                                                                                        
    int n;//指hashMap里面的数组长度                                                                                              
    int i;//指hashMap里面的数组下标                                                                                              
    // 如果table未初始化或长度为0,则进行初始化(立刻扩容)                                                                                     
    if ((tab = table) == null || (n = tab.length) == 0) {                                                                
        n = (tab = resize()).length;//初始化的时候,调用resize()方法,得到hashmap里面的数组长度                                               
    }                                                                                                                    
    // 如果节点hash值对应的数组位置为空,直接赋值                                                                                           
    if ((p = tab[i = (n - 1) & hash]) == null) {// i = (n - 1) & hash 求hashmap数组下标并赋值给i,判断相对应的数组节点是否为空                   
        tab[i] = newNode(hash, key, value, null);  // 如果为空,直接增加一个节点,很简单                                                  
    } else {  //如果不为空的话                                                                                                  
        Node<K,V> e; // 对应下标的新的节点的node值                                                                                  
        K k;//相对应下标的node值p的key                                                                                           
        //如果hash值一样,key值也一样(注意,因为hash值一样,key值可能不一样;先对比hash值再对比key值的(因为对比hash值速度更快)),则直接替换                                
        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) {                                                                       
                if ((e = p.next) == null) {  //下一个节点恒等于空,说明是放进链表的末尾嘛                                                     
                    p.next = newNode(hash, key, value, null); //在末尾放这个node值嘛                                             
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st (实质上,一旦到了这个阈值8,而不是超过,都会转换成红黑树)                  
                        treeifyBin(tab, hash);                                                                           
                    break;                                                                                               
                }                                                                                                        
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {                          
                    break;                                                                                               
                }                                                                                                        
                p = e;                                                                                                   
            }                                                                                                            
        }                                                                                                                
        //改变value值                                                                                                       
        if (e != null) { // existing mapping for key                                                                     
            V oldValue = e.value;                                                                                        
            if (!onlyIfAbsent || oldValue == null) {//注释里:if true, don‘t change existing value,但是put这方方法传过来的是false       
                e.value = value;                                                                                         
            }                                                                                                            
            afterNodeAccess(e);                                                                                          
            return oldValue;                                                                                             
        }                                                                                                                
    }                                                                                                                    
    ++modCount;                                                                                                          
                                                                                                                         
    //如果大于阈值,则扩容                                                                                                         
    if (++size > threshold) {                                                                                            
        resize();                                                                                                        
    }                                                                                                                    
    afterNodeInsertion(evict);                                                                                           
    return null;                                                                                                         
}  
View Code

 扩容方法resize()(涉及到位移,看不懂,有空再看):

技术分享图片
final Node<K,V>[] resize() {                                                                                                                 
    Node<K,V>[] oldTab = table;// 旧数组赋值给oldTab                                                                                               
    int oldCap = (oldTab == null) ? 0 : oldTab.length;//oldCap是指oldCapacity,是指旧hashmap的数组的长度                                                 
    int oldThr = threshold;//旧阈值                                                                                                             
    int newCap; //新数组容量                                                                                                                      
    int newThr = 0;//新容量                                                                                                                     
    if (oldCap > 0) { //table扩容过                                                                                                                  
        if (oldCap >= MAXIMUM_CAPACITY) { //如果超过最大容量,就不再扩容,注意threshold = Integer.MAX_VALUE;                                                  
            threshold = Integer.MAX_VALUE;                                                                                                   
            return oldTab;                                                                                                                   
        } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {// 如果老容量扩大2倍仍不超过最大值,则新容量为原来的2倍          
            newThr = oldThr << 1; // double threshold                                                                                        
        }                                                                                                                                    
    } else if (oldThr > 0) {// initial capacity was placed in threshold;使用带有初始容量的构造器时,table容量为初始化得到的threshold                                                                      
        newCap = oldThr;                                                                                                  
    } else { // zero initial threshold signifies using defaults;new HashMap().put("",""):默认构造器走这里     
        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;  // 如果一开始是new hashmap(),则不走下面这一步了,因为oldTal为空嘛                                                                            
    if (oldTab != null) {         //对新扩容后的table进行赋值                                                                                                             
        for (int j = 0; j < oldCap; ++j) {                                                                                                   
            Node<K,V> e;// 相对应的下标值                                                                                                           
            if ((e = oldTab[j]) != null) {                                                                                                   
                oldTab[j] = null;  // 将老数组元素置空,让垃圾回收器回收                                                                                      
                if (e.next == null) {   // 如果数组元素没有链表,直接添加到新数组                                                                               
                    newTab[e.hash & (newCap - 1)] = e;                                                                                       
                } else if (e instanceof TreeNode) {  // 如果e是树节点,则按照树结构处理该分支                                                                  
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);                                                                       
                } else { // preserve order 如果e是链表节点,则按照链表结构处理该分支                                                                             
                    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;                                                                                                                           
}
View Code

 

 get方法:

技术分享图片
final Node<K,V> getNode(int hash, Object key) {                                                                           
    Node<K,V>[] tab; // table副本                                                                                           
    Node<K,V> first; // 相对应下标的那个数组                                                                                        
    Node<K,V> e;                                                                                                          
    int n; // table的数组长度                                                                                                  
    K k;                                                                                                                  
    // table一定不能大于0啊,否则就返回空啊                                                                                              
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {                         
     // always check first node  如果hash值一样,而且key值也一样;高度注意:桶中第一项(数组元素)相等(是桶中第一项元素,是第一项):第一项特别判断,因为链表红黑树不影响                 
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) {                           
            return first;                                                                                                 
        }                                                                                                                 
        // 如果桶中第一个元素不相等,而且同志不止一个元素                                                                                        
        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;                                                                                                          
}            
View Code

 

参考链接:

HashMap实现原理及源码分析:https://www.cnblogs.com/chengxiao/p/6059914.html

深入理解HashMap的扩容机制:https://www.cnblogs.com/yanzige/p/8392142.html(从这里可以了解到,jdk7的扩容标准跟jdk8是不一样的)


以上是关于HashMap的主要内容,如果未能解决你的问题,请参考以下文章

HashMap原理:哈希函数的设计

HashMap深度解析

JDK源码阅读之 HashMap

ArrayList 和 HashMap 的默认大小是多数?

如何将 Parcelable 与 HashMap 一起使用

hashmap冲突的解决方法以及原理分析: