目录

  • HashMap介绍
  • HashMap 常问
  • (1)HashMap实现原理
  • (2) HashMap在什么条件下扩容?
  • (3) 讲讲HashMap的get/put的过程?
  • (4) 为什么HashMap的在链表元素数量超过8时改为红黑树?
  • (5) HashMap的并发问题?
  • (6) 你一般用什么作为HashMap的key?
  • (7)哈希冲突
  • HashMap 源码解析
  • 静态成员变量
  • HashMap 成员变量
  • HashMap put方法
  • HashMap resize方法
  • HashMap get()
  • hash()

  • HashMap与HashTable、ConcurrentHashMap区别
  • HashTable
  • HashMap:
  • ConcurrentHashMap:


android HashMap 允许重复吗_链表

HashMap介绍

  • 说一下HashMap的特性?
    1.HashMap存储键值对,key和value都可以是null,key值不可重复,value可以重复;
    2.Hashmap不是同步的,线程不安全;
    3.HashMap底层是hash表,不能保证是有序的;
    4.HashMap的数据结构主要是数组+链表,到JDK1.8时是数组+链表+红黑树。

HashMap 常问

(1)HashMap实现原理

  • 看过HashMap源码吗,知道原理吗?
    1.HashMap存储键值对,key和value都可以是null,key值不可重复,value可以重复;Entry类其实是一个链表结构,有next指针,可以指向下一个Entry实体;
    2.Hashmap不是同步的,线程不安全;
    3.HashMap底层是hash表,不能保证是有序的;
    4.HashMap的数据结构主要是数组+链表,到JDK1.8时是数组+链表+红黑树。
    5.HashMap最常用的就是put和get操作,put就是对key进行hashcode()函数计算得到key在桶数组中的位置来存储Entry对象,get就是根据key计算桶索引,然后比对链表上的key进行查找。
  • 为什么用数组+链表?
    数组是用于确定桶的位置,利用key的hash值对数组长度取模得到索引,而链表是为了解决hash冲突,当不同的key计算得到的索引一样,就会在数组对应位置上形成一条链表。
  • hash冲突你还知道哪些解决办法?
    比较出名的有四种 (1)开放定址法 (2)链地址法 (3)再哈希法 (4)公共溢出区域法
    HashMap中使用的是链地址法
  • 用LinkedList代替数组结构可以么?可以
    Entry[] table = new Entry[capacity];
    ps: Entry就是一个链表节点。
    List table = new LinkedList();
    1.为什么HashMap不用LinkedList,而选用数组?
    因为用数组效率最高!
    2.ArrayList,底层也是数组,查找也快啊,为啥不用ArrayList?
    因为采用基本数组结构,扩容机制可以自己定义,HashMap中数组扩容刚好是2的幂,在做取模运算的效率高。 而ArrayList的扩容机制是1.5倍扩容。
  • 了解TreeMap吗?
    TreeMap 则是基于红黑树的一种提供顺序访问的 Map,和HashMap不同,它的get、put、remove之类操作都是O(logn)的复杂度。
  • 了解 ConcurrentHashMap吗
    Java8 ConcurrentHashMap结构基本上和Java8的HashMap一样,只是ConcurrentHashMap是线程安全的,使用synchronized+CAS来保证线程安全性。

(2) HashMap在什么条件下扩容?

  • HashMap在什么条件下扩容?
    如果bucket满了(超过loadfactor*currentcapacity),就要resize。 loadfactor为0.75,为了最大程度避免哈希冲突 currentcapacity为当前数组大小。
  • 为什么扩容是2的次幂?
    HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同。那么就要通过一个算法来实现数据分配均匀。
    实际就是取模,hash%length。 但是,取模运算不如位移运算快。
    因此,源码中做了优化hash&(length-1)。
    2n是100000,-1就是11111,她和hash与运算,可以保证均匀分布。
    来看一下jdk1.8里的hash方法源码。这么做就是为了降低hash冲突的几率。
