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 (Entrye = 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 (Entrye = 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) { Entrye = 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(); Entryentry = 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) { Entryt = 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) { Entryp = 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 (Entrye = 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 (Entrye = 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 (Entrye = 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