在Java的集合中Map接口的实现实例中用的比較多的就是HashMap。今天我们一起来学学HashMap,顺便学学和他有关联的HashTable、TreeMap
在写文章的时候各种问题搞得我有点迷糊尤其是csdn中放的java代码显示了乱七八糟的东西搞得 写了两次,可能有些东西写错了…… 希望大家指正
一、HashMap
1、基于哈希表的 Map 接口的实现。此实现提供全部可选的映射操作,并同意使用 null 值和 null 键。
(除了非同步和同意使用 null 之外,HashMap 类与 Hashtable 大致同样。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
2、HashMap 的实例有两个參数影响其性能:初始容量 和载入因子。容量是哈希表中桶的数量。初始容量仅仅是哈希表在创建时的容量。载入因子是哈希表在其容量自己主动添加之前能够达到多满的一种尺度。当哈希表中的条目数超出了载入因子与当前容量的乘积时,则要对该哈希表进行rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
3、HashMap底层是哈希表实现(格式像数组链表的组合)。当创建一个HashMap对象的时候创建Hash表,哈希表的容量就是哈希中桶的个数,假设在创建对象的时候指定了容量则创建的哈希表的容量就是桶的个数,而这个桶的个数就是一个比指定容量小的最大值,也就是最接近指定的容量的那个数并且这个数是2的n次幂。
为什么桶的个数不是指定的容量的大小而是比这个小,这个看以下的源代码就明确了
假设在创建的时候没有指定初始容量则使用默认值: 默认值为 16,容量的值是2的n次幂,负载因子默觉得0.75
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 16;
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
5、在2中已经说了影响实例性能的两个因素,所以在创建实例的时候我们要依照自己的需求来设置这两个值。当空间大而对查询效率要求高的时候能够将初始容量设置的大一些。而载入因子小一些这种话查询效率高。但空间利用率不高,而当空间比較小而效率要求不是非常高的时候能够将初始容量设置小一些而载入因子设置大一些,这样查询速度会慢一些而空间利用率会高一些,这就是由于HashMap底层使用的是数组和链表的实现方式,详细的分析看以下内容。
6、哈希表结构:
7、依照keykeyword的哈希值和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[]里面。
12、HashMap类源代码:
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. * 默认的容量必须为2的幂 */ 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. * 到这里就发现了,HashMap就是一个Entry[]类型的数组了。 */ transient Entry<K,V>[] table;
13、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 */ // 初始容量(必须是2的n次幂),负载因子 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; // 获取最小于initialCapacity的最大值,这个值是2的n次幂,所以我们定义初始容量的时候尽量写2的幂 while (capacity < initialCapacity) // 使用位移计算效率更高 capacity <<= 1; this.loadFactor = loadFactor; //哈希表的最大容量的计算,取两个值中小的一个 threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); //创建容量为capacity的Entry[]类型的数组 table = new Entry[capacity]; useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); init(); }
14、HashMap--put:
这里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会被替换成新值。
15、Put方法:
public V put(K key, V value) {
//key为null的entry总是放在数组的头节点上,也就是上面说的"桶"中
if (key == null)
return putForNullKey(value);
// 获取key的哈希值
int hash = hash(key);
// 通过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;
}
16、Entry内部类:
static class Entry<K,V> implements Map.Entry<K,V> {
// keywordkey
final K key;
// keywordkey所相应的value值
V value;
// 这个Entry 对象名称为next ,看到这个大体明确了他就是指向下一个节点即指向下一个Entry对象
// entry 链表的构成也是这个属性
Entry<K,V> next;
// keykeyword的哈希值
int hash;
/**
* Creates new entry.
* 构造函数创建一个Entry 对象
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
17、addEntry(hash,key,value,i)方法:
//bucketIndex 桶的索引值,桶中仅仅能存储一个值(一个Entry 对象)也就是头节点 void addEntry(int hash, K key, V value, int bucketIndex) { // 假设数组中存储的元素个数大于数组的临界值(这个临界值就是 数组长度*负载因子的值 )则进行扩容 if ((size >= threshold) && (null != table[bucketIndex])) { // 扩容,将大小扩为原来的两倍 resize(2 * table.length); hash = (null != key) ?18、调用addEntry(hash,key,value,i)方法时假设size大于临界值threshold则首先调用resize 方法:hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
当哈希表的容量超过默认容量时,必须调整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; // 又一次进行散列 transfer(newTable, rehash); table = newTable; // 临界值又一次赋值 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
19、常量MAXIMUM_CAPACITY:
/** * 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;
20、当哈希表建好后调用transfer (Entry[] newTable,boolean rehash)方法将原有的数据进行又一次散列
/** * Transfers all entries from current table to newTable. * 将全部entry对象从当前表拷贝到NewTable */ void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; //table 就是一个Entry<K,V>[]类型的数组 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; } } }
21、调用createEntry方法时创建一个Entry对象并将其加入到index 0(“桶”)的位置
/** * Like addEntry except that this version is used when creating entries * as part of Map construction or "pseudo-construction" (cloning, * deserialization). This version needn't worry about resizing the table. * * Subclass overrides this to alter the behavior of HashMap(Map), * clone, and readObject. */ void createEntry(int hash, K key, V value, int bucketIndex) { // 将原来的首节点保存到e变量中 Entry<K,V> e = table[bucketIndex]; // 将新加入的这个节点保存到首节点并且这个节点指向之前的节点 table[bucketIndex] = new Entry<>(hash, key, value, e); // 元素个数加 1 size++; }HashMap里面也包括一些优化方面的实现。这里也说一下。比方:Entry[]的长度一定后。随着map里面数据的越来越长。这样key的哈希值冲突的概率也就越大同一个index的链就会非常长。会不会影响性能?HashMap里面设置一个因子(负载因子),随着map的size越来越大,Entry[]会以一定的规则加长长度。
22、HashMap-get:
public V get(Object key) { // map中能够存储key value 为null // 这个和put相应在put的时候假设key为null则放在“桶中”即头节点 if (key == null) // 相同取得时候假设key为null则取“桶位置的值” return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ?null : entry.getValue(); }
23、getForNullKey() 获取key为null的value值:
/** * Offloaded version of get() to look up null keys. Null keys map * to index 0. 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. */ private V getForNullKey() { // 通过这个循环知道key为null的时候插叙的就是Index为0的地方的值(桶) for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
24、getEntry(key)方法 : 获取key相应的entry 对象,假设HashMap不包括keyword为key的则映射返回null
final Entry<K,V> getEntry(Object key) { //获取key的哈希值 int hash = (key == null) ?0 : hash(key); //通过key的哈希值以及table.length 来确定index的值(桶的索引) 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; }
25、确定数组的index(桶的索引):hashcode % table.length取模
HashMap存取时,都须要计算当前key应该相应Entry[]数组哪个元素。即计算数组下标。计算方法例如以下:
/** * Returns index for hash code h. *返回h这个hashcode的index0的位置(桶的位置)
*/ static int indexFor(int h, int length) { return h & (length-1); }
简单总结:
1、HashMap 是链式数组(存储链表的数组)实现查询速度能够。并且能高速的获取key相应的value;
2、查询速度的影响因素有 容量和负载因子,容量大负载因子小查询速度快但浪费空间,反之则相反。
3、数组的index值是(key keyword, 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的存储位置。
二、TreeMap
红黑树是一种自平衡二叉查找树。既然是二叉树则会满足二叉树的规定。
树中的每一个节点的值,都会大于或等于它的左子树种的全部节点的值。而且小于或等于它的右子树中的全部节点的值。
2、TreeMap的底层使用了红黑树来实现,像TreeMap对象中放入一个key-value 键值对时。就会生成一个Entry对象。这个对象就是红黑树的一个节点,事实上这个和HashMap是一样的。一个Entry对象作为一个节点。仅仅是这些节点存放的方式不同。
3、存放每个Entry对象时都会依照key键的大小依照二叉树的规范进行存放,所以TreeMap中的数据是依照key从小到大排序的。
4、TreeMap类源代码:
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable { /** * The comparator used to maintain order in this tree map, or * null if it uses the natural ordering of its keys. * * @serial */ private final Comparator<? super K> comparator; // 根节点 private transient Entry<K,V> root = null; /** * The number of entries in the tree * 树中的节点数,即entry对象的个数 */ private transient int size = 0; /** * The number of structural modifications to the tree. * 树改动的次数 */ private transient int modCount = 0;
5、TreeMap的内部类Entry<K k,V v>即一个节点:
static final class Entry<K,V> implements Map.Entry<K,V> { // keywordkey 依照key的哈希值来存放 K key; // key相应的value值 V value; // 左节点 Entry<K,V> left = null; // 右节点 Entry<K,V> right = null; // 父节点 Entry<K,V> parent; boolean color = BLACK; /** * Make a new cell with given key, value, and parent, and with * {@code null} child links, and BLACK color. */ Entry(K key, V value, Entry<K,V> parent) { this.key = key; this.value = value; this.parent = parent; } /** * Returns the key. * * @return the key */ public K getKey() { return key; } /** * Returns the value associated with the key. * * @return the value associated with the key */ public V getValue() { return value; } /** * Replaces the value currently associated with the key with the given * value. * * @return the value associated with the key before this method was * called */ public V setValue(V value) { V oldValue = this.value; this.value = value; return oldValue; } public boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<?,?> e = (Map.Entry<?,?
>)o; return valEquals(key,e.getKey()) && valEquals(value,e.getValue()); } }
6、put(K key,V value) 加入方法加入一个节点:
public V put(K key, V value) { Entry<K,V> t = root; //推断根节点是否存在,假设不存在 if (t == null) { compare(key, key); // type (and possibly null) check // 将新的key-value对创建一个Entry,并将该Entry作为root root = new Entry<>(key, value, null); // 计算节点数 size = 1; modCount++; return null; } int cmp; Entry<K,V> parent; // split comparator and comparable paths // 假设有根节点则,加入的key和root节点的key进行比較,推断是做左节点、右节点 Comparator<? super K> cpr = comparator; // 假设比較器cpr不为null,即表明採用定制排序方式 if (cpr != null) { //比較算法的開始,这里完毕了比較和存储 do { // 使用parent暂存上次循环后的t所相应的Entry。假设是首次则是root节点。7、TreeMap的get方法parent = t; // 新插入的key和当前节点(首次是root节点)t的key进行比較 cmp = cpr.compare(key, t.key); // 假设新插入的key的值小于t的key值,那么t=t.left即再用当前节点的左节点进行比較 if (cmp < 0) t = t.left; // 假设新插入的key的值大于t的key的值,那么t等于t的右节点即在用当前节点的右节点进行比較 else if (cmp > 0) t = t.right; else // 假设两个key的值相等。那么新的value覆盖原有的value,并返回原有的value return t.setValue(value); //假设t节点的左节点、右节点不为空则继续循环。知道null为止。这样也就找到了新加入key的parent节点。
} while (t != null); } else { if (key == null) throw new NullPointerException(); Comparable<? super K> k = (Comparable<? super K>) key; do { // 使用parent上次循环后的t所引用的Entry parent = t; // 拿新插入的key和t的key进行比較 cmp = k.compareTo(t.key); // 假设新插入的key小于t的key。那么t等于t的左节点 if (cmp < 0) t = t.left; // 假设新插入的key大于t的key。那么t等于t的右节点 else if (cmp > 0) t = t.right; else // 假设两个key相等,那么新的value覆盖原有的value。并返回原有的value return t.setValue(value); } while (t != null); } //新创建一个节点即put进来的key value Entry<K,V> e = new Entry<>(key, value, parent); // 假设新插入的key的值小于parent的key的值 则e作为parent的左子节点 if (cmp < 0) parent.left = e; // 假设新插入的key的值大于parent的key的值 则e作为parent的右子节点 else parent.right = e; // 修复红黑树 fixAfterInsertion(e); size++; modCount++; return null; }
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while {(p != null)
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
get和put的查找方式是一样的所以就不多说了!
注意这里我们没说红黑树仅仅是说了二叉树的查找,事实上TreeMap是使用红黑树的,所以有个fixAfterInsertion(e)方法这种方法就是保持红黑树的方法。
红黑树的维护能够看看这篇文章:http://www.cnblogs.com/skywang12345/p/3245399.html
总结:
程序加入新节点时,总是从树的根节点開始比較。即将根节点当成当前节点。
假设新增节点大于当前节点而且当前节点的右节点存在。则以右节点作为当前节点,假设新增节点小于当前节点而且当前节点的左子节点存在,则以左子节点作为当前节点;假设新增节点等于当前节点,则用新增节点覆盖当前节点,并结束循环 直到某个节点的左右子节点不存在。将新节点加入为该节点的子节点。假设新节点比该节点大,则加入其为右子节点。假设新节点比该节点小,则加入其为左子节点。