在Java的集合中Map接口的实现实例中用的比较多的就是HashMap,今天我们一起来学学HashMap,顺便学学和他有关联的HashTable、HashTree
一、HashMap
1、基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
2、HashMap 的实例有两个参数影响其性能:初始容量 和加载因子。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行rehash
操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
3、HashMap底层是哈希表实现(格式像数组链表的组合),当创建一个HashMap对象的时候创建Hash表,哈希表的容量就是哈希中桶的个数,如果在创建对象的时候指定了容量则创建的哈希表的容量就是桶的个数,而这个桶的个数就是初始时指定的容量。
如果在创建的时候没有指定初始容量则使用默认值: 默认值为 16
<span style="font-family:SimSun;font-size:18px;"> /** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 16;</span>
4、不管是创建时指定了容量还是使用默认的容量,这个值不等于存储对象的个数,因为在开始就说了他是基于数组和链表实现的而且还有加载因子,所以容量不等于存储对象个数。
5、在2中已经说了影响实例性能的两个因素,所以在创建实例的时候我们要按照自己的需求来设置这两个值,当空间大而对查询效率要求高的时候可以将初始容量设置的大一些,而加载因子小一些这样的话查询效率高,但空间利用率不高,而当空间比较小而效率要求不是很高的时候可以将初始容量设置小一些而加载因子设置大一些,这样查询速度会慢一些而空间利用率会高一些,这就是因为HashMap底层使用的是数组和链表的实现方式,具体的分析看下面内容。
6、哈希表结构:
7、按照key关键字的哈希值和buckets数组的长度取模查找桶的位置,如果key的哈希值相同,Hash冲突(也就是指向了同一个桶)则每次新添加的作为头节点,而最先添加的在表尾。
8、HashMap中的桶的个数就是下图中的0- n的数组的长度,存储第一个entry的位置叫‘桶(bucket)’而桶中只能存一个值也就是链表的头节点,链表的每个节点就是添加的一个值(HashMap内部类Entry的实例Entry有哪些属性之后在详说),也可以这样理解,一个entry 类型的存储链表的数组。数组的索引位置就是一个个桶的索引地址。
9、通过6、7两张图我们了解了哈希表的结构,从两张图也可以看出他的这种格式像是链表的数组。
10、从上图我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。
11、HashMap其实也是一个线性的数组实现的,所以可以理解为其存储数据的容器就是一个线性数组。这可能让我们很不解,一个线性的数组怎么实现按键值对来存取数据呢?这里HashMap有做一些处理。
首先HashMap里面实现一个静态内部类Entry,其重要的属性有key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[]类型的数组,Map里面的内容都保存在Entry[]里面。
Ha<span style="font-family:SimSun;">shMap类源码:</span> public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { /** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 16; /** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * The table, resized as necessary. Length MUST Always be a power of two. <span style="font-family:SimSun;"> *调整表是必要的而且长度必须是2的幂。 * 定义了一个Entry[] 类型的数组和上面的理解吻合 </span> */ <span style="color:#CC0000;"> transient Entry<K,V>[] table;</span>
HashMap类构造函数源码:
/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and load factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive * <span style="font-family:SimSun;">初始容量和负载</span>因子进行初始化Ha<span style="font-family:SimSun;">shMap对象</span> <span style="font-family:SimSun;"> </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); // Find a power of 2 >= initialCapacity int capacity = 1; while (capacity < initialCapacity) <span style="font-family:SimSun;"> // 使用位移运算效率高 </span> <span style="color:#CC0000;">capacity <<= 1;</span> this.loadFactor = loadFactor; threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); <span style="color:#990000;"> <span style="font-family:SimSun;">// 创建Entry[]类型的数组</span> <span style="font-family:SimSun;"> </span> table = new Entry[capacity];</span> useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); init(); }
12、HashMap--put:
疑问思考:如果两个key通过hash%Entry[].length得到的index相同,会不会有覆盖的危险?
这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的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这个属性链接在一起。所以疑问不用担心。也就是说数组(桶)中存储的是最后插入的元素。如果hash%Entry[].length得到的index相同相同而且key.equals(keyother) 也相同,则这个Key对应的value会被替换成新值。
public V put(K key, V value) { if (key == null) <pre name="code" class="html"> <span style="color:#CC0000;"> //null总是放在数组的第一个链表中,也就是上面说的桶中</span>
return putForNullKey(value);
// 获取key的哈希值
int hash = hash(key.hashCode());
// 通过key的哈希值和table的长度取模确定‘桶’(bucket)的位置
int i = indexFor(hash, table.length); //遍历链表 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k;//如果key映射的entry在链表中已存在,则entry的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; } } modCount++;
addEntry(hash, key, value, i); return null; }
Entry内部类:
static class Entry<K,V> implements Map.Entry<K,V> { // 添加的kye关键字 final K key; // 添加的value值 V value; // Entry对象 指向的下一个Entry对象 Entry<K,V> next; // key关键字的hash值 int hash; /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }
addEntry(hash,key,value,i)方法:
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; <pre name="code" class="html"> //参数e, 是Entry.next,指向下一个节点也就是在他之前添加的那个entry, //将新创建的Entry放到bucketIndex索引处,并让新的Entry指向原来的Entry
table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //如果size超过threshold,则扩充table大小。再散列 if (size++ >= threshold)
// table 对象扩展为原来的两倍
resize(2 * table.length);}
HashMap里面也包含一些优化方面的实现,这里也说一下。比如:Entry[]的长度一定后,随着map里面数据的越来越长,这样key的哈希值冲突的概率也就越大同一个index的链就会很长,会不会影响性能?HashMap里面设置一个因子(负载因子),随着map的size越来越大,Entry[]会以一定的规则加长长度。
resize 扩容:当哈希表的容量超过默认容量时,必须调整table的大小。当容量已经达到最大可能值时,那么该方法就将容量调整到Integer.MAX_VALUE返回,这时,需要创建一张新表,将原表的映射到新表中。
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]; boolean oldAltHashing = useAltHashing; useAltHashing |= sun.misc.VM.isBooted() && (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); boolean rehash = oldAltHashing ^ useAltHashing; // 数据的拷贝 <span style="color:#CC0000;">transfer(newTable, rehash);</span> table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
transfer 方法进行哈希表的重建,并重建链表
/** * Transfers all entries from current table to newTable. */ 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; } } }
13、HashMap-get
public V get(Object key) { if (key == null) return <span style="color:#FF6600;">getForNullKey()</span>; Entry<K,V> entry =<span style="color:#FF6600;"> getEntry(key);</span> return null == entry ? null : entry.getValue(); }
getForNullKey() 获取key为null的value值:
/** * Offloaded version of<span style="color:#FF6600;"> get() to look up null keys. Null keys map * to index 0.</span> This null case is split out into separate methods * for the sake of performance in the two most commonly used * operations (get and put), but incorporated with conditionals in * others. * key为null的话获取的entry 就是index为0 值 */ private V getForNullKey() { for (Entry<K,V> e =<span style="color:#FF6600;"> table[0]</span>; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
getEntry(key)方法 : 获取key对应的entry 对象,如果HashMap不包含关键字为key的则映射返回null
/** * Returns the entry associated with the specified key in the * HashMap. Returns null if the HashMap contains no mapping * for the key. */ final Entry<K,V> getEntry(Object key) { // 获取key的哈希值 int hash = (key == null) ? 0 :<span style="color:#FF6600;"> hash(key);</span> // 通过key的哈希值确定数组的index位置 (桶的位置) for (Entry<K,V> e = table[<span style="color:#FF6600;">indexFor</span>(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; }
获取key的哈希值:
final int hash(Object k) { int h = 0; if (useAltHashing) { if (k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h = hashSeed; } h ^= k.hashCode(); // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
确定数组的index:hashcode % table.length取模
HashMap存取时,都需要计算当前key应该对应Entry[]数组哪个元素,即计算数组下标,计算方法如下:
/** * Returns index for hash code h. * 返回h这个hashcode的index位置 */ static int indexFor(int h, int length) { return h & (length-1); }
简单总结:
1、HashMap 是链式数组(存储链表的数组)实现查询速度可以,而且能快速的获取key对应的value;
2、查询速度的影响因素有 容量和负载因子,容量大负载因子小查询速度快但浪费空间,反之则相反;
3、数组的index值是(key 关键字, hashcode为key的哈希值, len 数组的大小):hashcode%len的值来确定,如果容量大负载因子小则index相同(index相同也就是指向了同一个桶)的概率小,链表长度小则查询速度快,反之index相同的概率大链表比较长查询速度慢。
4、对于HashMap以及其子类来说,他们是采用hash算法来决定集合中元素的存储位置,当初始化HashMap的时候系统会创建一个长度为capacity的Entry数组,这个数组里可以存储元素的位置称为桶(bucket),每一个桶都有其指定索引,系统可以根据索引快速访问该桶中存储的元素。
5、无论何时HashMap 中的每个桶都只存储一个元素(Entry 对象)。由于Entry对象可以包含一个引用变量用于指向下一个Entry,因此可能出现HashMap 的桶(bucket)中只有一个Entry,但这个Entry指向另一个Entry 这样就形成了一个Entry 链。
6、通过上面的源码发现HashMap在底层将key_value对当成一个整体进行处理(Entry 对象)这个整体就是一个Entry对象,当系统决定存储HashMap中的key_value对时,完全没有考虑Entry中的value,而仅仅是根据key的hash值来决定每个Entry的存储位置。
二、HashTree
1、有时间了补充完整 先休息 ……