HashMap源码解析
来源:互联网 发布:淘宝卖家服务市场 编辑:程序博客网 时间:2024/05/02 00:58
一起来看下
定义:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
集成AbstractMap类,实现了Map、Cloneable/
常量定义:
<span style="white-space:pre"></span> <pre name="code" class="html">//存储数据的Entry数组,它的大小必须是2的幂transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; //map中保存的键值对的数量 transient int size; //需要调整大小的极限值 int threshold; //装载因子 final float loadFactor; //map修改的次数 transient int modCount; //默认的map大小 static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE; //哈希因子 transient int hashSeed = 0;
//默认初始大小 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; //默认的装载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // static final Entry<?,?>[] EMPTY_TABLE = {};在HashMap中,使用Entry这一对象来存储元素结构,它在Map接口中定义:
<pre name="code" class="html"> <span style="white-space:pre"></span> 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; threshold = initialCapacity; init(); } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } //构造方法很好理解,其中init()函数为空 //使用一个Map来构造新的map的构造函数 public HashMap(Map<? extends K, ? extends V> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); //扩张表 inflateTable(threshold); //将老的map中的元素全部放入到新的map之中 putAllForCreate(m); }
对构造方法分析下:构造方法一共四个,第一个也就是主要用的,它的参数传入了两个参数,初始容量和负载因子;并且将扩展阈值的大小变为初始容量;最后一个构造函数,使用一个Map对象作为参数,来构建一个新的Map;
这个函数里面有两个新的函数,分别是inflateTable和putAllForCreate,下来看看实现:
private void inflateTable(int toSize) { // 前面提到了,table的长度一定是2的幂,这个函数是计算大于且最接近toSize的数的;这里是将容量扩大到大于toSize的最小的2的幂 int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new Entry[capacity]; //初始化哈希的掩码值 initHashSeedAsNeeded(capacity); }在inflateTable里面,我们看见一个roundUpToPowerOf2()的函数,它的作用我在上面已经谢了,看下具体实现:
//此函数返回大于等于最接近number的2的冪数:如果number>MAXIMUM_CAPACITY,返回MAXIMUM_CAPACITY;Integer.highestOneBit(number)返回小于等于最接近number的2的冪数,比如5是101,对5调用次函数,返回1000//Integer.bitCount()是返回number中2进制中1的个数;因为number的最高位为1,所以当二进制中1的个数多余1,就说明最number大鱼Integer.highestOneBit(number),小于这个数字的2倍;因此让他扩大一倍,就是最接近大于等于number的数字了 private static int roundUpToPowerOf2(int number) { // assert number >= 0 : "number must be non-negative"; int rounded = number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (rounded = Integer.highestOneBit(number)) != 0 ? (Integer.bitCount(number) > 1) ? rounded << 1 : rounded : 1; return rounded; }
private void putAllForCreate(Map<? extends K, ? extends V> m) { for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) putForCreate(e.getKey(), e.getValue()); }
对旧map中的每一个元素进行putForCreate()的操作,
private void putForCreate(K key, V value) { int hash = null == key ? 0 : hash(key); int i = indexFor(hash, table.length); /** * <span style="font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 13.9200000762939px; line-height: 20.8800010681152px;">该方法先计算需要添加的元素的hash值和在table数组中的索引i。接着遍历table[i]的链表,若有元素的key值与传入key值相等,则替换value,结束方法。若不存在key值相同的元素,则调用createEntry创建并添加元素。</span> */ for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { e.value = value; return; } } createEntry(hash, key, value, i); }
第一步显示计算hash的值,如果为返回0,否则根据hash函数返回一个值;看下Hash()函数:
// 这个方法的主要作用是防止质量较差的哈希函数带来过多的冲突(碰撞)问题。对hashCode再次哈希的原因是减少哈希冲突 final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
下来,根据hash的值,找到在table中的位置:indexFor()函数:
static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1); }这里,它通过 h & (table.length -1) 来得到该对象的保存位,而HashMap底层数组的长度总是 2 的n 次方,这是HashMap在速度上的优化。
当length总是 2 的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
这看上去很简单,其实比较有玄机的,我们举个例子来说明:
假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下:
h & (table.length-1) hash table.length-1
8 & (15-1): 0100 & 1110 = 0100
9 & (15-1): 0101 & 1110 = 0100
-----------------------------------------------------------------------------------------------------------------------
8 & (16-1): 0100 & 1111 = 0100
9 & (16-1): 0101 & 1111 = 0101
从上面的例子中可以看出:当它们和15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hash值会与15-1(1110)进行“与”,那么 最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!而当数组长度为16时,即为2的n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
最后的创建新的Entry对象函数:
void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
到此,与HashMap相关的方法就已经分析完毕,下来看下HashMap常用的几个方法。
常用方法:
首先,看下HashMap中的put方法:
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
put函数先判断table是否为空表,如果是空表则先扩张整个表,inflateTable上面已经写过~~;然后判断key的值是不是为null,如果为null,存key为null的entry元素;否则,找出其hash值和在table中的下标,然后判断将存的元素的key值时候已经在map中有,如果存在,需改value值,返回此entry~,如果没有,添加新的,返回null;
private V putForNullKey(V value) {
<span style="white-space:pre"></span>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; } } modCount++; addEntry(0, null, value, 0); return null; }
这个函数先遍历table,如果table中有entry的key为null,则修改它的value,否则,创建新的entry添加到table中~~其中e.recordAccess方法的作用记录当调用put函数时,所存的entry元素的key已经存在,覆盖value的时间,这个函数是个空函数~~。
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
这个函数是添加新的entry的,再添加的时候,我们肯定会遇到这样一个情况,如果table的大小已经满了,且带添加的这个key需要新的table空间,则需要扩展原有的table了;这里判断如果大小大于或者等于阈值且当前添加的元素部位null,扩充table,调用resize()函数;
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
resize函数先判断原来table的大小,如果达到最大值,不再扩充,阈值设为最大值;否则,将table的长度翻倍,再讲以前的元素全部放入到新的table中:
<pre name="code" class="html"> void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next;//这里又将链表倒序了一次。 } } }
<span style="font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 13.9200000762939px; line-height: 20.8800010681152px;"> 从上面的代码可以看出,HashMap之所以不能保持元素的顺序有以下几点原因:第一,插入元素的时候对元素进行哈希处理,不同元素分配到table的不同位置;第二,容量拓展的时候又进行了hash处理;第三,复制原表内容的时候链表被倒置。</span>
在来看看get方法:
public V get(Object key) { if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); }
首先判断key是不是为null,如果为null,去找到key为null的entry,否则,根据key的值去找,函数很好理解~
private V getForNullKey() { if (size == 0) { return null; } for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; }
再来看看删除remove();
public V remove(Object key) { Entry<K,V> e = removeEntryForKey(key); return (e == null ? null : e.value); }
final Entry<K,V> removeEntryForKey(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev; while (e != null) { Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; }上面的这个过程就是先找到table数组中对应的索引,接着就类似于一般的链表的删除操作,而且是单向链表删除节点,很简单。在C语言中就是修改指针,这个例子中就是将要删除节点的前一节点的next指向删除被删除节点的next即可。
在看看clear方法:
public void clear() { modCount++; Arrays.fill(table, null); size = 0; }
直接将所有的元素变为null;
在看看两个新增的函数(相比起hashtable)
containskey()
public boolean containsKey(Object key) { return getEntry(key) != null; }直接利用getEntry函数进行判断
containsValue()
private boolean containsNullValue() { Entry[] tab = table; for (int i = 0; i < tab.length ; i++) for (Entry e = tab[i] ; e != null ; e = e.next) if (e.value == null) return true; return false; }
没什么好说的~~
其余的基本上都是很少用的api了,大家可以自己分析分析;
我们都知道hatshable与hashmap的区别一个是同步的,另一个是不同步的,但是hashmap还有一个ConcurrentHashMap是同步的,他和hashtable有什么区别呢?下来分析hashtable与ConcurrentHashMap
- Android源码解析 -- HashMap
- HashMap源码解析
- HashMap源码解析
- HashMap源码解析
- HashMap源码解析
- HashMap源码解析
- HashMap源码解析
- Java HashMap 源码解析
- Java HashMap 源码解析
- Java HashMap 源码解析
- 源码解析HashMap
- Java:HashMap源码解析
- HashMap 源码解析
- HashMap源码解析
- HashMap 源码解析
- Java源码解析-hashmap
- HashMap源码解析
- HashMap源码解析
- 黑马程序员——Android程序运行过程
- 关键字:android,android开发,下划线,中划线
- Leetcode26-Remove Duplicates from Sorted Array
- Fields属性查询
- Cygwin NDK 安装配置详解
- HashMap源码解析
- 读懂卷积神经网络CNN
- 九九乘法表
- 1、SVN学习之windows下svn的安装
- [leetcode]18 Min Stack
- 工作杂谈:工作方法
- Android中监听点击事件----学习笔记
- Android09_Activity及其生命周期
- Git分支