一、什么是HashMap
- Hash散列:将一个任意的长度通过某种(Hash算法)算法转换成一个固定的值。
- Map:地图 x,y存储
- 总结:通过HASH出来的值,然后通过这个值 值定位到这个map,然后value存储到这个map中
- 疑问
- 1.key可以为空吗?
- Null当成一个key来存储
- 2.如果hash key 重复了 value会覆盖吗?
- 不会覆盖,而是将原来的值赋给oldValue,然后将新的值赋给value。原来的对象存放在entry对象的next里面, 例如:
- 1.key可以为空吗?
Map map = new HashMap();
map.put("name", "zhangsan");
//调用put方法时,若key没有重复,返回值为null;若重复,则为返回之前存储的值。
map.put("name","wanger"); //此处会返回"zhangsan"
- 3.HashMap什么时候扩容?
– 执行put()方法的时候,阈值达到容量的3/4会进行扩容!
– 扩容为偶数依次为: 16 2×16=32 2×32=62 2×64 ……
– HashMap的table: 数组+链表 数据结构
二、源码分析
1.初始化参数介绍
- int initCapacity (初始化数组大小,默认值为16)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
- int maximum_capacity(数组最大容量)
static final int MAXIMUM_CAPACITY = 1 << 30;
- float loadFactor (负载因子,默认值为0.75f)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
- table:初始化一个名为table的entry数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
2.put方法分析
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
//首先根据key值计算出HashCode值
int hash = hash(key.hashCode());
/*然后通过一个indexFor方法计算出此元素该存放于table数组的哪个位置, 再检测此table的此坐标位置的entry链是否存在此key或此key值,若存在 则更新此元素的value值*/
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//添加当前的表项到i的位置
addEntry(hash, key, value, i);
return null;
}
3.get方法分析
public V get(Object key) {
//键为null的entry在table[0]
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//先根据key获取hash值,与put方法一致。
int hash = (key == null) ? 0 : hash(key);
// 同样通过位与运算获取Entry[] tables下标
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
// 当匹配到hash值相同,key相同的Entry元素时,返回Entry对象的vlaue
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
4.扩容源码分析
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//生成一个新的table数组(entry对象数组)
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
//根据新的Capacity和负载因子去生成新的临界值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/* 因为table数组的容量增加了,那么相应的table的length也增加了,那么之前存储的元素的位置 也就不一样了,比如之前的length是16,现在的length是32,那么hashCode模16和HashCode 模32的结果很有可能会不一样,所以就只有重新去计算新的位置,方法是遍历数组,再遍历数组上 的entry链*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> 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、总结
当负载因子较大时,去给table数组扩容的可能性就会少,所以相对占用内存较少(空间上较少),但是每条entry链上的元素会相对较多,查询的时间也会增长(时间上较多)。反之就是,负载因子较少的时候,给table数组扩容的可能性就高,那么内存空间占用就多,但是entry链上的元素就会相对较少,查出的时间也会减少。所以才有了负载因子是时间和空间上的一种折中的说法。所以设置负载因子的时候要考虑自己追求的是时间还是空间上的少。
注意:设置initCapacity的时候,尽量设置为2的幂,这样会去掉计算比initCapactity大,且为2的幂的数的运算。
三、手写实现
1.定义Map接口
public interface Map<K,V> {
public V put(K k,V v);
public V get(K k);
public int size();
//HashMap内部的Entry对象
public interface Entry<K,V>{
public K getKey();
public V getValue();
}
}
2.实现类HashMap
public class HashMap<K,V> implements Map<K, V> {
private static int defaultLength = 16;
private static float defaultLoader = 0.75f;
private Entry<K,V>[] table = null;
private int size = 0;
public HashMap(int length, float loader) {
defaultLength = length;
defaultLoader = loader;
table = new Entry[defaultLength];
}
public HashMap() {
this(defaultLength,defaultLoader);
}
public V put(K k, V v) {
size++;
int index = hash(k);
Entry<K,V> entry = table[index];
if(entry == null){
table[index] = newEntry(k,v,null);
}else{
table[index] = newEntry(k, v, entry);
}
return table[index].getValue();
}
public V get(K key) {
int index = hash(key);
if(table[index] == null){
return null;
}
return find(key,table[index]);
}
public int size() {
return size;
}
class Entry<K,V> implements Map.Entry<K, V>{
K k;
V v;
Entry<K,V> next;
public Entry(K k, V v, Entry<K,V> next) {
this.k = k;
this.v = v;
this.next = next;
}
public K getKey() {
return k;
}
public V getValue() {
return v;
}
}
//key->hash
public int hash(K k){
int m = defaultLength;
int i = k.hashCode() % m;
return i >= 0 ? i : -i;
}
private Entry<K, V> newEntry(K k, V v, Entry<K,V> next) {
return new Entry<K,V>(k,v,next);
}
//根据key和table查找元素
private V find(K key, Entry<K,V> entry) {
if(key == entry.getKey() || key.equals(entry.getKey())){
if(entry.next != null){
System.out.println("oldValue:"+entry.next.getValue());
}
return entry.getValue();
}
else{
if(entry.next != null){
return find(key,entry.next);
}
}
return null;
}
}
四、不足之处(伸缩性角度)
1.伸缩性
每当hashmap扩容的时候需要重新去add entey对象 需要重新Hash,然后放入我们新的entry table数组里面
当我们知道HashMap需要存多少值(或值特别大的时候),最好指定他们的扩容大小,防止在put的时候进行多次再扩容
2.时间复杂度
时间复杂度与key是否冲突有关(理想状态为O(1)), hash算法决定了效率