以下是JDK1.8之前版本的源码简介
一、什么是HashMap?
Hash:散列将一个任意的长度通过某种(hash函数算法)算法转换成一个固定值。
Map:存储的集合、类似于地图X,Y坐标的存储
总结:通过hash出来的值,然后通过这个值定位到这个map,然后把这个value存储到这个map中 ~~ hashMap基本原理
二、HashMap形式?
key,value。例如 wukong 30 、电话薄 a-z字母等等
二、面试中问到的问题?
1.0 key值可以为空吗?
【回答】hashMap把Null当做一个key值来存储,原因看源码
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//源码中判断了,如果key值未空的时候,没有返回错误信息,也是允许存储的
if (key == null)
return putForNullKey(value);
int hash = hash(key);
.............
}
2.0 HashMap和Hashtable的区别
【回答】HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别在于HashMap允许空(null)键值(key),由于非线程安全,效率上可能高于Hashtable。
HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。
HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因为contains方法容易让人引起误解。
Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。
最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步。
Hashtable和HashMap采用的hash/rehash算法都大概一样,所以性能不会有很大的差异。
3.0 如果Hash key重复了,那么value值会覆盖吗?
【回答】不会覆盖、简单解释:在Entry类中,有个Entry< K,V > next 实例变量;它是来存储hashKey冲突时,存放就的value值。不会覆盖。详细也是见源码
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry next;
int hash;
......
}
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//这里实际上是先获取原来的value,保存老的备份,可以通过 xxx.get("xiaozheng").next.getValue()获取。
V oldValue = e.value; // 假设原来的是30,传进来的是31 。 目前还是30
//然后在把当前的value赋给它
e.value = value; // 31
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
【回答】在put的时候,HashMap集合的容量高于0.75的时候,进行扩容。而且扩容是偶数的,以双倍的形式向上扩容。具体也是看源码
5.0 HashMap性能受什么影响?
【回答】HashMap主要是受 初始化容量跟加载因子。初始化容量:创建一个HashMap默认给与多大的容量的值。加载因子:HashMap在扩容之前最大可以达到的容量。具体原因的话,看下面的“|不足之处“就明白了
6.0 HashMap table的数据结构?
【回答】数组+链表。取两者的优点
四、源码分析
4.1 初始化参数介绍
4.1.1 初始化容量
/**
* 初始化容量 1左移4位 6位
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
4.1.2 最大容量
/**
* 最大容量 1左移30位 也就是 2的30次方。存储的HashMap的个数不能超过该最大容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
4.1.3 加载因子:这也就可以解释为什么上述我说:HashMap集合的容量高于0.75的时候,进行扩容。0.75不是我说的,而是在代码中定义好,0.75
/*
* 加载因子系数
* 可以理解成:在当前容量的四分之三的时候进行扩容。
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
4.1.2 其他初始化参数
/*
* 创建一个Entry对象
*/
static final Entry,?>[] EMPTY_TABLE = {};
/**
* 对table进行赋值
*/
transient Entry[] table = (Entry[]) EMPTY_TABLE;
transient int size;
//扩容变量
int threshold;
//临时加载因子
final float loadFactor;
//修改标记
transient int modCount;
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
4.2 HashMap构造方法
构造方法的话,我们只需要了解下述三个构造方法即可 - - 我们可以手动指定HashMap的初始化容量以及它的加载因子。这也是提高HashMap性能的一种方式。例如:如果你知道你的hashMap需要存储10万个map。那么一开始可以调大你的初始化容量。避免一开始16个集合,多次扩容,多次拷贝带来的时间、性能消耗
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
4.3 put方法分析
上述第一个问题跟第三个问题的答案就这下述代码里面
public V put(K key, V value) {
//判断table是否已经初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key未空的话,调用存空key的方法,没有报错,也就是支持空key的情况
if (key == null)
return putForNullKey(value);
//这也是hash的定义,散列,将key进行某种算法(hash算法),转化成一个固定的值。也就是map数据的下标
int hash = hash(key);
int i = indexFor(hash, table.length);
//for循环判断key的hash值是否是相等的,如果相等的话,就进行换位~~ 这里结合下面下面的图可以理解
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//这里获取原来旧的value
V oldValue = e.value;
//再把当前的value跟key对应,没有覆盖
e.value = value;
//存储旧value
e.recordAccess(this);
return oldValue;
}
}
//修改标记+1
modCount++;
addEntry(hash, key, value, i);
return null;
}
//核心代码,if(),也就是当前容量达到0.75,就会执行resize()方法,进行扩容。这也是上述4.0问题的答案。
//下面我们可以看一下resize的方法--扩容方法
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//执行扩容
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void resize(int newCapacity) {
//老的table数据
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建Entry对象数组
Entry[] newTable = new Entry[newCapacity];
//这个方法是干什么用的呢? -- 回答:赋值 ,
//可见每次扩容的时候都必须把原来就的数据赋值到新的数组过去,这就产生很大的开销,这也就能够解释为什么加载因子和初始量影响着HashMap性能这个问题
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
4.4 get方法分析
比较简单,紧跟4.5一起看
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
4.5 entry对象介绍
Entry有next属性变量。专门用来处理key值出现重复的情况用的,详细解释看下图片
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
//默认情况之下,next=null,当出现一个key相同的情况之下,当前的value就会被替换,同时当前next-》会指向原来的value。看图片
Entry next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry n) {
value = v;
next = n;
key = k;
hash = h;
}
.......
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry e : table) {
while(null != e) {
Entry next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
五、不足之处
5.1 HashMap获取Map集合的时间复杂度?
【回答】:与key值是否重复有关系,一般情况下g(O)1。如果出现key值重复的话,那就另外计算。key值是否重复取决于我们的Hash算法
总结:时间复杂度:你的hash算法绝对了你的效率
5.2 从伸缩性的角度分析不足之处
【回答】每当hashMap扩容的时候需要重新去add entry对象,需要重新Hash。然后放入我们新的entry table数组里面。
如果你们的工作中你知道你的hashMap需要存多少值,几千或者几万的时候,最好就是指定它们的扩容大小,防止在put的时候进行再次扩容 多次扩容