目录
一、整体关系
二、Collection
List
1.ArrayList
(1)可以加入多个null值
(2)ArrayList底层维护了一个Object类型的可变数组,有transient关键字,表示该数组不会被序列化
其扩容机制为:
①当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第1 次添加,则扩容elementData为10, 如需要再次扩容,则扩容elementData为1.5倍。
②如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍。
效果展示①
从debug中可以知道arrayList无参构造器创建后是为空的。
在加入一个元素之后数组扩容为大小为10的数组
在超过10容量时,数组会进行1.5倍扩容,成为大小为15的数组,如果再超过15,那么就是扩容为
大小为22的数组,再是33,49,73.....
效果展示②
这里可以看到,如果给出初始容量时,数组会初始化为该容量大小
当添加第6个数的时候,数组容量大小不够,会进行1.5倍扩容
容量5扩容后容量为7,当超过7之后,还是按照1.5倍扩容
源码解析①
从这里可以看出调用的无参构造器是会创建一个空数组
当加入第一个元素时,因为数组为空,会调用下面的方法来确定扩容大小(这里并没有对数组进行真正的扩容,只是确定了在数组中放入元素需要的最小容量,理论上放入一个元素只需要1个大小的容量,但是java默认空数组直接扩容为10)
因为前面传入的参数minCapacity是为1的(数组大小 + 1),所以返回较大的默认大小10
这个方法里面的grow方法才是进行真正的扩容(modCount只是用来统计修改次数的)
private void grow(int minCapacity) {
int oldCapacity = elementData.length; //原数组的容量大小,不是元素个数
//(oldCapacity >> 1)是向右位移1位,简单来说就是除以2,
//所以等价于 oldCapacity + (oldCapacity / 2) 约等于 1.5倍的oldCapacity
//如果是空数组,则newCapacity还是0
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果1.5倍原数组大小比所需最小容量小,则数组直接扩容成所需最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果新数组容量超过一定大小,则换用更大的扩容方法
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//创建一个新数组,同时将原先的数据复制进去
elementData = Arrays.copyOf(elementData, newCapacity);
}
最后把要加入的数据加入进去
当加入第二个数据时,因为之前已经扩容为10,则会跳过grow方法,直接加入数据
源码解析②
如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍。
当给定一个参数后,会调用下图有参构造器
后面的与源码解析①差不多一样,不过还有一个顺便说一下,就是当给定容量为1时,如果一直按照1.5倍扩容,那么则会一直保持为1,所以grow方法中的有if语句防止了这种情况。
2.Vector
Vector和ArrayList基本上差不多,不过ArrayList适用于单线程,Vector是线程同步的,适用于多线程,还有两者扩容机制也有些不同,具体的比较如下图:
(取自韩顺平的java视频)
(1)vector底层也是一个Object数组,不过它有protected关键字,而ArrayList的底组的数组关键字为trasnient。
①如果是无参,默认10,满后,按2倍扩容
②如果指定大小,则每次直接按2倍扩容
效果展示①
初始化
扩容
效果展示②
初始化
扩容
源码解析
注意:vector创建之后就会有一个容量为10的数组,但是ArrayList刚创建时,数组容量为0的,只有当加入元素时,检测到数组容量不够,才会进行扩容,扩容大小也为10。
当超过当前容量时,会扩容为原来容量的两倍
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//当capacityIncrement > 0 时,那么按照capacityIncrement的值来扩容
//否则,就再加上一个oldCapacity扩容
//capacityIncrement默认为0
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
这里capacityIncrement是在调用下面有参构造器时会被赋值的,默认为0,当程序员自己定义这个值时,相当于按照这个capacityIncrement进行扩容,不会默认为两倍扩容。
3.LinkedList
LinkedList底层不同于ArrayList和Vector,底层维护一个双向链表进行数据存储的。
数组可以实现随机存取,而链表需要一个个遍历查找,所以数组改查效率更高
但数组进行增删时,要把原来元素进行增删后,再创建一个数组重新放入,而链表可以通过改变指向来实现一个元素的增删,所以链表的增删效率更高。
(取自韩顺平的java视频)
源码解析
增加结点
void linkLast(E e) {
final Node<E> l = last; //相当于一个临时变量,接收last指向的结点node1
//因为要在链表后面再加入一个元素,那么原先链表的最后一个结点node1成为倒数第二个结点
//新加入的结点node2成为最后一个结点
//所以node2会指向node1,所以把l当做其前驱
final Node<E> newNode = new Node<>(l, e, null);
//last总会指向最后一个结点
//所以last指向新节点
last = newNode;
//如果l为空,那么这个新加入的结点为第一个结点
//first总是指向第一个结点,所以会指向这个新结点
if (l == null)
first = newNode;
else
//前面node2已经指向node1,node也要指向node2
l.next = newNode;
size++;
modCount++;
}
删除结点
默认删除第一个结点
private E unlinkFirst(Node<E> f) {
final E element = f.item;
final Node<E> next = f.next;
//把f结点设为空,这样GC机制会将这个结点的空间回收
f.item = null;
f.next = null;
first = next;
//如果next为空,说明把f结点已经是最后一个结点
//删除f结点整个双向链表都没有结点了
//所以把last置为空
if (next == null)
last = null;
else
//f结点删除后,f的下一个结点就是第一个结点
next.prev = null;
size--;
modCount++;
return element;
}
Set
1.HashSet
维护了一个哈希表,即数组+链表+红黑树组成,且无序。
hashSet的数据存放模型如下图(没把红黑树包括进去):
(1)HashSet底层维护的是HashMap
(2)可以存放null值,但只能存放一个,数据存放不保证顺序
源码解析
添加数据
这里的PRESENT是一个Object对象,因为HashSet实际上是一个HashMap,所以加入的值(java)作为key,value就用一个Object对象代替,感觉无多少实际意义。
计算出加入的值(java)的hash值,这里的(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;
//如果为空,进行扩容
//扩容的数组初始大小为16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果这个位置没有结点,则直接放入
//如果有节点,此时p已经被赋值为第一个结点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//不同键值算出来的hash值可能相同
//放入的结点与p的hash值是否相同,
//如果相同,则再比较键值,键值的地址相同或者键值的类的equals方法比较相同
//则认定为加入的数据与p相同,那么就无法加入
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//将该位置第一个结点的值赋给e
e = p;
//如果p为TreeNode结点,那么是按照红黑树中插入结点的方法进行数据的插入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//遍历该位置的链表
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//TREEIFY_THRESHOLD的值为8,当链表中已经有8个结点了
//当要加入第9个结点时,会执行treeifyBin()方法
//注意这里并不一定会进行树化,要看数组的大小,具体的要看treefiBin()
//红黑树的搜索、添加,删除的效率为O(logn)
//链表的搜索、添加,删除的效率为O(n)
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果要加入的数据不能加入,则e不会为空,则返回的值不为空
//在add方法中会进行返回值是否为空的判断,
//如果为空,add方法返回true,否则返回false
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果结点个数大于阈值(阈值默认为为数组大小的0.75)
//如果数组大小为32,阈值为24,当加入第25个结点时
//即使所有结点都在一个数组位置上,也会进行扩容操作
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
①resize()
对于为null的数组变量,会初始化为16,当超过阈值需要扩容时,会增大到原来的2倍同时会将数据重新放置。(重新放置的代码过长,自行查看)
②treeifyBin()
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//要对当前数组的大小进行判断,如果小于64,那么不会进行树化
//只会对数据进行2倍的扩容
//也就是树化的条件有两个
//1.数组的大小需要大于64
//2.某一个位置的结点数超过8个
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//这里也不会进行树化,只是将这个位置的链表变成双向链表
//因为把链表变成红黑树之后,根结点不一定在第一个
//所以双向链表为后面方便把红黑树的根结点移到链表的第一个
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
//真正的进行树化
hd.treeify(tab);
}
}
treeify()方法就是判断该结点该往左,往右,还是当做根结点,然后最后再做一次平衡
2.TreeSet
底层基于红黑树实现,且有序。
(1)TreeSet底层维护的是TreeMap
(2)可以给TreeSet设置比较器
注意点:compareTo实现Comparable接口,compare实现Comparator接口,
Comparable位于java.util包下,Comparator位于java.lang包下,
Comparable是内比较器,一般在构建类时,就实现了Comparable
Comparator是外比较器,根据当时的需求再进行构造
Comparator的优先级较高
Comparator和Comparable的区别(转载自 想飞的yu)
底层把匿名内部类比较器传给TreeMap对象的comparator属性
由下图可知当根据comparator的compare方法进行是否相同的判断时,如果判断两个值是相同的,则不会加入相同的数据
源码解析
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
//调用自己创建的比较器
cmp = cpr.compare(key, t.key);
//为什么当 第一个参数-第二个参数 < 0 时按从小到大
//因为当小于0时是往左子树那边插入
//而数据按红黑树(特殊的二叉搜索树)的中序遍历输出
//所以会从小到大输出,以上仅个人猜想
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
//如果相等,则返回0,则插入不成功
//只是覆盖了这个key的值
//因为TreeSet中value=PRESENT,相当于没有插入
else
return t.setValue(value);
} while (t != null);
所以如果改变一下比较器,按照长度进行排列的话,会出现“wzy”加入不进去这种情况:
3.LinkedHashSet
底层维护的是数组+双向链表,初始容量与HashMap相同,为16
整体形状就是原来的HashSet加上了双向指针,构成双向链表
(1)LinkedHashSet底层维护的是LinkedHashMap(HashMap的一个子类),多了before和after属性来创建双向链表
(2)可以按添加的顺序进行存储
(3)数组是HashMap$Node[ ] ,存放的数据是 LinkedHashMap$Entry类型,
LinkedHashMap$Entry是HashMap$Node的子类
(4)添加数据
实际还是调用HashMap的add()方法把数据加入进去
三、Map
1.HashMap
因为前面HashSet底层是HashMap,这里就不再过多赘述
2.TreeMap
因为前面TreeSet底层是TreeMap,这里就不再过多赘述
3.HashTable
同HashMap差不多,不过它是线程安全的。
(1)键和值都不能为null,否则会抛出NullPointerException
(2)底层有数组Hashtable$Entry[ ] ,初始化大小为11,加载因子还是0.75
(3)扩容是 原先的容量 * 2 + 1
4.Properties
继承了HashTable类,同HashTbale类似,也是用键值对保存数据,可以用于从xxx.properties文件中,加载数据到Properties类对象中
总结
用于记录学习过程