前言
在java业界,有一些问题在面试的时候可以说基本上是必问的。比如:java中HashMap、HashTable、ConcurrentHashMap的区别,又或者是ArrayList 和 LinkedList 和 Vector 的区别。
而这些问题,我总结一下,基本上都是java中的数据结构问题,我们对这些数据结构的了解是非常浅薄的,比如说在1.8JDk版本前后的这些数据结构的变化是非常大的,但是我们对这些却依然不是很了解,不深入进去,很难明白到底发生了什么。那么,接下来的一系列博客,都让我们来分析一下java中的数据结构吧。
今天,我们先了解的是Hashmap。
ps : 我现在的jdk版本是1.8。
简介
在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。
而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值时,将链表转换为红黑树,这样大大减少了查找时间。
主要参数
先来看HashMap里面的一些参数:
//默认初始大小是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
**************************************************************************************
//设置的最大容量是2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
**************************************************************************************
//设置的默认加载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
**************************************************************************************
//当某个桶节点数量大于8时,会转换为红黑树。
static final int TREEIFY_THRESHOLD = 8;
**************************************************************************************
//当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。
static final int UNTREEIFY_THRESHOLD = 6;
**************************************************************************************
//当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。
static final int MIN_TREEIFY_CAPACITY = 64;
*************************************************************************************
//也是加载因子,只不过这个是变量。
final float loadFactor;
*************************************************************************************
//临界值,也就是元素数量达到临界值时,会进行扩容。
int threshold;
PS:HashMap默认的加载因子是0.75,最大容量是16,因此可以得出HashMap的默认容量是:0.75*16=12。
加载因子是表示Hsah表中元素的填满的程度.若:加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.反之,加载因子越小,填满的元素越少,好处是冲突的机会减小了,但空间浪费多了。
构造函数
看了HashMap的一些参数,现在我们来了解一下HashMap的构造函数:
我们看到在源码中,HashMap有四个构造函数,在其中,HashMap(int initialCapacity)这个构造函数,实际上指向了另外一个构造函数HashMap(int initialCapacity, float loadFactor)。我们还是把所有的构造函数都列出来吧。构造函数1
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 设置加载因子为默认值0.75
}
构造函数2
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR); //指向构造函数3,且加载因子为默认的0.75
}
构造函数3
public HashMap(int initialCapacity, float loadFactor) {
//判断初始加载容量是否小于0,若是,直接抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
//判断初始加载容量是否大于最大容量,若是,设置初始加载容量为最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//判断加载因子是否小于0或者判断是否为NaN,若是,抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
//设置初始加载因子
this.loadFactor = loadFactor;
//对临界值进行初始化,tableSizeFor(t)这个方法会返回大于t值的,且离其最近的2次幂,例如t为29,则返回的值是32
this.threshold = tableSizeFor(initialCapacity);
}
PS:NaN 实际上就是 Not a Number的简称。0.0f/0.0f的值就是NaN,从数学角度说,0/0就是一种未确定。
构造函数4
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR; //设置加载因子为默认的0.75
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//获取该map的实际长度
int s = m.size();
if (s > 0) {
//判断table是否初始化,如果没有初始化
if (table == null) { // pre-size
//求出需要的容量,因为实际使用的长度=容量*0.75得来的,+1是因为小数相除,基本都不会是整数,容量大小不能为小数的,后面转换为int,多余的小数就要被丢掉,所以+1,例如,map实际长度22,22/0.75=29.3,所需要的容量肯定为30,有人会问如果刚刚好除得整数呢,除得整数的话,容量大小多1也没什么影响
float ft = ((float)s / loadFactor) + 1.0F;
//判断该容量大小是否超出上限。
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//对临界值进行初始化,tableSizeFor(t)这个方法会返回大于t值的,且离其最近的2次幂,例如t为29,则返回的值是32
if (t > threshold)
threshold = tableSizeFor(t);
}
//如果table已经初始化,且map的实际长度大于临界值,则进行扩容操作,resize()就是扩容。
else if (s > threshold)
resize();
//遍历,把map中的数据转到hashMap中。
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
HashMap的扩容方法非常复杂,等有机会了我们再来分析,我们只要明白在HashMap中的一些参数,就大概能明白HashMap的扩容方法的原理。
//当某个桶节点数量大于8时,会转换为红黑树。
static final int TREEIFY_THRESHOLD = 8;
**************************************************************************************
//当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。
static final int UNTREEIFY_THRESHOLD = 6;
**************************************************************************************
//当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。
static final int MIN_TREEIFY_CAPACITY = 64;
JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值时,将链表转换为红黑树,因此,在HashMap的扩容方法中,不仅仅是容量的变化,而且也有着数据结构本质上的变化。
总结
我们发现,java中的HashMap是在与时俱进,变化一直都在。
还有一些关于HashMap的put和get方法,我们在这里没有进行分析,但是由于本人的技术还不够,对于红黑树的不熟悉,因此不能深入解析,希望以后在了解了红黑树以后,能够回过头来更深入的了解HashMap。