static final int hash(Object key) {
     int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

(3) 讲讲HashMap的get/put的过程?

  • 知道hashmap中put元素的过程是什么样吗?

android HashMap 允许重复吗_数组_02

  1. 对key的hashCode()做hash,然后再计算index;
  2. 如果没碰撞直接放到bucket里;
  3. 如果碰撞了,以链表的形式存在buckets后;
  4. 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
  5. 如果节点已经存在就替换old value(保证key的唯一性)
  6. 如果bucket满了(超过load factor*current capacity),就要resize。
  • 知道hashmap中get元素的过程是什么样吗?
  1. bucket里的第一个节点,直接命中;
  2. 如果有冲突,则通过key.equals(k)去查找对应的entry
  3. 若为树,则在树中通过key.equals(k)查找,O(logn);
  4. 若为链表,则在链表中通过key.equals(k)查找,O(n)。
  • hash算法是干嘛的?还知道哪些hash算法?
    把一个大范围映射到一个小范围。
    把大范围映射到一个小范围的目的往往是为了节省空间,使得数据容易保存。
    比较出名的算法有MD4、MD5等
  • 说说String中hashcode的实现?
public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

String类中的hashCode计算方法还是比较简单的,就是以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模。
哈希计算公式可以计为s[0]31^(n-1) + s[1]31^(n-2) + … + s[n-1]
那为什么以31为质数呢?
主要是因为31是一个奇质数,所以31i=32
i-i=(i<<5)-i,这种位移与减法结合的计算相比一般的运算快很多。

(4) 为什么HashMap的在链表元素数量超过8时改为红黑树?

  • jdk1.8中HashMap与之前有哪些不同?
    • 由数组+链表的结构改为数组+链表+红黑树。
    • 优化了高位运算的hash算法:h^(h>>>16)
    • 扩容后,元素要么是在原位置,要么是在原位置再移动2次幂的位置,且链表顺序不变。
    最后一条是重点,因为最后一条的变动,HashMap在1.8中,不会在出现死循环问题。
  • 为什么在解决hash冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?
    因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。 当元素小于8个当时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于8个的时候,此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。
  • 不用红黑树,用二叉查找树可以么?
    二叉查找树在特殊情况下会退化成一条线性结构
  • 当链表转为红黑树后,什么时候退化为链表?
    为6的时候退转为链表。中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

(5) HashMap的并发问题?

  • HashMap在并发编程环境下有什么问题(jdk1.8以后)?
    • 多线程put的时候可能导致元素丢失
    • put非null元素后get出来的却是null
  • 如何解决这些问题?
    使用ConcurrentHashmap,Hashtable等线程安全集合类。

(6) 你一般用什么作为HashMap的key?

android HashMap 允许重复吗_hashmap_03

  • 键可以为Null值么?
    可以,key为null的时候,hash算法最后的值以0来计算,也就是放在数组的第一个位置。
  • 一般用什么作为HashMap的key?
    一般用Integer、String这种不可变类当HashMap当key,而且String最为常用。
  1. 因为是final型,字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
  2. 因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的,这些类已经很规范的覆写了hashCode()以及equals()方法。
  • 用可变类当HashMap的key有什么问题?
    hashcode可能发生改变,导致put进去的值,无法get出,如下所示:
HashMap<List<String>, Object> changeMap = new HashMap<>();
List<String> list = new ArrayList<>();
list.add("hello");
Object objectValue = new Object();
changeMap.put(list, objectValue);
System.out.println(changeMap.get(list));
list.add("hello world");   // hashcode发生了改变
System.out.println(changeMap.get(list));

输出结果如下

java.lang.Object@33909752
null

(7)哈希冲突

android HashMap 允许重复吗_链表_04


android HashMap 允许重复吗_数组_05


高16bit不变,低16bit和高16bit做了一个异或。

设计者认为这方法很容易发生碰撞。为什么这么说呢?不妨思考一下,在n - 1为15(0x1111)时,其实散列真正生效的只是低4bit的有效位,当然容易碰撞了。

因此,设计者想了一个顾全大局的方法(综合考虑了速度、作用、质量),就是把高16bit和低16bit异或了一下。设计者还解释到因为现在大多数的hashCode的分布已经很不错了,就算是发生了碰撞也用O(logn)的tree去做了。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。

HashMap 源码解析

静态成员变量

//默认数组的初始容量是16,必须是2的幂
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
	//数组的最大容量
	static final int MAXIMUM_CAPACITY = 1 << 30;
	//默认负载因子是0.75,和数组大小一起使用,判断是否扩容
	//size>loadfactor*capacity 扩容
	static final float DEFAULT_LOAD_FACTOR = 0.75f; 
	//一个桶的树化阈值
	//当桶中的元素个数超过这个值,链表需要在转为红黑树
	//这个值必须是8,要不然频繁转换,效率也不高
	static final int TREEIFY_THRESHOLD = 8;
	//一个树的链表还原阈值
	//当扩容的时候,桶中元素的个数小于这个值,就会把树结构还原为链表结构
	//这个值应该就是比上面那个小,至少是6,避免频繁转换
	static final int UNTREEIFY_THRESHOLD = 6;
	//用到红黑树的最小容量
	//哈希表中的容量大于这个值,表中的桶才能进行红黑树转换
	//桶元素太多就会扩容,而不是转为红黑树
	//这个值不能小于4*TREEIFY_THRESHOLD
	static final int MIN_TREEIFY_CAPACITY = 64;

HashMap 成员变量

transient java.util.HashMap.Node<K,V>[] table;//桶数组

    transient Set<Map.Entry<K,V>> entrySet;//返回一个迭代器遍历Map结构

    transient int size;//整个hashmap 所包含的节点数

    transient int modCount;//修改次数,比如Put,remove的次数
    //和迭代器配合使用,在迭代过程中,如果其它线程更改了这个值,抛出ConcurrentModificationException异常
  
    int threshold;//hashmap扩容的阈值,值为 loadFactor*table.length,eg:0.75*16=12,数组大小超过 12时就会进行扩容
    final float loadFactor;//负载因子

Node的数据结构:

static class Node<K,V> implements Map.Entry<K,V> {
	final int hash;//key的hash值
    final K key;//key
    V value;//value
    Node<K,V> next;//下一节点的引用
}

HashMap put方法

public V put(K key, V value) {
		//hash(key)就是求key的hash值
		//调用putVal()方法
        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;
        //将成员变量 table 赋值给本地变量 tab,并且将tab的长度赋值给本地变量 n 
        //如果tab为空或者数组长度为0,进行初始化,调用 resize()方法,并且获取赋值后的数组长度
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
            
        //(n-1) & hash根据key的hash值和数组length-1与操作,得到数组的位置,赋值给i
        //p=tab[i]是将位置i的数组对应的key赋值给p
        ///如果当前数组中取出的key为空,就新建一个节点,插到当前数组位置上
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //如果不为空,表示这样的hash值已经存在了,存在hash冲突,或者直接会替换原来的值
        else {
        	//声明本地变量
            Node<K,V> e;
            K k;
            // 如果取出来的节点 hash值相等,key也和原来的一样( == 或者 equals方法为true),直接将这个节点p赋值给刚刚声明的本地变量e
            //另外这里还将节点p的key赋值给了本地变量 k 
            ///检查第一个node是不是要找的值
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果 hash值一样,但不是同一个 key,则表示hash冲突,接着判断这个节点是不是红黑树的节点
            //如果是,则生成一个红黑树的节点然后赋值给本地变量 e 
            //第一个节点是树节点,即属于红黑树冲突处理
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //不是红黑树,hash冲突,开始扩展链表
            else {
                //遍历p后面的链表  
                for (int binCount = 0; ; ++binCount) {
                	//e表示到p后面的节点,如果e为null,表示下一节点不存在,直接将新的key,value放在链表后面
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //放入后,判断链表长度是否到达红黑树的阈值8,大于等于则调用 treeifyBin()将链表转为红黑树
                        //treeifyBin首先判断当前hashMap的长度,如果不足64,只进行resize,扩容table,如果达到64,那么将冲突的存储结构为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果后面的节点不为空,表示p后面还有节点
                    //将e赋值给p(p的下一节点赋值给p),继续下一轮的循环
                    //如过有相同的key值,就结束遍历
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
             //e不等于null,则表示key值相等,替换原来的value即可
             //更新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;
            }
        }
        //完成put操作,修改次数+1
        ++modCount;
        //put新节点后,size+1
        //如果大于阈值(0.75*初始容量),扩容2倍
        if (++size > threshold)
            resize();
        //插入新节点后,回调方法
        afterNodeInsertion(evict);
        //插入新节点后,返回null
        return null;
    }

HashMap resize方法

final Node<K, V>[] resize() {
		//原来的数组
        Node<K, V>[] oldTab = table;
        //获得原来数组的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //获得扩容的阈值
        int oldThr = threshold;
        //定义新的数组长度,新的阈值
        int newCap, newThr = 0;
        
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            } 
            //将原来的数组长度 * 2 ,判断是否小于最大值,并且原来的数组长度大于默认初始长度(16)
            //直接双倍扩容
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; 
        	} 
        	//第一次初始化表
        	else if (oldThr > 0) 
            	newCap = oldThr;
       		else {               
	        	//第一次调用 resize方法,初始化数组长度,阈值,这里就对应我们前面成员变量的分析了:
	            //阈值 = 加载因子 * 数组长度
	            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;
        //开始将旧数组的长度复制到新数组
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K, V> e;
                if ((e = oldTab[j]) != null) {
                	//原数组的值先置换为null,帮助gc
                    oldTab[j] = null;
                    //如果节点的next为空(没有形成链表),直接赋值到新数组
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //如果节点的next不为空 ,但是已经是红黑树了,按照红黑树的规则来置换
                    else if (e instanceof TreeNode)
                        ((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                    //形成了链表,不再使用链表
                    else { // preserve order
                    	//声明四个引用,可以防止多线程环境下死循环
                        Node<K, V> loHead = null, loTail = null;
                        Node<K, V> hiHead = null, hiTail = null;
                        Node<K, V> next;
                        do {
                        //新表是旧表的2倍容量,把单链表拆成两队
                            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;
    }

HashMap get()

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
final Node<K,V> getNode(int hash, Object key) {
    	//声明本地变量,提高性能
        Node<K,V>[] tab; 
        Node<K,V> first, e; 
        int n;
        K k;
        //本地变量赋值,n是数组的长度
        //通过key的hash值计算出key在数组中的位置,取出该节点
        //如果不为空,表示key在数组中是存在的,接下去就是遍历
        ///找到插入的第一个node
        if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        	//从第一个node开始,如果hash值相等,key值相等,那么这个节点就是我们想要找的,直接返回。
            if (first.hash == hash && // always check first node
                ((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 {
                	//遍历链表后面的node,找到了key和hash值都相同的,返回
                    if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        //没找到,直接返回null
        return null;
    }

hash()

static final int hash(Object key) {
        int h;
        //把高16bit和低16bit异或,减少哈希冲突
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

HashMap与HashTable、ConcurrentHashMap区别

HashTable

  • 底层数组+链表实现,无论key还是value都不能为null;
  • 线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
  • 初始size为11,扩容:newsize = olesize*2+1
  • 计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length

HashMap:

  • 底层数组+链表实现,可以存储null键和null值
  • 线程不安全
  • 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
  • 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
  • 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
  • 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
  • 计算index方法:index = hash & (tab.length – 1)

ConcurrentHashMap:

  • 底层采用分段的数组+链表实现
  • 线程安全
  • 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
  • Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
  • 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
  • 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容