Java中的Collection和Map(三)--Map体系
Map集合,即我们常用的key-Value 集合,Map以键值对的形式来存储数据,我们常用Map集合有:HashMap,TreeMap,WeakHashMap,EnumMap,LinkedHahMap,HashTable。他们都是以key-Value键值对形式存储数据。
1、HashMap
HashMap 底层采用Hash表结构来存储数据的。
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry[] table = (Entry[]) EMPTY_TABLE;
我们存储的数据就存放在Entry 类型的table数组中,Entry 是HashMap一内部类,他实现了接口Map.Entry
static class Entryimplements Map.Entry { final K key; V value; Entry next; int hash; /** * Creates new entry. */ Entry(int h, K k, V v, Entry 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; } public final boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry)o; Object k1 = getKey(); Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } public final int hashCode() { return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); } public final String toString() { return getKey() + "=" + getValue(); } /** * This method is invoked whenever the value in an entry is * overwritten by an invocation of put(k,v) for a key k that's already * in the HashMap. */ void recordAccess(HashMap m) { } /** * This method is invoked whenever the entry is * removed from the table. */ void recordRemoval(HashMap m) { } }
当我们使用 new HashMap
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
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();
}
当我们调用put(K k,V v) 方法的时候,底册又是如何做的呢:
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 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;
}
从上面的代码中我们可以看出,当我们首次向一个新创建的Map 中put 数据的时候,首先是通过 inflateTable 方法用来初始化用来存数据table数组的长度。初始化长度的同时通过initHashSeedAsNeeded 方法 来初始化哈希掩码值。
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
如果我们存放的 key-Value 键值对中的键值为null, 调用用putForNullKey(value) 方法 来存放我们的键值对。
private V putForNullKey(V value) {
for (Entry 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;
}
从代码中我们可以看到,如果key 已存在于原来的map 中,将原来key所对应的value替换为我们新的value,并将原来的value 返回。如过不存在,则调用addEntry方法将我们的key-value 存入 ,并返回null(key)。
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);
}
addEntry 方法首先要做的事情是判断是否需要 扩充table 的长度。如果需要的话调用resize 方法来扩充table 的长度,算出hash ,通过indexFor 方法算出元素所要存放于数组中对应的下标,通过createEntry 方法将元素存入Mpa中。
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
如果 存如的 key-value 键值对key 不为空。首先通过indexFor 方法算出方法算出元素所要存放于数组中对应的下标i, 从i 开始向后查找元素是否存在于map中如果存在替换原map 中key 所对应的value 值,并将 旧值返回。如果不存在调用addEntry 方法存入。
以上就是map put 值得过程。而当我们通过 value get(Key key) 方法来取值的时候是如何做的呢。
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry 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;
}
知道了put 的流程,get 方法理解起来就一目了然了。
下面我们再来看下我们通常用的 containsKey(Key key) 和containsValue(Value value)方法。
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
public boolean containsValue(Object value) {
if (value == null)
return containsNullValue();
Entry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (value.equals(e.value))
return true;
return false;
}
是不是有种一目了然地感觉。
综上所述:我们知道了HashMap 底层数据结构是hash 表结构。我们讲到了put 方法、get方法 以及containsValue 和containsKey 方法的内部实现。唯独没有说到Hash表到底是中结构,底层的hash 算法是如何实现的。这些会在后面的章节中慢慢说明。
2、TreeMap
TreeMap 底层机构是一种属结构。TreeMap 具有排序排序功能。 我们在使用TreeMap 的时候可以传入我们自己的比较器,对其进行排序。如果不传入的话TreeMap 会使用自己key 默认的比较器进行排序。如果我们想要是用自己定义的类型来作为TreeMap的key 的话 ,我们自定义的类需要实现Comparable 接口实现其compareTo(T o) 方法。或者在使用的时候我们传入我们自己定义的比较器。下面我们就来看下TreeMap 的底层结构:
跟HashMap 一样TreeMap 底层也是用 Entry来存储数据的,但是TreeMap 有自己的Entry 实现(树)。
static final class Entryimplements Map.Entry { K key; V value; Entry left = null; Entry right = null; Entry 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 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()); } public int hashCode() { int keyHash = (key==null ? 0 : key.hashCode()); int valueHash = (value==null ? 0 : value.hashCode()); return keyHash ^ valueHash; } public String toString() { return key + "=" + value; } }
知道了TreeMap 底层Entry 的实现我们再来看下 put 方法是如何向TreeMap 中存放数据的。
public V put(K key, V value) {
Entry t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
当我们想一TreeMap 中存入第一个元素的时候,底层树种不存在节点,这是我们创建一个节点做为树的根节点。节点存放有key 和value 的值(Entry)。以后每次put 分为两种情况。1、如果在我门 new TreeMap 的时候传入了我们自己的比较器 ,就采用我们自己定义的比较器来比较key 值得大小,来确定元素在书中存放的位置。2、如果没有传入我们自己定义的比较器(这中情况下 我们的key 类型需要实现Comparable 接口)使用key 默认实现的 比较方法。
在来看以下 TreeMap 的get 方法:
public V get(Object key) {
Entry p = getEntry(key);
return (p==null ? null : p.value);
}
final Entry 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 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 方法的实现。无非就是 通过我们自己传入的比较器,或者key 自身的比较方法来来寻找相同的key 值。找到的话返回当前key 所对应的 value 找不到的话返回null。
同样containsKey 和 containsValue 实现思路差不多。
3、WeakHashMap
WeakHashMap实现了Map接口,是HashMap的一种实现,他使用弱引用作为内部数据的存储方案,WeakHashMap可以作为简单缓存表的解决方案,当系统内存不够的时候,垃圾收集器会自动的清除没有在其他任何地方被引用的键值对。
如果需要用一张很大的HashMap作为缓存表,那么可以考虑使用WeakHashMap,当键值不存在的时候添加到表中,存在即取出其值。
WeakHashMap weakMap = new WeakHashMap(); for(int i = 0; i < 100000; i++){ Integer ii = new Integer(i); weakMap.put(ii, new byte[i]); } HashMap map = new HashMap (); for (int i = 0; i < 10000; i++) { Integer ii = new Integer(i); map.put(ii, new byte[i]); }
这2段代码分别用-Xmx2M的参数运行,运行的结果是第一段代码可以很好的运行,第二段代码会出现“Java Heap Space”的错误,这说明用WeakHashMap存储,在系统内存不够用的时候会自动回收内存。
如果WeakHashMap的key在系统内持有强引用,那么WeakHashMap就退化为HashMap,所有的表项无法被垃圾收集器自动清理。
4、EnumMap
与枚举类型键一起使用的专用 Map 实现。枚举映射中所有键都必须来自单个枚举类型,该枚举类型在创建映射时显式或隐式地指定。枚举映射在内部表示为数组。此表示形式非常紧凑且高效。
枚举映射根据其键的自然顺序 来维护(该顺序是声明枚举常量的顺序)。在 collection 视图(keySet()、entrySet() 和 values())所返回的迭代器中反映了这一点。
由 collection 视图返回的迭代器是弱一致 的:它们不会抛出 ConcurrentModificationException,也不一定显示在迭代进行时发生的任何映射修改的效果。
不允许使用 null 键。试图插入 null 键将抛出 NullPointerException。但是,试图测试是否出现 null 键或移除 null 键将不会抛出异常。允许使用 null 值。
像大多数 collection 一样,EnumMap 是不同步的。如果多个线程同时访问一个枚举映射,并且至少有一个线程修改该映射,则此枚举映射在外部应该是同步的。这一般通过对自然封装该枚举映射的某个对象进行同步来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap(java.util.Map 方法来“包装”该枚举。最好在创建时完成这一操作,以防止意外的非同步访问:
Mapm = Collections.synchronizedMap(new EnumMap (...));
实现注意事项:所有基本操作都在固定时间内执行。虽然并不保证,但它们很可能比其 HashMap 副本更快。
下面我们简单来看下 EnumMap 底层简单实现:
先从构造方法:
private final ClasskeyType; private transient K[] keyUniverse; private transient Object[] vals; public EnumMap(Class keyType) { this.keyType = keyType; keyUniverse = getKeyUniverse(keyType); vals = new Object[keyUniverse.length]; } public EnumMap(EnumMap m) { keyType = m.keyType; keyUniverse = m.keyUniverse; vals = m.vals.clone(); size = m.size; } public EnumMap(Map m) { if (m instanceof EnumMap) { EnumMap em = (EnumMap ) m; keyType = em.keyType; keyUniverse = em.keyUniverse; vals = em.vals.clone(); size = em.size; } else { if (m.isEmpty()) throw new IllegalArgumentException("Specified map is empty"); keyType = m.keySet().iterator().next().getDeclaringClass(); keyUniverse = getKeyUniverse(keyType); vals = new Object[keyUniverse.length]; putAll(m); } }
EnumMap 提供了三种不同参数的构造方法。
再来看下put 方法:
public V put(K key, V value) {
typeCheck(key);
int index = key.ordinal();
Object oldValue = vals[index];
vals[index] = maskNull(value);
if (oldValue == null)
size++;
return unmaskNull(oldValue);
}
private Object maskNull(Object value) {
return (value == null ? NULL : value);
}
private V unmaskNull(Object value) {
return (V) (value == NULL ? null : value);
}
从中我们可以看出put 方法插入值得时候 是想vals 数组中加入了数据。
再看get 方法:
public V get(Object key) {
return (isValidKey(key) ?
unmaskNull(vals[((Enum)key).ordinal()]) : null);
}
private boolean isValidKey(Object key) {
if (key == null)
return false;
// Cheaper than instanceof Enum followed by getDeclaringClass
Class keyClass = key.getClass();
return keyClass == keyType || keyClass.getSuperclass() == keyType;
}
从上面我们可以看出:枚举映射在内部表示为数组。
5、LinkedHashMap
LinkedHashMap是HashMap的一个子类,它保留插入的顺序,如果需要输出的顺序和输入时的相同,那么就选用LinkedHashMap。
LinkedHashMap是Map接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
LinkedHashMap实现与HashMap的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。
对于LinkedHashMap而言,它继承与HashMap、底层使用哈希表与双向链表来保存所有元素。其基本操作与父类HashMap相似,它通过重写父类相关的方法,来实现自己的链接列表特性。下面我们来分析LinkedHashMap的源代码:
public class LinkedHashMapextends HashMap implements Map
LinkedHashMap采用的hash算法和HashMap相同,但是它重新定义了数组中保存的元素Entry,该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而在哈希表的基础上又构成了双向链接列表。看源代码:
//true表示按照访问顺序迭代,false时表示按照插入顺序 private final boolean accessOrder; /** * 双向链表的表头元素。 */ private transient Entryheader; /** * LinkedHashMap的Entry元素。 * 继承HashMap的Entry元素,又保存了其上一个元素before和下一个元素after的引用。 */ private static class Entry extends HashMap.Entry { Entry before, after; …… }
HashMap.Entry:
static class Entryimplements Map.Entry { final K key; V value; Entry next; final int hash; Entry(int h, K k, V v, Entry n) { value = v; next = n; key = k; hash = h; } }
通过源代码可以看出,在LinkedHashMap的构造方法中,实际调用了父类HashMap的相关构造方法来构造一个底层存放的table数组。如:
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
HashMap中的相关构造方法:
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)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}
我们已经知道LinkedHashMap的Entry元素继承HashMap的Entry,提供了双向链表的功能。在上述HashMap的构造器中,最后会调用init()方法,进行相关的初始化,这个方法在HashMap的实现中并无意义,只是提供给子类实现相关的初始化调用。
LinkedHashMap重写了init()方法,在调用父类的构造方法完成构造后,进一步实现了对其元素Entry的初始化操作。
void init() {
header = new Entry<>(-1, null, null, null);
header.before = header.after = header;
}
LinkedHashMap并未重写父类HashMap的put方法,而是重写了父类HashMap的put方法调用的子方法void recordAccess(HashMap m) ,void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),提供了自己特有的双向链接列表的实现。
HashMap 的put 方法:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry 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;
}
重写方法:
void recordAccess(HashMapm) { LinkedHashMap lm = (LinkedHashMap )m; if (lm.accessOrder) { lm.modCount++; remove(); addBefore(lm.header); } } void addEntry(int hash, K key, V value, int bucketIndex) { // 调用create方法,将新元素以双向链表的的形式加入到映射中。 createEntry(hash, key, value, bucketIndex); // 删除最近最少使用元素的策略定义 Entry eldest = header.after; if (removeEldestEntry(eldest)) { removeEntryForKey(eldest.key); } else { if (size >= threshold) resize(2 * table.length); } } void createEntry(int hash, K key, V value, int bucketIndex) { HashMap.Entry old = table[bucketIndex]; Entry e = new Entry (hash, key, value, old); table[bucketIndex] = e; // 调用元素的addBrefore方法,将元素加入到哈希、双向链接列表。 e.addBefore(header); size++; } private void addBefore(Entry existingEntry) { after = existingEntry; before = existingEntry.before; before.after = this; after.before = this; }
6、HashTable 和HashMap 的区别:
HashTable 和HashMap 大致相同,我们这部在累述,在这只说下他们的区别:
public class Hashtableextends Dictionary implements Map , Cloneable, java.io.Serializable public class HashMap extends AbstractMap implements Map , Cloneable, Serializable
Hashtable 继承自Dictionary HashMap 继承自AbstractMap。
HashTable 的put 方法:
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry tab[] = table;
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
V old = e.value;
e.value = value;
return old;
}
}
modCount++;
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = hash(key);
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
Entry e = tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
return null;
}
注意1 方法是同步的
注意2 方法不允许value==null
注意3 方法调用了key的hashCode方法,如果key==null,会抛出空指针异常 HashMap的put方法如下
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 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;
}
注意1 方法是非同步的
注意2 方法允许key==null
注意3 方法并没有对value进行任何调用,所以允许为null