1.7HashMap源码分析
public HashMap() {
//DEFAULT_INITIAL_CAPACITY:默认的初始化容量 16
//DEFAULT_LOAD_FACTOR:默认加载因子 0.75F
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);//调用本类的其他构造器
}
public HashMap(int initialCapacity, float loadFactor) {
//三个if都是检查插入的形参是否合法
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);
//实际的加载因子被赋值为0.75
this.loadFactor = loadFactor;
//阈值 = 16; 阈值初始化为16
threshold = initialCapacity;
init();
}
public V put(K key, V value) {
//table是数组,存储键值对的数组,元素的类型Entry类型。
//如果HashMap还没有添加过元素,table就是一个空数组
if (table == EMPTY_TABLE) {
inflateTable(threshold);//阈值 = 16; 阈值初始化为16
//如果数组是空数组,长度变为16,threshold = capacity * loadFactor = 16 * 0.75 = 12
}
//HashMap允许key为null,Hashtable不允许
if (key == null)//如果key为null,特殊处理
return putForNullKey(value);
//计算key的hash值
int hash = hash(key);
//计算新的映射关系的存储下标table[i]
int i = indexFor(hash, table.length);
//先取出table[i]的头结点
//如果头结点不满足,就依次判断下面的结点 e = e.next
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果e.hash == hash 并且 要么是e.key 和新的映射关系的key地址相同或equls相同
//说明e的key与新的映射关系的key相同
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//用新的value覆盖原来的value
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//添加新的映射关系到table[i]的位置,作为table[i]的头结点,原来table[i]下面的链表连接到它next中
addEntry(hash, key, value, i);
return null;
}
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
//如果数组的长度不是2的n次方,纠正为2的n次方
int capacity = roundUpToPowerOf2(toSize);
//重新计算阈值 = capacity * loadFactor;
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//table重新建新数组 ,长度为 capacity
table = new Entry[capacity];
//暂时不管它,hash种子有关
initHashSeedAsNeeded(capacity);
}
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
//Integer.highestOneBit((number - 1) << 1):这个方法的作用就是把一个非2的你次方数字变为2的n次方的数字
}
private V putForNullKey(V value) {
//整个for循环的作用:
//(1)先取出数组table[0]的第一个元素e,如果e不为null
//(2)判断e的key是否为null,如果e.key为null,就用新的value覆盖原来的value
//(3)e=e.next,继续判断下一个结点
//所有的操作都是在table[0]下面的
//key为null的键值对,一定是存储在table[0]下面的。
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//把新的键值对(null,value)存储到table[0]的下面
addEntry(0, null, value, 0);//hash=0,key=null,value=value,bucketIndex=0
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//size:HashMap中所有键值对的个数
//size >= threshold,达到阈值 并且 table[bucketIndex]非空
//同时满足它俩的话,就会扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//把数组table扩大为原来的2倍
hash = (null != key) ? hash(key) : 0; //重写计算key的hash值
bucketIndex = indexFor(hash, table.length);//重新计算[bucketIndex]
/*
为什么数组扩容后,要重新计算下标?
index = hash & table.length-1; 如果table.length变了,就需要重新计算 [index]
*/
}
createEntry(hash, key, value, bucketIndex);
}
//本来我们想着直接使用key的hashCode()计算的结果的,但是很多时候用户自己实现的hashCode()不是很好
//冲突现象比较严重,所以他在hashCode()的基础上做了一些干扰的操作,使得hash值更分散。
//如果hash值更分散,那么存储到table中就会更均匀分布,而不是都在某个table[index]
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//取出[bucketIndex]位置的元素,即table[bucketIndex]的头结点
Entry<K,V> e = table[bucketIndex];
//table[bucketIndex]的头结点变为新结点(key,value)的Entry对象。
//原来table[bucketIndex]下面的链表作为新结点的next
table[bucketIndex] = new Entry<>(hash, key, value, e);
//元素个数增加
size++;
}
//结点类型,类似于我们在LinkedList中看到都Node,只是另一种形式的结点
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
}
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
//用key的hash值, 和数组的长度-1做运算得到下标,范围[0,table.length-1]范围内
return h & (length-1);
}
1.8HashMap源码分析
public HashMap() {
//把加载因子,初始化为0.75
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//虽然是JDK1.7的算法不同,但是仍然是为了干扰key对象的hashCode值,得到一个更加分散的hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
//tab就是table
//如果table是null或者是一个长度为0的数组
//用n记录了table的长度
if ((tab = table) == null || (n = tab.length) == 0)
//对table重新调整了一下大小,长度变为16,threshold = 12
n = (tab = resize()).length;
/*
i = (n-1) & hash = (table.length-1) & hash;
p = table[i]的头结点
如果p为null,说明table[i]还没存储过其他元素
*/
if ((p = tab[i = (n - 1) & hash]) == null)
//直接创建一个Node结点放到table[i]中,新结点的next是null
tab[i] = newNode(hash, key, value, null);
else {//如果p不为空,说明table[i]下面有其他结点
Node<K,V> e; K k;
//第一个if,是判断 table[i]的头结点是否是和新的映射关系的key重复
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;//如果重复,用e记录
else if (p instanceof TreeNode)
//如果p是树结点,就在树中查找是否有重复的key,如果有重复的用e记录哪个重复的结点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {//如果p下面是链表,就在链表中查找是否有重复的key,如果有用e记录哪个重复的结点
//一边找,一边记录当前链表的结点的个数
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//当链表的结点的个数 >= TREEIFY_THRESHOLD(树化阈值) - 1
//因为新结点还未加入,如果加入,就称为8个了
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//考虑树化
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果e不为null,说明找到了重复的,就用新的value覆盖原来的value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果++size > threshold,说明要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//用oldCap记录原来的table的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;//如果原来的table是空的,原来的容量就是0,否则就是取原来table的长度
int oldThr = threshold;//用oldThr记录原来的阈值
int newCap, newThr = 0;
//原来的Table的容量非0,相当于是对原来table的一个扩容操作
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY; //新数组的容量为默认初始化容量16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新的阈值是 16 * 0.75 = 12
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//创建了一个新的数组,长度为newCap
//让table指向新的数组
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
ArrayList源码分析
private class ArrayList<E>{
Object[] elementData;//数组用来存储ArrayList的元素
private int size;//记录实际存的元素的个数
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//空数组,长度为0的数组
/*
这里的空数组,不能存储元素。它的作用是,当我们new ArrayList(),并没有存储元素时,
并不需要申请额外的空间。因为你此时还没有存储元素,也可能不存。
例如:当你某个方法 public ArrayList select(String goodsName){根据商品名称,查询符合的商品信息
//....
//结果有可能没有找到,我们又不希望返回null,调用者有可能粗心没有检查null,会报空指针异常。
//所以我这里给它new ArrayList(),但是里面确实没有元素,所以就不要new Object[10];浪费了
}
*/
private static final int DEFAULT_CAPACITY = 10; //默认容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public boolean add(E e) {
//看是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//将新元素存储到elementData[size]位置,并且size加1
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//判断elementData是否是空数组,如果是,意味着我还未存过元素
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//minCapacity:最小需要容量
//Math.max(DEFAULT_CAPACITY, minCapacity)返回DEFAULT_CAPACITY和minCapacity中最大值
//DEFAULT_CAPACITY:10
//minCapacity是形参,调用这个方法时传入的实参值是多少它就是多少。
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
//minCapacity:最小需要容量
//elementData.length:当前数组的长度
//if (minCapacity - elementData.length > 0) 意味着当前数组不够用了
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
//oldCapacity:原来的数组的长度
int oldCapacity = elementData.length;
//newCapacity:新数组的长度 = 原来数组的长度 + 原来数组的长度 /2; 即1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//minCapacity:最小需要容量
//如果1.5倍还不够
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity; //新数组的长度就按照你需要的容量来
//新数组的长度 > 最大数组容量,就给你一个最大的数组容
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//从elementData复制元素到新数组中,新数组的长度为newCapacity
elementData = Arrays.copyOf(elementData, newCapacity);
}
public void add(int index, E element) {
rangeCheckForAdd(index);
//看是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//将[index]以及后面的元素往右移动
System.arraycopy(elementData, index, elementData, index + 1,size - index);
//将新元素添加到 elementData[index]
elementData[index] = element;
//元素个数增加
size++;
}
private void rangeCheckForAdd(int index) {
//elementData实际已经存储[0,size-1]
//可插入的位置[0,size]
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
public E remove(int index) {
rangeCheck(index);//删除时的下标检查
modCount++;
//用oldValue记录要被删除的[index]位置的元素,因为之后要返回被删除的元素
E oldValue = elementData(index);
//要移动的元素的个数
int numMoved = size - index - 1;
//如果要移动的元素的个数>0,再调用System.arraycopy()方法移动,如果是0就不调用了
//如果我们要删除的是当前数组的[size-1]的元素,意味着不需要移动,那么就不用调用System.arraycopy方法,不需要入栈,出栈,浪费时间
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
//让GC垃圾回收器,回收elementData[size-1]位置的无用元素
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {//如果o是空,那么我们看elementData中谁是null
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {//如果o非空,那么用equals比较,看哪个满足,而且用o.equals(xx)
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
public E set(int index, E element) {
rangeCheck(index);//检查下标
//先记录要被替换的[index]位置的元素
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
public E get(int index) {
rangeCheck(index);//检查下标
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
public Iterator<E> iterator() {
return new Itr();
}
//实现了Iterator接口
private class Itr implements Iterator<E> {
//游标,当前迭代器遍历到动态数组哪个位置了
int cursor; // index of next element to return
//上一个迭代的位置
int lastRet = -1; // index of last element returned; -1 if no such
//后面单独讲
int expectedModCount = modCount;
public boolean hasNext() {
//有效元素的范围[0,size-1]
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
//用i记录当前迭代器遍历到动态数组哪个位置了
int i = cursor;
//加这个判断是以防用户在next()方法之前没有调用hasNext()方法
if (i >= size)
throw new NoSuchElementException();
//用一个变量记录了当前动态数组的elementData
Object[] elementData = ArrayList.this.elementData;
//和并发有关
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;//下次访问的元素的下标
return (E) elementData[lastRet = i];//i是本次遍历的数组的下标位置,用lastRet记录,对应下一次调用next方法,lastRet就是上次的下标
//变量是在remove方法中用到了
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
//删除的是你刚刚调用next()方法取走的位置的元素
ArrayList.this.remove(lastRet);
cursor = lastRet;//因为删除元素,[cursor]会被往前移动,所以这里要cursor = lastRet
lastRet = -1;//因为[lastRet]位置被删除了,然后不存在了,如果连续调用remove()就会报错
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
LinkedList的源码分析
public class LinkedList<E>{
int size = 0;
Node<E> first;//头结点
Node<E> last;//尾结点
private static class Node<E> {
E item; //数据项
Node<E> next; //下一个结点的地址
Node<E> prev; //上一个结点的地址
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
public boolean add(E e) {
linkLast(e);//连接到最后
return true;
}
void linkLast(E e) {
//last是当前链表的最后一个结点
final Node<E> l = last;
//创建新结点
//Node(Node<E> prev, E element, Node<E> next) :
//新结点的prev是 原来链表的最后一个结点l,即让新结点的prev指向原来的最后一个结点
//新结点的next是 null
//新结点的数据项是:e
final Node<E> newNode = new Node<>(l, e, null);
//新结点变成了链表的最后一个结点
last = newNode;
//l是原来链表的最后一个结点
//如果l==null,说明原来链表是一个空链表
if (l == null)
//first:链表的第一个结点,
//现在这个新结点即是第一个结点,又是最后一个结点
first = newNode;
else//l==null不成立,说明原来的链表非空
//l是原来链表的最后一个结点,原来的最后一个结点指向了新结点
l.next = newNode;
//结点个数增加
size++;
modCount++;
}
public void add(int index, E element) {
checkPositionIndex(index);//检查index是否合法,合法的位置[0,size]
if (index == size)//连接到最后
linkLast(element);
else //否则,就是插入到中间
linkBefore(element, node(index));
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
//返回了链表的[index]位置的结点
Node<E> node(int index) {
// assert isElementIndex(index);
//index < (size >>1) 即 index < size/2,说明位置在链表的前半段
if (index < (size >> 1)) {
//从头开始找比较合适
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {//否则,说明位置在链表的后半段
//从最后往前找比较合适
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
//e:是要新增的结点的数据
//succ是[index]位置的结点
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
//pred是[index]位置的前一个结点
final Node<E> pred = succ.prev;
//创建新结点,Node(Node<E> prev, E element, Node<E> next)
//新结点的prev指向[index]位置的前一个结点
//新结点的next指向原来[index]的结点
final Node<E> newNode = new Node<>(pred, e, succ);
//原来[index]位置的结点 的prev指向新结点
succ.prev = newNode;
//原来[index]位置的前一个结点是null,说明要插入的位置是头结点的位置
if (pred == null)
//新结点就是新的头结点
first = newNode;
else
//否则原来[index]位置的前一个结点的next指向新结点
pred.next = newNode;
size++;
modCount++;
}
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {//如果条件满足,说明x是要被删除的结点
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {//如果条件满足,说明x是要被删除的结点
unlink(x);
return true;
}
}
}
return false;
}
//x是要被删除的结点
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;//被删除结点的数据
final Node<E> next = x.next;//被删除结点的下一个结点
final Node<E> prev = x.prev;//被删除结点的上一个结点
//被删除结点的上一个结点是null,说明被删除结点是第一个结点
if (prev == null) {
//第一个结点变为被删除结点的下一个结点
first = next;
} else {
//否则被删除结点的上一个结点 的next 指向 被删除结点的下一个结点
prev.next = next;
//被删除结点与它之前的上一个结点断开
x.prev = null;
}
//被删除结点的下一个结点是null,说明被删除结点是最后一个结点
if (next == null) {
//链表的最后一个结点,变成被删除结点的上一个结点
last = prev;
} else {
//否则,被删除结点的下一个结点的prev = 被删除结点的上一个结点
next.prev = prev;
//被删除结点与它之前的下一个结点断开
x.next = null;
}
//把被删除结点的数据置空
x.item = null;//经过x.prev = null ,x.next == null, x.item==null,相当于x与其结点断开关系,数据也置空,可以彻底被回收
//元素个数减少
size--;
modCount++;
return element;
}
public E remove(int index) {
checkElementIndex(index);//检查index是否合法,合法的位置[0,size-1]
return unlink(node(index));
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
public E get(int index) {
checkElementIndex(index);
//node()返回了链表的[index]位置的结点
return node(index).item;
}
}
关于HashMap的面试问题
1、HashMap的底层实现
答:JDK1.7及其之前的版本是数组+链表,JDK1.8是数组+链表/红黑树
2、HashMap的数组的元素类型
答:java.util.Map$Entry接口类型。
JDK1.7的HashMap中有内部类Entry实现Entry接口
JDK1.8的HashMap中有内部类Node和TreeNode类型实现Entry接口,并且TreeNode是Node的子类。
3、为什么要使用数组?
答:因为数组的访问的效率高或者说,根据[下标]操作效率高
4、为什么数组还需要链表?或问如何解决hash或[index]冲突问题?
答:为了解决hash和[index]冲突问题
(1)两个不相同的key的hashCode值本身可能相同
(2)两个不相同的key的hashCode值不同,但是经过hash()运算,结果相同
(3)两个hashCode不相同的key,经过hash()运算,结果也不相同,但是通过 hash & table.length-1运算得到的[index]可能相同
那么意味着table[index]下可能需要存储多个Entry的映射关系对象,所以需要链表
5、HashMap的数组的初始化长度
答:默认的初始容量值是16,也可以手动指定
6、HashMap的映射关系的存储索引index如何计算
答:hash & table.length-1
7、为什么要使用hashCode()? 空间换时间
答:因为hashCode()是一个整数值,可以用来直接计算index,效率比较高,用数组这种结构虽然会浪费一些空间,但是可以提高查询效率。
8、hash()函数的作用是什么
答:在计算index之前,会对key的hashCode()值,做一个hash(key)再次哈希的运算,这样可以使得Entry对象更加散列的存储到table中
JDK1.8关于hash(key)方法的实现比JDK1.7要简洁。 key.hashCode() ^ key.Code()>>>16; 因为这样可以使得hashCode的高16位信息也能参与到运算中来
9、HashMap的数组长度为什么一定要是2的幂次方
答:因为2的n次方-1的二进制值是前面都0,后面几位都是1,这样的话,与hash进行&运算的结果就能保证在[0,table.length-1]范围内,而且是均匀的。
10、HashMap 为什么使用 &按位与运算代替%模运算?
答:因为&基于二进制补码的位运算符,效率高,
11、HashMap的数组什么时候扩容?
答:JDK1.7版:当要添加新Entry对象时发现(1)size达到threshold(2)table[index]!=null时,两个条件同时满足会扩容
JDK1.8版:当要添加新Entry对象时发现(1)size达到threshold(2)当table[index]下的结点个数达到8个但是table.length又没有达到64。两种情况满足其一都会导致数组扩容
而且数组一旦扩容,不管哪个版本,都会导致所有映射关系重新调整存储位置。
12、如何计算扩容阈值(临界值)?
答:threshold = capacity * loadfactor
13、loadFactor为什么是0.75,如果是1或者0.1呢有什么不同?
答:1的话,会导致某个table[index]下面的结点个数可能很长
0.1的话,会导致数组扩容的频率太高
14、JDK1.8的HashMap什么时候树化?
答:当table[index]下的结点个数达到8个但是table.length已经达到64
15、JDK1.8的HashMap什么时候反树化?
答:当table[index]下的树结点个数少于等于6个
(1)当继续删除table[index]下的树结点,最后这个根结点的左右结点有null,会反树化
(2)当重新添加新的映射关系到map中,导致了map重新扩容了,这个时候如果table[index]下面还是小于等于6的个数,那么会反树化
16、JDK1.8的HashMap为什么要树化?
答:因为当table[index]下的结点个数超过8个后,查询效率就低下了,修改为红黑树的话,可以提高查询效率
17、JDK1.8的HashMap为什么要反树化?
答:因为因为当table[index]下树的结点个数少于6个后,使用红黑树反而过于复杂了,此时使用链表既简洁又效率也不错
18、作为HashMap的key类型重写equals和hashCode方法有什么要求
(1)equals与hashCode一起重写
(2)重写equals()方法,但是有一些注意事项;
-
自反性:x.equals(x)必须返回true。 对称性:x.equals(y)与y.equals(x)的返回值必须相等。 传递性:x.equals(y)为true,y.equals(z)也为true,那么x.equals(z)必须为true。 一致性:如果对象x和y在equals()中使用的信息都没有改变,那么x.equals(y)值始终不变。 非null:x不是null,y为null,则x.equals(y)必须为false。
(3)重写hashCode()的注意事项
-
如果equals返回true的两个对象,那么hashCode值一定相同,并且只要参与equals判断属性没有修改,hashCode值也不能修改; 如果equals返回false的两个对象,那么hashCode值可以相同也可以不同; 如果hashCode值不同的,equals一定要返回false; hashCode不宜过简单,太简单会导致冲突严重,hashCode也不宜过于复杂,会导致性能低下;
19、为什么大部分 hashcode 方法使用 31?
答:因为31是一个不大不小的素数,而且是一个2的n次方-1的一个素数。用这样的一个数来计算,底层使用二进制计算效率会更高,hashCode冲突概率比较低。
20、请问已经存储到HashMap中的key的对象属性是否可以修改?为什么?
答:如果该属性参与hashCode的计算,那么不要修改。因为一旦修改hashCode()已经不是原来的值。 而存储到HashMap中时,key的hashCode()-->hash()-->hash已经确定了,不会重新计算。用新的hashCode值再查询get(key)/删除remove(key)时,算的hash值与原来不一样就不找不到原来的映射关系了。
21、所以为什么,我们实际开发中,key的类型一般用String和Integer
答:因为他们不可变。
22、为什么HashMap中的Node或Entry类型的hash变量与key变量加final声明?
答:因为不希望你修改hash和key值
23、为什么HashMap中的Node或Entry类型要单独存储hash?
答:为了在添加、删除、查找过程中,比较hash效率更高,不用每次重新计算key的hash值
24、请问已经存储到HashMap中的value的对象属性是否可以修改?为什么?
答:可以。因为我们存储、删除等都是根据key,和value无关。
25、如果key是null是如何存储的?
答:会存在table[0]中