1.HashMap为Map接口的一个实现类,实现了所有Map的操作。HashMap除了允许key和value保存null值和非线程安全外,其他实现几乎和HashTable一致。
2.HashMap使用散列存储的方式保存kay-value键值对,因此其不支持数据保存的顺序。如果想要使用有序容器可以使用LinkedHashMap。
3.在遍历HashMap的时候,其遍历节点的个数为bucket的个数+HashMap中保存的节点个数。因此当遍历操作比较频繁的时候需要注意HashMap的初始化容量不应该太大。 这一点其实比较好理解:当保存的节点个数一致的时候,bucket越少,遍历次数越少。
4.另外HashMap在resize的时候会有很大的性能消耗,因此当需要在保存HashMap中保存大量数据的时候,传入适当的默认容量以避免resize可以很大的提高性能。
5.HashMap是非线程安全的类,当作为共享可变资源使用的时候会出现线程安全问题。需要使用线程安全容器
transient Node[] table; //Node类型的数组,记我们常说的bucket数组,其中每个元素为链表或者树形结构 transient int size;//HashMap中保存的数据个数 int threshold;//HashMap需要resize操作的阈值 final float loadFactor;//负载因子,用于计算threshold
transient Node[] table; //Node类型的数组,记我们常说的bucket数组,其中每个元素为链表或者树形结构 transient int size;//HashMap中保存的数据个数 int threshold;//HashMap需要resize操作的阈值 final float loadFactor;//负载因子,用于计算threshold
/此构造函数创建一个空的HashMap,其中负载因子为默认值0.75 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } //传入默认的容量大小,创造一个指定容量大小和默认负载因子为0.75的HashMap public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } //创建一个指定容量和指定负载 public HashMap(int initialCapacity, float loadFactor) { this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }
其中在指定初始化容量的时候,会根据传入的参数来确定HashMap的容量大小。
初始化this.threshold的值为入参initialCapacity距离最近的一个2的n次方的值。
此处赋值为this.threshold,是因为构造函数的时候并不会创建table,只有实际插入数据的时候才会创建。目的应该是为了节省内存空间.
在第一次插入数据的时候,会将table的capacity设置为threshold,同时将threshold更新为loadFactor * capacity
HashMap在插入数据的时候传入key-value键值对。使用hash寻址确定保存数据的bucket。当第一次插入数据的时候会进行HashMap中容器的初始化。
case initialCapacity = 0: this.threshold = 1; case initialCapacity为非0且不为2的n次方: this.threshold = 大于initialCapacity中第一个2的n次方的数。 case initialCapacity = 2^n: this.threshold = initialCapacity
其中resize函数的源码如下,主要操作为根据cap和loadFactory创建初始化table
int newCap, newThr = 0; if (oldThr > 0) // 当构造函数中传入了capacity的时候 newCap = oldThr; //newCap = threshold 2的n次方,即构造函数的时候的初始化容量 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } float ft = (float)newCap * loadFactor; // 2的n次方 * loadFactory newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); threshold = newThr; //新的threshold== newCap * loadFactory Node[] newTab = (Node [])new Node[newCap]; //长度为2的n次方的数组 table = newTab;
在初始化table之后,将数据插入到指定位置,其中bucket的确定方法为:
i = (n-1) & hash// 此处n-1必定为 0000 1111 1111....的格式,取&操作之后的值一定在数组的容量范围内。
其中hash的取值方法为:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
在HashMap的定义中实现了Cloneable接口,Cloneable是一个标识接口,主要用来标识 Object.clone()的合法性,在没有实现此接口的实例中调用 Object.clone()方法会抛出CloneNotSupportedException异常。可以看到HashMap中重写了clone方法。
HashMap实现Serializable接口主要用于支持序列化。同样的Serializable也是一个标识接口,本身没有定义任何方法和属性。另外HashMap自定义了
private void writeObject(java.io.ObjectOutputStream s) throws IOException private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException
两个方法实现了自定义序列化操作。
注意:支持序列化的类必须有无参构造函数。这点不难理解,反序列化的过程中需要通过反射创建对象。
提问:
1、hashmap的默认容量是多少,为什么这样设计?
在HashMap中,有两个比较容易混淆的关键字段:size和capacity ,这其中capacity就是Map的容量,而size为Map中的元素个数。capacity默认16
因为位运算直接对内存数据进行操作,不需要转成十进制,所以位运算要比取模运算的效率更高,所以HashMap在计算元素要存放在数组中的index的时候,使用位运算代替了取模运算。之所以可以做等价代替,前提是要求HashMap的容量一定要是2^n 。既然一定要设置一个默认的2^n 作为初始值,那么就需要在效率和内存使用上做一个权衡。这个值既不能太小,也不能太大。
太小了就有可能频繁发生扩容,影响效率。太大了又浪费空间,不划算。所以,16就作为一个经验值被采用了。
2、hashmap 1.7为什么要先扩容再添加,1.8为什么是先添加再扩容
3、1.7&1.8插入数据的规则是什么?
JDK1.7用的是头插法,而JDK1.8及之后使用的是尾插法
4、hashmap是否线程安全?为什么?替换方案有哪些,是如何实现的?
不是线程安全。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用Collections.synchronizedMap方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问。