一、HashMap
1、HashMap基本概念
HashMap是基于哈希表的Map接口的非同步实现。HashMap存储的是key-value键值对,并允许key和value为null。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体,其继承了这两种数据结构的优点,实现了寻址容易,插入删除也比较容易。
HashMap的结构示意图:
注意:HashMap元素的保存顺序和插入顺序不同,特别是它不能保证插入元素的顺序恒久不变,因为在HashMap扩容后会重新计算每个元素的hashCode,并以此调整元素位置。
2、HashMap存储原理
HashMap内部维护了一个存储数据的Entry数组,另外设计一个哈希函数,根据每一个元素的Key(关键字)的hashCode值(key.hashCode(),Object类中包含hashCode方法,具体的类中可能会重写hashCode()方法)再重新计算一个hash值,hash和数组长度减一按位与就可以得到对应的Entry数组的下标,数组存储的元素是一个Entry类。
例如, 第一个键值对A进来。通过计算其key的hashCode得到的index=0。记做:Entry[0] = A。
第二个键值对B,通过计算其index也等于0, HashMap会将B.next =A,Entry[0] =B,
第三个键值对 C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方事实上存取了A,B,C三个键值对,它们通过next这个属性链接在一起。我们可以将这个地方称为桶。 对于不同的元素,可能计算出了相同的函数值,这样就产生了“冲突”,这就需要解决冲突,“直接定址”与“解决冲突”是哈希表的两大特点。
3、HashMap工作原理
HashMap是基于哈希法的原理,使用put(key, value)将对象存储到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,根据返回的hashCode调用hash()方法(HashMap类的里面有hash()方法)得到hash值,其用于找到bucket(桶)位置来储存Entry对象。HashMap是在bucket中储存Entry对象,并不是仅仅只在bucket中存储值。
下面分析一下put()和get()方法的源码:
先给出需要的hash()和indexFor()方法的源码:
//求hash值的方法,重新计算hash值 static int hash(int h) {//h是key的hashCode值 h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } // 返回h在数组中的索引值,这里用&代替取模,旨在提升效率 // h & (length-1)保证返回值的小于length static int indexFor(int h, int length) { return h & (length-1); }
put()方法源码:
// 将“key-value”添加到HashMap中 public V put(K key, V value) { // 若“key为null”,则将该键值对添加到table[0]中。 if (key == null) return putForNullKey(value); // 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。 int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; // 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出! if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 若“该key”对应的键值对不存在,则将“key-value”添加到table中 modCount++; //将key-value添加到table[i]处 addEntry(hash, key, value, i); return null; } // putForNullKey()的作用是将“key为null”键值对添加到table[0]位置 private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 如果没有存在key为null的键值对,则直接题阿见到table[0]处! modCount++; addEntry(0, null, value, 0); return null; }
put方法的过程:
1)、获取key;2)、根据key的hashCode值,通过hash函数计算得到hash值;对应的代码:int hash = hash(key.hashCode());(再哈希法,为了减少哈希冲突)
3)、通过indexFor()方法,得到桶号(Entry数组的下标)index,方法是将hash值与Entry数组长度减一按位与:hash & (table.length-1)。
4)、 存放key和value在桶内。table[index]=Entry对象;
综述:当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode值用于找到桶号来储存Entry对象。
get方法源码:
// 获取key对应的value public V get(Object key) { if (key == null) return getForNullKey(); // 获取key的hash值 int hash = hash(key.hashCode()); // 在“该hash值对应的链表”上查找“键值等于key”的元素 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; //判断key是否相同 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } //没找到则返回null return null; } // 获取“key为null”的元素的值 // HashMap将“key为null”的元素存储在table[0]位置,但不一定是该链表的第一个位置! private V getForNullKey() { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
get方法的过程:
1)、获取key ;
2)、根据key的hashCode值,通过hash函数计算得到hash值;对应的代码:int hash = hash(key.hashCode());(再哈希法)
3)、通过indexFor()方法,得到桶号(Entry数组的下标)index,方法是将hash值与Entry数组长度减一按位与: hash & (table.length-1)。(2、3两步与put方法相同)
4)、比较桶的内部元素是否与key相等,若都不相等,则没有找到。比较的时候遍历链表,依次与key比较。
5)、取出相等的记录的value。
综述:当我们给get()方法传递键时,我们先对键调用hashCode()方法,返回的hashCode值用于找到桶号,比较桶内元素的key是否与key相同,若相同,则取出对应的value;若都不相同,则没有找到。
4、HashMap的碰撞与解决方法
当两个不同的对象的hashCode相同时,它们的bucket位置相同,“碰撞”会发生。因为HashMap使用LinkedList存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在LinkedList中。
当我们调用get()方法,HashMap会使用键对象的hashCode找到bucket位置,然后遍历LinkedList直到找到值对象。找到bucket位置之后,会调用key.equals()方法去找到LinkedList中正确的节点。所以碰撞发生的话,会增加比较的次数。
5、HashMap的初始容量及扩容
HashMap内Entry数组的初始长度默认是16,当Entry数组的利用率超过一个阈值(加载因子)时,HashMap会进行扩容。
默认的加载因子大小为0.75,也就是说,当一个HashMap填满了75%的bucket时候,将会创建原来HashMap大小的两倍的bucket数组(HashMap大小永远都是2的整数幂),来重新调整HashMap的大小,并将原来的对象放入新的bucket数组中(会重新计算每个Entry对象的桶号)。
重新调整HashMap大小存在什么问题吗?在多线程的情况下,可能产生条件竞争。因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在LinkedList中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在LinkedList的尾部,而是放在头部,这是为了避免尾部遍历。如果条件竞争发生了,那么就死循环了(链表会形成一个环)。
二、Hashtable
1、基本概念
Hashtable是基于哈希表的Map接口的同步实现,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会进行扩容。Hashtable使用synchronized来保证线程安全,但在线程竞争激烈的情况下,Hashtable的效率非常低下。因为当一个线程访问Hashtable的同步方法时,其他线程访问Hashtable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。
2、Hashtable与HashMap的联系与区别
1、二者的存储结构和解决冲突的方法都是相同的。
2、Hashtable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
3、Hashtable中key和value都不允许为null,而HashMap中key和value都允许为null(key只能有一个为null,而value则可以有多个为null)。但是如果在Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常。
4、Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。
5、Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值,Hashtable在求hash值对应的位置索引时,用取模运算;而HashMap在求位置索引时,hash值与Entry数组长度减一做按位与,即index= hash & (length-1)。
三、ConcurrentHashMap
更新中。。。