深入JDK源码系列:HashMap详解
2018-03-05 09:40
746 查看
继续挖掘 HashMap 背后的故事。
例如:
Hashtable 则保留了 contains,containsValue 和 containsKey 三个方法,其中 contains 和 containsValue 功能相同。
Hashtable 中,key 和 value 都不允许出现 null 值。
HashMap 中,null 可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为 null 。当 get() 方法返回 null 值时,可能是 HashMap 中没有该键,也可能使该键所对应的值为 null 。因此,在 HashMap 中不能由 get() 方法来判断 HashMap 中是否存在某个键, 而应该用 containsKey() 方法来判断。
Hashtable 采用的是 2 * old + 1 ,而 HashMap 是 2 * old。
1. 特性
HashMap 是一个双列集合,存储的元素都是 key - value 形式的键值对,使用哈希表作为内部数据结构,不保证存入和取出元素的顺序,允许使用 null 键和 null 值,方法上没有使用 synchronized ,线程不安全,继承自 AbstractMap 类,实现了 Map 接口。public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { private static final long serialVersionUID = 362498820763181265L; // ...省略... }
2. 内部数据结构
内部是一个 Node 数组,而数组的每一项又是一个单向链表,这个链表用来存储发生哈希冲突的元素,在 Java8 中,如果发生哈希冲突的次数超过了 8 次,就会将链表转化成一颗红黑树。/** * The table, initialized on first use, and resized as * necessary. When allocated, length is always a power of two. * (We also tolerate length zero in some operations to allow * bootstrapping mechanics that are currently not needed.) * * 第一次使用的时候便会初始化的数组,在需要的情况下可以调整它的大小 * 调整大小时,数组的长度必须是 2 的整数倍 * 在进行某些操作不需要该数组的时候,允许数组的长度为 0 ,以便于在虚拟机启动的时候节省内存 */ transient Node<K,V>[] table; /** * Basic hash bin node, used for most entries. (See below for * TreeNode subclass, and in LinkedHashMap for its Entry subclass.) * * HashMap 中所存储的元素的类型,可以看出我们存储的 key 和 value 都是作为这个 Node 类的两个属性 * 这个 Node 中由于有一个 next 属性用来指向发生哈希冲突的下一个节点,所以构成了一个单向链表 */ static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } } /** * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * * 一旦哈希冲突的次数达到了这个门限值(8),会将单向链表转化成一颗红黑树 * 遍历单链表的时间复杂度是O(n),而遍历一颗二叉树的时间复杂度是O(logn) * 这就会将最坏的情况下 HashMap 的查找的时间复杂度从O(n)提高到O(logn) */ static final int TREEIFY_THRESHOLD = 8; /** * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn * extends Node) so can be used as extension of either regular or * linked node. * * 频繁的发生哈希冲突时所采用的红黑树的类型定义 */ static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion 下一步删除的时候用来断开连接 boolean red; TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); } }
3. 一些重要的属性
/** * The default initial capacity - MUST be a power of two. * HashMap 默认的容量,必须是 2 的整数倍 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 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. * HashMap 中 Node 数组的大小最大是 2 的 30 次方 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * The load factor used when none specified in constructor. * 默认的装载因子,当 HashMap 中元素的个数已经达到了容量 * 0.75f 时,对哈希桶(形象的形容 Node 数组)进行扩容,容量是原来的 2 倍 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * The load factor for the hash table. *上面是默认的装载因子的大小,这个是实际值 * @serial */ final float loadFactor; /** * Holds cached entrySet(). Note that AbstractMap fields are used * for keySet() and values(). * 存放的是键值对对象 Entry (其实 Node 节点实现了这个 Map.Entry 接口) */ transient Set<Map.Entry<K,V>> entrySet; /** * The bin count threshold for untreeifying a (split) bin during a * resize operation. Should be less than TREEIFY_THRESHOLD, and at * most 6 to mesh with shrinkage detection under removal. * * 将哈希桶的数据结构由红黑树装换成单向链表的条件: * 哈希桶的中的节点树最大为6个的时候,将其装换成单向链表 */ static final int UNTREEIFY_THRESHOLD = 6; /** * The smallest table capacity for which bins may be treeified. * (Otherwise the table is resized if too many nodes in a bin.) * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts * between resizing and treeification thresholds. * 如果哈希桶的数据结构要转换为红黑树,那么哈希桶最少的数量应达到这个值 */ static final int MIN_TREEIFY_CAPACITY = 64; /** * The number of key-value mappings contained in this map. * * HashMap 中所存储的键值对的数量,并不是哈希桶的容量 */ transient int size; /** * The next size value at which to resize (capacity * load factor). * 当数组中下一个元素的个数达到这个门限值的时候,将会 rehash (重新调整数组的大小,并把原数组中的元素都存放到新数组中) * * 门限值 = 数组容量 * 装载因子的大小 * @serial */ int threshold;
4. HashTable 和 HashMap的区别
4.1 继承的父类不同
Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { ... } public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable { ... }
4.2 线程的安全性不同
Hashtable 中的方法是Synchronize的,而HashMap中的方法在默认的情况下是非Synchronize的。例如:
/** * Returns the number of keys in this hashtable. * * 这是 Hashtable 的 size 方法 * @return the number of keys in this hashtable. */ public synchronized int size() { return count; } /** * Returns the number of key-value mappings in this map. * * 这是 HashMap 的 size 方法 * @return the number of key-value mappings in this map */ public int size() { return size; }
4.3 是否提供 contains 方法
HashMap 把 Hashtable 的 contains 方法去掉了,改成 containsValue 和 containsKey,因为 contains 方法容易让人引起误解。Hashtable 则保留了 contains,containsValue 和 containsKey 三个方法,其中 contains 和 containsValue 功能相同。
/** * 这是 HashTable 的 containsValue 方法,可以看出它调用的就是 contains 方法,但是这里并没有加 synchronized 同步,是因为... */ public boolean containsValue(Object value) { return contains(value); } /** * 是因为 contains 方法上已经加了同步 * */ public synchronized boolean contains(Object value) { if (value == null) { throw new NullPointerException(); } Entry<?,?> tab[] = table; for (int i = tab.length ; i-- > 0 ;) { for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) { if (e.value.equals(value)) { return true; } } } return false; }
4.4 key 和 value 是否允许 null 值
其中 key 和 value 都是对象,并且不能包含重复 key,但可以包含重复的 value。Hashtable 中,key 和 value 都不允许出现 null 值。
HashMap 中,null 可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为 null 。当 get() 方法返回 null 值时,可能是 HashMap 中没有该键,也可能使该键所对应的值为 null 。因此,在 HashMap 中不能由 get() 方法来判断 HashMap 中是否存在某个键, 而应该用 containsKey() 方法来判断。
4.5 :哈希值的计算方法不同
Hashtable 直接使用的是对象的 hashCode ,而 HashMap 则是在对象的 hashCode 的基础上还进行了一些变化。4.6 内部实现使用的数组初始化和扩容方式不同
HashTable 初始大小是11,而 HashMap 初始大小是16。Hashtable 采用的是 2 * old + 1 ,而 HashMap 是 2 * old。
/** * Constructs a new, empty hashtable with a default initial capacity (11) * and load factor (0.75). * 初始容量默认为 11 ,装载因子是 0.75 */ public Hashtable() { this(11, 0.75f); }
相关文章推荐
- 深入JDK源码系列:HashSet详解
- 深入理解JAVA集合系列一:HashMap源码解读
- JDK源码阅读系列---HashMap
- 给jdk写注释系列之jdk1.6容器(4)-HashMap源码解析
- ConcurrentHashMap JDK1.8源码深入剖析(一)
- JDK源码系列(2)----HashMap源码分析
- 深入JDK源码之HashMap
- 一,初调HashMap,如何修改JDK的源码进行调试 【深入JDK源码】
- 集合详解(四)----HashSet和HashMap源码剖析(JDK1.7)
- 源码分析系列1:HashMap源码分析(基于JDK1.8)
- JDK源码学习系列08----HashMap
- HashMap源码解析 给jdk写注释系列之jdk1.6容器(4)
- 深入Java基础(四)--哈希表(1)HashMap应用及源码详解
- Java 1.8 HashMap 源码中 put()方法详解
- JDK1.8源码阅读系列之三:Vector_1
- JDK源码分析(三)——HashMap 下(基于JDK8)
- 深入seajs源码系列二
- Jdk 8系列之重新认识HashMap
- java核心基础--jdk源码分析学习--HashMap
- HashMap源码分析(基于JDK1.6)