现在面试被问到的很多问题都和集合有关,所以我在这里总结一下
1)同步性:Vector 是线程安全的(同步),而ArrayList 是线程序不安全的;
2)数据增长:当需要增长时,Vector 默认增长一倍,而ArrayList 却是一半。
通过put和get存储和获取对象,存储对象时,我们将K/V传给put方法时,它调用hashcode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量。获取对象时,我们将K传递给get,他调用hashcode计算hash从而得到bucket位置,并进一步调用equals()方法确认键值对。
在这里不得不提一下HashMap几个特性:
默认大小为16( 2 的4次方)
扩容机制:当目前数超过总数的75%时扩展,扩展为原来的2倍,也就是2的n+1次方。
最大存2的30次方,在空间大于8的时候会变成红黑树的数据结构,当又小于6的时候又会变回链表
List和Set都继承Collection,但是Map不是Collection的子接口。
List 可以允许重复的元素 可以插入多个null元素 有序的容器,插入的顺序和输出的顺序一样
Set 不允许重复元素 只允许一个null元素 无序容器
Map 键值对存储,键必须唯一,但是值可以重复 键只允许一个null,值可以允许有多个null
List 和 Set 是存储单列数据的集合,Map 是存储键和值这样的双列数据的集合;List 中存储的数据是有顺序,并且允许重复;Map 中存储的数据是没有顺序的,其键是不能重复的,它的值是可以有重复的,Set 中存储的数据是无序的,且不允许有重复,但元素在集合中的位置由元素的 hashcode 决定,位置是固定的(Set 集合根据 hashcode进行数据的存储,所以位置是固定的,但是位置不是用户可以控制的,所以对于用户来说 set 中的元素还是无序的);
拉链法解决冲突的做法是: 将所有关键字为同义词的结点链接在同一个单链表中 。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于1,但一般均取α≤1。拉链法适合未规定元素的大小。
当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。如果有两个值对象储存在同一个bucket,将会遍历LinkedList直到找到值对象。找到bucket位置之后,会调用keys.equals()方法去找到LinkedList中正确的节点,最终找到要找的值对象。(当程序通过 key 取出对应 value 时,系统只要先计算出该 key 的 hashCode() 返回值,在根据该 hashCode 返回值找出该 key 在 table 数组中的索引,然后取出该索引处的 Entry,最后返回该 key 对应的 value 即可。)
在多线程的情况下,当重新调整HashMap大小的时候,就会存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历。如果条件竞争发生了,那么就会产生死循环了。
对于HashMap这个类大家一定不陌生,想必多多少少用过或者了解过,今天我来和大家谈谈HashMap的源码,JDK为1.8
继承AbstractMap类,实现map接口等等
当你不设置它的容量时默认的容量大小 为16
这个属性代表着它的最大容量,大小可以理解为2的30次方
负载因子,如果构造方法没有指定负载因子的话默认0.75,也就是当它容量达到百分之75的时候扩容
这个属性代表着如果它的链表结构长度达到了8的时候链表的数据结构将会变成红黑树,提高效率
如果链表结构长度小于6的时候又会从红黑树变成链表,因为当你长度就这么长的时候链表性能反而更好
容器可以树化的最小表容量,可以理解为当它变成红黑树的最小表容量
在第一次使用的时候初始化
保存缓存的set映射 遍历map的时候需要
长度,不多说
代表它被修改的次数
常用的属性就介绍到这里,接下来我们来详细看看HashMap是怎么样put添加数据的
put方法直接是返回putVal,那我们详细看看这个方法
注释写在代码中
/**
*put方法的内部
* Implements Map.put and related methods
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent false, boolean evict true) {
Node[] tab; Node p; int n, i;
//如果是空,也就代表是第一次put ,
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; //生成初始化的长度
if ((p = tab[i = (n - 1) & hash]) == null) //如果值==null,则证明这个数据为null
tab[i] = newNode(hash, key, value, null); //hash为key通过hash方法的到 将tab的第0个值设置为null
else { //反之则不为空时
Node e; K k; //创建一个node<>和 一个key类型的对象 k
if (p.hash == hash && //如果p的hash和key的hash一致并且p的key==k或者参数key不为空并且key equals k 则将p赋给e
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode) //如果p是TreeNode类型的则进入 putTreeVal方法
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { //如果p.next为空,则新建一个node
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //如果循环次数>=变成红黑树的要求数
treeifyBin(tab, hash); //调整表的大小
break;
}
if (e.hash == hash && //相同hash和key则停止遍历
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; //将e赋值给p
}
}
if (e != null) { //如果e!=null,则表示map中的键有和put的键重复的,将会返回旧数据的value
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; //更改次数+1
if (++size > threshold) //长度+1,如果长度大于需要扩容的大小时 resize初始化或加倍表格大小
resize();
afterNodeInsertion(evict); //节点插入后的一个回调方法
return null;
}
}
其中这个方法又调用了resize()方法
//初始化或加倍表格大小。如果为null,则分配符合字段阈值中保存的初始容量目标。
//否则,因为我们正在使用2次幂扩展,所以每个bin中的元素必须保持在相同的索引处,
//或者在新表中以2的偏移量移动。
final Node[] resize() {
Node[] oldTab = table;//table为map初始化的node
int oldCap = (oldTab == null) ? 0 : oldTab.length; //如果为空则返回0,否则返回oldTab的长度
int oldThr = threshold;//threshold表示到达该数map将会扩容 例如刚开始新建为16*0.75
int newCap, newThr = 0;//newThr为要达到扩容的数 newCap为map的大小容量
if (oldCap > 0) { //如果map初始化的node大于0
if (oldCap >= MAXIMUM_CAPACITY) { //如果大于或者等于最大允许容量 1<<30
threshold = Integer.MAX_VALUE; //threshold 等于int的最大数值
return oldTab; //将map初始化的node返回
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && //如果 newCap==0或者大小正常 再将oldCap*2赋给newCap,判断这个newCap是否小于1<<30并且大于默认长度16
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold //newThr则为下一个调整扩容的2倍
}
else if (oldThr > 0) // initial capacity was placed in threshold //如果到达扩容数大于0,newCap得到到达扩容数的值
newCap = oldThr;
else { //零初始阈值表示使用默认值
newCap = DEFAULT_INITIAL_CAPACITY; //newCap为默认map大小16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //newThr为默认将要到达则需要扩容的数
}
if (newThr == 0) { //如果将要扩容数newThr为0,
float ft = (float)newCap * loadFactor; //ft为map新容量*哈希表的加载因子 默认是0.75,但可以new对象时可以手动设置
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE); //当map新容量小于1<<30时并且ft小于1<<30时
返回ft不然为int最大值 newThr为将要达到扩容时数值 (第一次为12)
}
threshold = newThr; //将newThr赋值给达到将要扩容的数
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap]; //new一个容量为新大小的tab
table = newTab; //将新大小的tab赋值给 一个抑制的table
if (oldTab != null) { //如果旧tab不是空
for (int j = 0; j < oldCap; ++j) { //遍历旧tab
Node e;
if ((e = oldTab[j]) != null) { //当前node不为空
oldTab[j] = null; //赋值为空
if (e.next == null) //如果下一个为空
newTab[e.hash & (newCap - 1)] = e; //&代表如果前面的为false后面的将不会执行
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
else { // preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node 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;
}
https://blog.csdn.net/qq_41594146/article/details/84840689
https://blog.csdn.net/qq_41594146/article/details/84842763