HashMap / HashTable / HashSet
1、HashMap 总结
Map映射中不能包含重复的键,允许放入key
为null
的元素,也允许插入value
为null
的元素。
HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
HashMap 的实现不是同步的,不是线程安全的。
HashMap 的API
void clear() Object clone() boolean containsKey(Object key) boolean containsValue(Object value) Set<Entry<K, V>> entrySet() V get(Object key) boolean isEmpty() Set<K> keySet() V put(K key, V value) void putAll(Map<? extends K, ? extends V> map) V remove(Object key) int size() Collection<V> values()
entrySet()返回键-值集的Set集合; keySet()返回键集的Set集合; values()返回值集的Collection集合。
因为Map中不能包含重复的键;每个键最多只能映射到一个值。所以,键-值集、键集都是Set,值集时Collection。
Map.Entry是Map中内部的一个接口。Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。根据对冲突的处理方式不同,哈希表有两种实现方式,一种开放地址方式(Open addressing),另一种是冲突链表方式(Separate chaining with linked lists)。HashMap通过“拉链法”解决哈希冲突的。
2、HashMap存储原理
系统初始化 HashMap 时,会创建一个长度为 capacity 的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket 都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素。 无论何时,HashMap 的每个“桶”只存储一个元素(也就是一个 Entry),由于 Entry 对象可以包含一个引用变量(就是 Entry 构造器的的最后一个参数)用于指向下一个 Entry,因此可能出现的情况是:HashMap 的 bucket 中只有一个 Entry,但这个 Entry 指向另一个 Entry ——这就形成了一个 Entry 链。
有两个参数可以影响HashMap的性能:初始容量(inital capacity)和负载系数(load factor)。初始容量指定了初始table
的大小,负载系数用来指定自动扩容的临界值。当entry
的数量超过capacity*load_factor
时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。
将对象放入到HashMap或HashSet中时,有两个方法需要特别关心:hashCode()
和equals()
。hashCode()
方法决定了对象会被放到哪个bucket
里,当多个对象的哈希值冲突时,equals()
方法决定了这些对象是否是“同一个对象”。所以,如果要将自定义的对象放入到HashMap
或HashSet
中,需要@Override hashCode()
和equals()
方法。
HashMap类的put方法:
public V put(K key, V value) { // 如果 key 为 null,调用 putForNullKey 方法进行处理。 // null的hash值总是0,会被存储到table[0] if (key == null) return putForNullKey(value); // 根据key的hashCode方法返回值,计算 Hash 值 int hash = hash(key.hashCode()); // 搜索指定 hash 值在对应 table 中的索引 int i = indexFor(hash, table.length); // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素 for (Entry<K, V> e = table[i]; e != null; e = e.next) { Object k; // 找到指定 key 与需要放入的 key 相等(hash 值相同通过 equals 比较放回 true) if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 如果 i 索引处的 Entry 为 null,表明此处还没有 Entry modCount++; // 将 key、value 添加到 i 索引处 addEntry(hash, key, value, i); return null; }
- 对key做null检查。如果key是null,会被存储到table[0],因为null的hash值总是0。
- key的hashcode()方法会被调用,然后计算hash值。hash值用来找到存储Entry对象的数组的索引。对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 Hash 码值总是相同的。
- indexFor(hash, table.length)用来计算在table数组中存储Entry对象的精确的索引(即,该对象应该保存在 table 数组的哪个索引处)。
- 如果两个key有相同的hash值(也叫冲突),他们会以链表的形式来存储。所以,这里我们就迭代链表。
- 如果再次放入同样的key,会调用equals()方法来检查key的相等性(key.equals(k)),用当前Entry的value来替换之前的value。
- 如果两个key不同,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部。调用addEntry方法。
addEntry方法:
void addEntry(int hash, K key, V value, int bucketIndex) { // 获取指定 bucketIndex 索引处的 Entry Entry<K, V> e = table[bucketIndex]; // ① // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry table[bucketIndex] = new Entry<K, V>(hash, key, value, e); // 如果 Map 中的 key-value 对的数量超过了极限 if (size++ >= threshold) // 把 table 对象的长度扩充到 2 倍。 resize(2 * table.length); // ② }
总结:当向 HashMap 中添加 key-value 对,由其 key 的 hashCode() 返回值决定该 key-value 对(就是 Entry 对象)的存储位置。当两个 Entry 对象的 key 的 hashCode() 返回值相同时,将由 key 通过 eqauls() 比较值决定是采用覆盖行为(返回 true),还是产生 Entry 链(返回 false)。
系统总是将新添加的 Entry对象放入 table 数组的 bucketIndex索引处——如果 bucketIndex 索引处已经有了一个 Entry对象,那新添加的 Entry对象指向原有的 Entry对象(产生一个 Entry链),如果 bucketIndex 索引处没有 Entry对象,也就是上面程序①号代码的 e 变量是 null,也就是新放入的 Entry对象指向 null,也就是没有产生 Entry链。
- size:该变量保存了该 HashMap 中所包含的 key-value 对的数量。
- threshold:该变量包含了 HashMap 能容纳的 key-value 对的极限,它的值等于 HashMap 的容量乘以负载因子(load factor)。
HashMap类的get方法:
public V get(Object key) { // 如果 key 是 null,调用 getForNullKey 取出对应的 value if (key == null) return getForNullKey(); // 根据该 key 的 hashCode 值计算它的 hash 码 int hash = hash(key.hashCode()); // 直接取出 table 数组中指定索引处的值, for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next){ Object k; // 如果该 Entry 的 key 与被搜索 key 相同 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
- 对key进行null检查。如果key是null,table[0]这个位置的元素将被返回。
- key的hashcode()方法被调用,然后计算hash值。
- 在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。
总结:
- HashMap有一个叫做Entry的内部类,它用来存储key-value对。
- 上面的Entry对象是存储在一个叫做table的Entry数组中。
- table的索引在逻辑上叫做“桶”(bucket),它存储了链表的第一个元素。
- key的hashcode()方法用来找到Entry对象所在的桶。
- 如果两个key有相同的hash值,他们会被放在table数组的同一个桶里面。
- key的equals()方法用来确保key的唯一性。
- value对象的equals()和hashcode()方法根本一点用也没有。
3、HashMap 的遍历
Map<String, Integer> map = new HashMap<String, Integer>(); map.put("d", 2); map.put("c", 1); map.put("b", 1); map.put("a", 3);
(1)遍历 Map.Entry
Iterator<Map.Entry<String, Integer>> iter = map.entrySet().iterator(); while(iter.hasNext()){ Map.Entry<String, Integer> entry = iter.next(); String key = entry.getKey(); Integer val = entry.getValue(); }
(2)遍历 Key
Iterator<String> iter = map.keySet().iterator(); while(iter.hasNext()){ String key = iter.next(); }
(3)遍历 Value
Iterator<Integer> iter = map.values().iterator(); while(iter.hasNext()){ Integer key = iter.next(); }
4、HashMap 排序
List<Map.Entry<String, Integer>> entryList = new ArrayList<Map.Entry<String, Integer>>(map.entrySet()); Collections.sort(entryList, new Comparator<Map.Entry<String, Integer>>() { @Override public int compare(Entry<String, Integer> o1, Entry<String, Integer> o2) { // 根据value排序 return o2.getValue().compareTo(o1.getValue()); // return o2.getValue() - o1.getValue(); // 根据key排序 //return o1.getKey().compareTo(o2.getKey()); } }); // after sorted for(int i=0; i<entryList.size(); i++){ Map.Entry<String, Integer> entry = entryList.get(i); String key = entry.getKey(); Integer val = entry.getValue(); System.out.println(key + " " + val); }
5、HashTable (public class Hashtable extends Dictionary implements Map)
- Hashtable 中的方法是同步的。HashMap的同步问题可通过Collections的一个静态方法得到解决:Map Collections.synchronizedMap(Map m)
- Hashtable中,key和value都不允许出现null值。
- 哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
- Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。
6、HashSet
HashSet封装了一个 HashMap 对象来存储所有的集合元素,对HashSet的函数调用都会转换成合适的HashMap方法。所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。
7、集合中键值是否允许null小结
- List:可以有多个null,可以有重复值
- HashMap:允许一个null键与多个null值,若重复键,则覆盖以前值。
- TreeMap:不允许null键(实际上可以插入一个null键,如果这个Map里只有一个元素是不会报错的,因为一个元素时没有进行排序操作,也就不会报空指针异常,但如果插入第二个时就会立即报错),但允许多个null值,覆盖已有键值。
- HashSet:能插入一个null(因为内部是以 HashMap实现 ),忽略不插入重复元素。
- TreeSet:不能插入null (因为内部是以 TreeMap 实现 ) ,元素不能重复,如果待插入的元素存在,则忽略不插入,对元素进行排序。
- HashTable:不允许null键与null值(否则运行进报空指针异常)。也会覆盖重复值。
http://fangjian0423.github.io/2016/03/29/jdk_hashmap/ HashMap原理
https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/6-HashSet%20and%20HashMap.md 基于jdk1.7 -- HashMap原理