HashMap源码分析

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<K,V>[] table; //Node类型的数组,记我们常说的bucket数组,其中每个元素为链表或者树形结构
transient int size;//HashMap中保存的数据个数
int threshold;//HashMap需要resize操作的阈值
final float loadFactor;//负载因子,用于计算threshold
transient Node<K,V>[] 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<K,V>[] newTab = (Node<K,V>[])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);

}

 

 

Cloneable和Serializable分析

在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方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问。

 

发布了5 篇原创文章 · 获赞 5 · 访问量 2万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览