List集合
ArrayList 扩容机制
Java - ArrayList 中 elementData 为什么被 transient 修饰?
Array 和 ArrayList 有何区别?
- Array 可以存储基本数据类型和对象,ArrayList 只能存储对象
- Array 是指定固定大小的,而 ArrayList 大小是自动扩展的
- Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有
如何实现 Array 和 List 之间的转换?
Array 转 List: Arrays. asList(array) ;
List 转 Array:List 的 toArray() 方法。
Arrays.asList()的集合方法add()和remove()报错
我们其实可以用真正的ArraysList包装一下,这样就不会报错啦
//添加数据
List<Integer> numberList = new ArrayList(Arrays.asList(1));
numberList.add(2);
//遍历
for (Integer num : numberList) {
System.out.println(num);
}
线程安全的ArrayList
线程安全版的ArrayList的性能比较–Collections.synchronizedList与CopyOnWriteArrayList
Iterator,Iterable
RandomAccess 作用 及 binarySearch方法详解
Java容器的快速报错机制 ConcurrentModificationException 异常原因和解决方法
见链接(博客里还有一些其他并发编程的知识点)
hashcode 值是如何计算的?
hashCode真的是内存地址吗
什么是hashCode 以及 hashCode()与equals()的联系
https://blog.youkuaiyun.com/imagineluopan/article/details/121852635
https://blog.youkuaiyun.com/qq_42002006/article/details/118542766
Comparable 和 Comparator的用法
TreeMap 和 TreeSet 在排序时如何比较元素? Collections 工具类中的 sort()方法如何比较元素?
- TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。
- TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进行排序。
- Collections 工具类的 sort 方法有两种重载的形式,
第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;
第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。
Queue
SynchronousQueue实现原理
Map集合
红黑树的定义
HashMap底层实现源码分析
1. 成员变量
(1)loadFactor:装载因子。
(2)threshold:阈值。threshold的值等于table.length * loadFactor
对于this.threshold = tableSizeFor(var1) 的疑问
:
tableSizeFor(initialCapacity)判断指定的初始化容量是否是2的n次幂,如果不是那么会变为比指定初始化容量大的最小的2的n次幂。
但是注意,在tableSizeFor方法体内部将计算后的数据返回给调用这里了,并且直接赋值给threshold边界值了。有些人会觉得这里是一个bug,应该这样书写:
this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;
这样才符合threshold的意思(当HashMap的size到达threshold这个阈值时会扩容)。
但是请注意,在jdk8以后的构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算。
(3)size: 存放元素的个数,注意这个不等于数组的长度
当 size > threshold 时进行扩容
2. 构造方法
public HashMap(int var1, float var2) {
if (var1 < 0) {
throw new IllegalArgumentException("Illegal initial capacity: " + var1);
} else {
if (var1 > 1073741824) {
var1 = 1073741824;
}
if (var2 > 0.0F && !Float.isNaN(var2)) {
this.loadFactor = var2;
this.threshold = tableSizeFor(var1);
} else {
throw new IllegalArgumentException("Illegal load factor: " + var2);
}
}
}
public HashMap(int var1) {
this(var1, 0.75F);
}
public HashMap() {
this.loadFactor = 0.75F;
}
public HashMap(Map<? extends K, ? extends V> var1) {
this.loadFactor = 0.75F;
this.putMapEntries(var1, false);
}
3. 成员方法
(1)put 方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int var1, K var2, V var3, boolean var4, boolean var5) {
HashMap.Node[] var6;
int var8;
if ((var6 = this.table) == null || (var8 = var6.length) == 0) {
var8 = (var6 = this.resize()).length;
}
Object var7;
int var9;
if ((var7 = var6[var9 = var8 - 1 & var1]) == null) {
var6[var9] = this.newNode(var1, var2, var3, (HashMap.Node)null);
} else {
Object var10;
Object var11;
if (((HashMap.Node)var7).hash == var1 && ((var11 = ((HashMap.Node)var7).key) == var2 || var2 != null && var2.equals(var11))) {
var10 = var7;
} else if (var7 instanceof HashMap.TreeNode) {
var10 = ((HashMap.TreeNode)var7).putTreeVal(this, var6, var1, var2, var3);
} else {
int var12 = 0;
while(true) {
if ((var10 = ((HashMap.Node)var7).next) == null) {
((HashMap.Node)var7).next = this.newNode(var1, var2, var3, (HashMap.Node)null);
if (var12 >= 7) {
this.treeifyBin(var6, var1);
}
break;
}
if (((HashMap.Node)var10).hash == var1 && ((var11 = ((HashMap.Node)var10).key) == var2 || var2 != null && var2.equals(var11))) {
break;
}
var7 = var10;
++var12;
}
}
if (var10 != null) {
Object var13 = ((HashMap.Node)var10).value;
if (!var4 || var13 == null) {
((HashMap.Node)var10).value = var3;
}
this.afterNodeAccess((HashMap.Node)var10);
return var13;
}
}
++this.modCount;
if (++this.size > this.threshold) {
this.resize();
}
this.afterNodeInsertion(var5);
return null;
}
put 方法执行逻辑:
- 判断table是否为null,如果是则进行初始化
- 通过 hash^(length-1) 把元素散列到桶上
- 如果桶上没有节点,直接插入
- 如果桶上有节点
- 如果链表已经是红黑树,putTreeVal()
- 普通链表,则尾插法。插入后判断是否转红黑树
- 如果有相同的key,替换value
- 如果 size > threshold 则扩容
(2)resize 方法
final HashMap.Node<K, V>[] resize() {
HashMap.Node[] var1 = this.table;
int var2 = var1 == null ? 0 : var1.length;
int var3 = this.threshold;
int var5 = 0;
int var4;
if (var2 > 0) {
if (var2 >= 1073741824) {
this.threshold = 2147483647;
return var1;
}
// 对于 已经有数据的 hashmap:put元素后 size > threshold 进行扩容
if ((var4 = var2 << 1) < 1073741824 && var2 >= 16) {
var5 = var3 << 1;
}
} else if (var3 > 0) {
// 对于 有参构造器生成的 hashmap 进行扩容
var4 = var3;
} else {
// 对于 无参构造器生成的 hashmap 进行扩容
var4 = 16;
var5 = 12;
}
// 对于 有参构造器生成的 hashmap 进行扩容
if (var5 == 0) {
float var6 = (float)var4 * this.loadFactor;
var5 = var4 < 1073741824 && var6 < 1.07374182E9F ? (int)var6 : 2147483647;
}
this.threshold = var5;
HashMap.Node[] var14 = (HashMap.Node[])(new HashMap.Node[var4]);
// var14 是扩容之后的空数组
this.table = var14;
// 判空是因为,对于初始化的 map 集合不用执行
if (var1 != null) {
// 遍历 map 集合的table数组,从下标0 到length长度
for(int var7 = 0; var7 < var2; ++var7) {
HashMap.Node var8;
// var8 表示每个链表的第一个节点,只有不为null才有必要执行if
if ((var8 = var1[var7]) != null) {
var1[var7] = null;
if (var8.next == null) {
// 如果第二个节点为null(链表只有一个头节点)的执行逻辑
// var8.hash & (var4 - 1) 计算元素散列的位置,即元素在新数组的下标位置
var14[var8.hash & var4 - 1] = var8;
} else if (var8 instanceof HashMap.TreeNode) {
// 如果链表已经是红黑树结构了,处理逻辑如下(目前不具体分析)
((HashMap.TreeNode)var8).split(this, var14, var7, var2);
} else {
// 链表有超过1个节点的处理逻辑————变量比较多,下面我会配合图解,建议对比着看
// var9 用来记录扩容后一个链表上位置不变的节点
HashMap.Node var9 = null;
HashMap.Node var10 = null;
// var11 用来记录扩容后一个链表上位置变化的节点
HashMap.Node var11 = null;
HashMap.Node var12 = null;
HashMap.Node var13;
// do while 遍历整个链表
do {
var13 = var8.next;
// if 中的判断条件 逻辑十分巧妙,优化了重新分配元素的方法,下面有分析,可以先去看分析再来看代码。
if ((var8.hash & var2) == 0) { // 说明 元素所在数组下标位置不变
if (var10 == null) {
var9 = var8;
} else {
var10.next = var8;
}
var10 = var8;
} else { // 说明 元素所在数组下标位置要改变
if (var12 == null) {
var11 = var8;
} else {
var12.next = var8;
}
var12 = var8;
}
var8 = var13;
} while(var13 != null);
// 元素所在数组下标位置不变。这些节点都用 var9 保存着
if (var10 != null) {
var10.next = null;
var14[var7] = var9;
}
// 元素所在数组下标位置+原数组长度(比如从length = 4(0100) 扩容到 length = 16(0001 0000),则加上4)。这些节点都用 var11 保存着
if (var12 != null) {
var12.next = null;
var14[var7 + var2] = var11;
}
}
}
}
}
return var14;
}
resize 方法执行逻辑:
- 扩容 table 数组,对三种 hashmap 进行扩容
- 已经有数据的 hashmap:put元素后 size > threshold
- 自定义构造器生成的 hashmap
- 无参构造器生成的 hashmap
- 重新分配元素,对于刚初始化的hashmap不用
重新分配元素,正常的逻辑是:遍历元素,重新计算每个元素的hash值,hash&(length-1),再分配到相应的桶中。但是,源码中做出了优化。 参考文章
我们在扩充 HashMap 的时候,不需要重新计算 hash,只需要看看原来的 hash 值新增的那个 bit 是 1 还是 0 就可以了,是 0 的话索引没变,是 1 的话索引变成 “原位置 + 原数组长度”
体现在代码中的什么地方呢:
在resize方法中,如果你看到这样的逻辑 hashcode & length 那就是了
if ((var8.hash & var2) == 0) { // 体现在这里
if (var10 == null) {
var9 = var8;
} else {
var10.next = var8;
}
var10 = var8;
} else {
if (var12 == null) {
var11 = var8;
} else {
var12.next = var8;
}
var12 = var8;
}
tableSizeFor分析:扩容后的大小一定是2的n次幂
static final int tableSizeFor(int var0) {
int var1 = var0 - 1;
var1 |= var1 >>> 1;
var1 |= var1 >>> 2;
var1 |= var1 >>> 4;
var1 |= var1 >>> 8;
var1 |= var1 >>> 16;
return var1 < 0 ? 1 : (var1 >= 1073741824 ? 1073741824 : var1 + 1);
}
源码分析
var0 = 0000 0011 0000 0000 0000 0000 0000 0001
var1 = 0000 0011 0000 0000 0000 0000 0000 0000
var1 |= var1 >>> 1;
var1 >>> 1 = 0000 0001 1000 0000 0000 0000 0000 0000
var1 |= var1 >>> 1 = 0000 0011 1000 0000 0000 0000 0000 0000
var1 |= var1 >>> 2;
var1 >>> 2 = 0000 0000 1110 0000 0000 0000 0000 0000
var1 |= var1 >>> 2 = 0000 0011 1110 0000 0000 0000 0000 0000
var1 |= var1 >>> 4;
var1 >>> 4 = 0000 0000 0011 1110 0000 0000 0000 0000
var1 |= var1 >>> 4 = 0000 0011 1111 1110 0000 0000 0000 0000
var1 |= var1 >>> 8;
var1 >>> 8 = 0000 0000 0000 0011 1111 1110 0000 0000
var1 |= var1 >>> 8 = 0000 0011 1111 1111 1111 1110 0000 0000
var1 |= var1 >>> 16;
var1 >>> 16 = 0000 0000 0000 0000 0000 0011 1111 1111
var1 |= var1 >>>16 = 0000 0011 1111 1111 1111 1111 1111 1111
java7 HashMap 的死循环分析
ConcurrentHashMap 1.7 & 1.8 源码分析
为什么 Java7 采用头插法,而 Java8 却改成了尾插法?
头插法插入会改变链表的顺序,导致并发情况下可能出现环形链表的情况,而改为尾插法之后,由于新插入元素之后维持原来链表的顺序不变,不会有环形链表的情况出现,但是在并发的情况下,会出现值覆盖的情况。
图文分析 java7 transfer方法:
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;
//如果hashSeed变了,需要重新计算hash值
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;
}
}
}
java7 transfer方法图片分析 有一点不严谨:
hashmap的长度应该是2的n次幂,size的长度应该满足size <= length * threshold
但是我并没有考虑这一点。
如果有其他问题,我希望大家能帮我指出来,谢谢。