一.hashMap实现原理分析
- HashMap底层实现方式:
散列链表
,即数组+链表 -
hash表
:也叫散列表
,是通过关键码值(key-value)
而直接进行访问的数据结构
。
- 原理:
通过把关键码值映射到表中一个位置来访问记录
- 好处:这种数据结构可以加快查找的速度。
这里就需要将几种数据结构的优缺点进行比较了。
数组
优点:查询快,随机访问快
缺点:插入,删除慢链表
优点:插入,删除慢
缺点:随机访问慢-
hash表:通过索引的方式使得查询的时候提高查询的效率
优点:插入快,删除快,查找快
缺点:基于数组的,数组创建后难于拓展,当hash表被基本填满后,性能下降非常严重。因此在使用hash表的时候需要提前预估
表中要存多少个数据,避免扩展。/** * hashMap是将key-value看成一个数据实体entry来存储的,而且每个实体的索引值只与key有关,与value无关 */ public class HashMap<K, V> { //hashMap长度 private int size; //数组最小长度 private static final int MINIMUM_CAPACITY = 4; //数组最大长度 private static final int MAXIMUM_CAPACITY = 1 << 30; //空表的数组长度为2,意味着一建立就要强制扩容,这个是自己指定的 private static final Map.Entry[] EMPTY_TABLE = new HashMapEntry[MINIMUM_CAPACITY >> 1]; //装填因子,即数组扩容的阈值 private int threshold; private HashMapEntry<K, V>[] table;//核心数组 //在hashmap中允许key为空,而且只能有一个,并且单独存储,没有存储在核心数组里面 private HashMapEntry<K, V> entryForNullKey; @SuppressWarnings("unchecked") public HashMap() {//空参构造函数 table = (HashMapEntry<K, V>[]) EMPTY_TABLE; threshold = -1; } @SuppressWarnings("unchecked") public HashMap(int capacity) { if (capacity < 0) { throw new IllegalArgumentException(); } if (capacity == 0) { table = (HashMapEntry<K, V>[]) EMPTY_TABLE; threshold = -1; return; } //如果传进来的数组参数比最小数组长度还小,那数组长度就为最小长度 if (capacity < MINIMUM_CAPACITY) { capacity = MINIMUM_CAPACITY; } else if (capacity > MAXIMUM_CAPACITY) { capacity = MAXIMUM_CAPACITY; } else { capacity = roundUp2PowerOf2(capacity); } makeTable(capacity); } /** * 插入一个元素 * @param key * @param value * @return */ public V put(K key, V value) { if (key == null) { return putValueForNullKey(value); } else { /** * 如果键值对不为空,则先要找到key-value的位置 * 1.先生成key的hash值 * 2.根据hash值找到数组的角标 * 3.根据角标插入对应的位置 */ //键的hash值 int hash = secondaryHash(key.hashCode()); HashMapEntry<K, V>[] tab = table; /** * 得到数组的角标 * &运算的作用:0~min(a,b),可以取到最小值的最大值 */ int index = hash & (tab.length - 1); //如果存在指定的元素则覆盖,不存在则插入 //对羊肉串进行遍历 for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) { /** * key与hash值之间的关系 * key相同,hash值一定相同;反之,hash值相同,key不一定相同 */ if (e.hash == hash && key.equals(e.key)) { //同一个元素,则覆盖 V oldValue = e.getValue(); e.setValue(value); return oldValue; } } //没有覆盖,直接插入元素,但不一定是在数组中增加一个元素,可能是在某个羊肉串后面添加一个节点 /** * @see makeTable(int capacity) * @link{makeTable(int capacity) } * 虽然阈值定义的是capacity的3/4,但是capacity是个正数,所以threshold一个整数 */ if (size++ > threshold) { //假如一个元素插入成功,但是超过了阈值,就需要扩容 /** * 1.创建一个新的容量的数组 */ tab = doubleCapacity(); index = hash & (tab.length - 1); } addNewEntry(key, value, hash, index); } return value; } public V get(Object key){ if (key == null) { HashMapEntry<K,V> e=entryForNullKey; return e==null?null:e.getValue(); }else { int hash=secondaryHash(key.hashCode()); HashMapEntry<K,V>[] tab=table; int index=hash&(tab.length-1); for (HashMapEntry<K,V> e=tab[index];e!=null;e=e.next){ K eKey = e.key; if (eKey==key||(e.hash==hash&&key.equals(eKey))) { return e.value; } } return null; } } /** * 将key-value键值对添加到index的位置 * @param key * @param value * @param hash * @param index */ private void addNewEntry(K key, V value, int hash, int index) { //添加到队头的位置 table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]); } /** * 双倍扩容 * @return */ @SuppressWarnings("unchecked") private HashMapEntry<K, V>[] doubleCapacity() { HashMapEntry<K, V>[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { return oldTable; } else { int newCapacity = oldCapacity << 1; // TODO: 这里是否要判断newCapacity>MAXIMUM_CAPACITY? if (newCapacity >= MAXIMUM_CAPACITY) { newCapacity = MAXIMUM_CAPACITY; } //这里不能采用new的方式,因为这样就与旧的table产生不了联系 HashMapEntry<K, V>[] newTable = makeTable(newCapacity); /** * 双倍扩容时,表有可能是空表,即 * @link{<code>EMPTY_TABLE</code>}, 此时数组长度为4,但是size为0 */ if (size == 0) { return newTable; } /** * 双倍扩容之后,需要重新散列 */ for (int j = 0; j < oldTable.length; j++) { //拿到每个羊肉串 HashMapEntry<K, V> e = oldTable[j]; if (e == null) { continue; } /** * e.hash & oldCapacity <===> e.hash & oldTable.length===>结果的范围:[0,oldTable.length] * 对比: * int index = hash & (tab.length - 1)===>结果的范围:[0,oldTable.length-1] * “|”的作用:2*max(a,b)> a|b >max(a,b) */ int highBit = e.hash & oldCapacity;//0-oldTable.length //断链标志位 HashMapEntry<K, V> broken = null; newTable[j | highBit] = e;//重新散列成功oldTable.length-2*oldTable.length-1 /** * e=n: 每次指针后移的时候,都保留当前结点的前一个元素 **/ for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) { int nextHighBit = n.hash & oldCapacity; if (nextHighBit != highBit) { if (broken == null) { int nextNewIndex = j | nextHighBit;//新的索引 newTable[nextNewIndex] = n; } else { broken.next = n; } broken = e; highBit = nextHighBit; } if (broken != null) { broken.next = null; } } } return newTable; } } /** * hashMap 键的hash算法 * 可以看到:对键进行单独的hash运算 * @param h key的hash值 * @return 算法的意义:目的就是为了将h打散,而且在0到max 之间均匀分布 * 异或算法的作用:补位,多样化 */ private int secondaryHash(int h) { //拿到高12位, 拿到高20位 h ^= (h >>> 20) ^ (h >>> 12); // 拿到自己 拿到高25位, 拿到高28位 return h ^ (h >>> 7) ^ (h >>> 4); } /** * 给放空key的键值对赋值 * @param value * @return */ private V putValueForNullKey(V value) { HashMapEntry<K, V> newEntry = entryForNullKey; //如果空键的键值对不存在,则重新创建 if (newEntry == null) { addEntryForNullKey(value); //虽然空键键值对单独存放,但是也是hashmap集合中的一个元素,所以要size++ size++; return null; } else { V oldValue = newEntry.getValue(); newEntry.setValue(value); return oldValue; } } /** * 创建空键键值对,hash值只与key有关,键为null的键值对单独存放,所以next为空 * @param value */ private void addEntryForNullKey(V value) { entryForNullKey = new HashMapEntry<>(null, value, 0, null); } /** * 根据容量创建核心数组 * @param capacity */ private HashMapEntry<K, V>[] makeTable(int capacity) { //table=new HashMapEntry[capacity];//不要这么写 HashMapEntry<K, V>[] newTable = new HashMapEntry[capacity]; table = newTable;//尽量不要操作全局变量,把全局变量转成局部变量,通过操作局部变量达到操作全局变量的目的,防止bug threshold = (capacity >> 1) + (capacity >> 2);//3/4; return newTable; } /** * 将任意一个数转换成2的幂次方 * 是2的幂次方的数的特点: * 2 =10=1+1 * 4 =100=11+1 * 8 =1000=111+1 * 16=10000=1111+1 * 32=100000=11111+1 * <p> * @param i * @return */ private int roundUp2PowerOf2(int i) { i--; i = i >>> 1; i = i >>> 2; i = i >>> 4; i = i >>> 8; i = i >>> 16; return i; } /** * 1. 成员变量可以是任意数据类型,基本数据类型,引用数据类型,类就属于引用数据类型, * 因此内部类的另一种理解方式就是将其看成外部类的成员变量, * <code>成员变量</code>也是<code>全局变量</code>,是整个类中都要使用的变量 * 成员变量表示一个类的属性,这个属性自然在这个类中都成立,自然全局可用 * <p> * 2.hashMap是将key-value看成一个数据实体entry来存储的,而且每个实体的索引值只与key有关,与value无关 * 因此在entry中需要一个构造函数将key和value封装成entry */ private static class HashMapEntry<K, V> implements Map.Entry<K, V> { //每个键值对的键是唯一的,一旦被赋值,就不能被修改了,通过final修饰来达到这个目的 final K key; V value; final int hash;//hash是有key有关的hash算法生成的,因此也不能被被外界修改 HashMapEntry<K, V> next;//指向下一个节点的指针 public HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) { this.key = key; this.value = value; this.hash = hash; this.next = next; } @Override public K getKey() { return key; } @Override public V getValue() { return value; } @Override public V setValue(V value) { V oldValue = this.value; this.value = value; return oldValue; } /** * 任何一个对象都有hashcode方法,这个是从Object类继承过来的,hash算法是自己指定的 * @return */ @Override public int hashCode() { return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode()); } } }