简介
数组的不足
-
在数组初始化时长度必须定义,一旦定义就不能修改
-
存储的是同一类型的元素
-
数组扩容和裁剪比较麻烦
- 扩容的第一步先创建满足大小的数组,第二步拷贝原数组到新数组中,第三步改变数组对象的引用
集合的好处
- 可以动态保存任意多个对象
- 提供了许多对应的增删改查的方法
集合框架体系
Collection:单列集合
-
Collection接口继承自Iterable接口,有两个重要的子接口:List和Set
-
List的重要实现类:ArrayList Vector LinkedList
-
Set的重要实现类:HashSet TreeSet
-
HashSet的重要子类:LinkedHashSet
Map:双列集合
-
Map接口的重要实现类:HashMap Hashtable TreeMap
-
Hashtable的重要子类:Properties
-
HashMap的重要子类:LinkedHashMap
集合使用选择
![image-20210717162652364](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210717162652364.png)
Collection接口体系
常用方法
迭代器遍历
- Iterable接口中有一个iterator方法,返回值是Iterator对象。因此只要是实现了Collection的接口都实现了Iterable接口,因此他们都可以获取一个Iterator对象:用来遍历集合
- 把Iterator看作游标,hasNext()表示当前游标之后是否还有元素,next()表示游标下移,并返回下移后的上个元素
增强for循环
- 增强for可以直接用在集合上,也可以直接用在数组上
- 增强for底层仍然是迭代器,获取Iterator对象,调用hasNext()和next()方法
List
List接口体系
- List体系下的集合元素是有序的(存取顺序一致)
- List体系下的集合元素可以重复,可以存储null值
- List体系下的集合元素都有对应的索引
常用方法
遍历方式
- 普通for循环
- 迭代器遍历
- 增强for循环
ArrayList底层结构和源码分析
一些结论
- ArrayList可以存放null值
- ArrayList底层是由数组实现的
- ArrayList基本等同于Vector,但是ArrayList为了高执行效率,没有做线程安全方面的控制,所以它是线程不安全的,因此多线程情况下,不建议使用ArrayList。具体表现在:ArrayList的方法都没有使用synchronized关键字
源码分析
- ArrayList中维护了一个Object类型的数组elementData:transient Object[] elementData
- 当创建ArrayList对象时,如果使用的是无参构造器,则初始化elementData的大小为0,第一次添加,大小扩容至10,再次扩容,则大小扩容至1.5倍
- 如果使用的是指定参数大小的构造器,则指定elementData的大小,若需扩容,则大小扩容至1.5倍
使用无参构造方法创建ArrayList时:
向空的ArrayList中添加数据时:
确定是否要扩容:
真实的扩容过程:
使用有参构造创建ArrayList时:
![image-20210714150247844](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210714150247844.png)
Vector底层结构和源码分析
一些结论
- Vector底层也是一个elementData数组:protected Object[] elementData
- Vector是线程安全的,所以效率会低。具体表现在Vector类中的方法都有synchronized关键字
源码分析
我这里是JDK15的源码,与JDK1.8略有不同
- 创建Vector对象时,如果使用的是无参构造器,则初始化elementData的大小为0,第一次添加,大小扩容至10,再次扩容,则大小扩容至2倍
- 如果使用的是指定参数大小的构造器,则指定elementData的大小。若需扩容,则大小扩容至2倍
使用无参构造方法创建Vector时:
![image-20210714153333433](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210714153333433.png)
扩容的过程与ArrayList很相似
LinkedList底层结构和源码分析
- LinkedList底层实现了双向链表和双端队列
- 线程不安全
- LinkedList中维护了两个属性first和last分别指向首节点和尾节点
- 每个结点(Node对象)里面又维护了三个属性:prev(前一个) next(后一个) item
- 这样的双向链表增删很快,效率也高,只需要改变prev和next的指向
模拟一个LinkedList
class Node{
public Object item; //真正存放数据的地方
public Node prev; //指向上一个节点
public Node next; //指向下一个节点
public Node(Object name){
this.item = name;
}
}
public class Mock{
public static void main(String[] args){
Node jack = new Node("jack");
Node tom = new Node("tom");
Node zxl = new Node("zxl");
//连接这三个几点,形成双向链表
//jack --> tom --> zxl
jack.next = tom;
tom.next = zxl;
//jack <-- tom <-- zxl
zxl.prev = tom;
tom.prev = jack;
//维护首尾
Node first = jack;
Node last = zxl;
//从头到尾进行遍历
while(true){
if(first == null){
break;
}
System.out.println(first);
first = first.next;
}
//在jack和tom之间插入hsp
Node hsp = new Node("hsp");
jack.next = hsp;
hsp.next = tom;
tom.prev = hsp;
hsp.prev = jack;
}
}
源码分析
当使用无参构造创建LinkedList:
![image-20210715100340053](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210715100340053.png)
添加数据时,add的无参方法会添加数据到最后面:
![image-20210715100800796](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210715100800796.png)
当第一次添加数据时,l=last=null:
![image-20210715101625816](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210715101625816.png)
第一次添加数据成功后:
![image-20210715101915271](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210715101915271.png)
删除数据时,remove的无参方法会删除最前面:
进入
![image-20210715104353486](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210715104353486.png)
真正的删除方法:
![image-20210715104707645](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210715104707645.png)
修改某个节点对象的数据:
![image-20210715105933172](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210715105933172.png)
检查机制:
![image-20210715110049887](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210715110049887.png)
获得某个节点对象的数据:
![image-20210715110503934](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210715110503934.png)
索引查找:
![image-20210715110644789](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210715110644789.png)
索引查找需要经过很多次遍历:
![image-20210715111056433](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210715111056433.png)
查找头尾节点:
![image-20210715111449647](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210715111449647.png)
可以看出,LinkedList查找头尾的速度很快,但是索引查找的速度很慢。
Set
Set接口体系
- Set体系下的集合是无序的(存取顺序不一致)
- Set体系下的集合取出数据的顺序虽然与存放顺序不一致,但是取出的顺序是固定的
- Set体系下的集合元素不可以重复,最多只有一个null值
- Set体系下的集合没有索引
常用方法
参考Collection接口体系的常用方法
遍历方式
- 可以使用迭代器
- 可以使用增强for循环
HashSet的底层结构和源码分析
一些结论
- HashSet底层是HashMap
- HashSet取出元素的顺序取决于hash后,再确定索引的结果
模拟一个HashSet(HashMap)
class Node{
Object item;
Node next;
public Node(Object name){
this.item = name;
}
}
public class HashSetStructure{
public static void main(String[] args){
//创建一个数组,数组的类型是Node
//有些人直接把Node数组称为table
Node[] table = new Node[16];
//1.数组中添加Node数据
Node zxl = new Node("zxl");
table[2] = zxl;
//2.将新的Node数据挂载到后面
Node jack = new Node("jack");
zxl.next = jack;
//3.到这里,就完成了数组中存放着一个简单的链表,可以继续挂载:
Node rose = new Node("rose");
jack.next = rose;
}
}
源码分析
- HashSet的底层是HashMap,HashMap底层是:数组+链表+红黑树(**个人理解:**数组里存放着链表)
- 当链表达到一定量并且数组的大小再一定范围内时,会对链表进行树化
- 树化和剪枝:树化就是链表长度大于等于8转换为红黑树,剪枝就是红黑树经过删除后若长度小于8则转换为链表
扩容机制结论:
- 添加一个元素时,先得到hash值(根据hashCode得到),hash值会转换为索引值
- 找到存储数据表table,查看这个索引位置是否有已经存放的元素
- 如果没有,则直接添加,如果有,就调用equals比较,如果相同就放弃添加,如果不同就添加到最后
- 在JDK1.8后,如果一条链表的元素个数大于等于TREEIFY_THRESHOLD(默认为8),并且table的大小大于等于MIN_TREEIFY_CAPACITY(默认为64),就会进行树化转换为红黑树
- 第一次扩容数组大小为16,临界值=默认初始容量 ∗ * ∗加载因子=16$*$0.75=12,后续达到临界值便扩大2倍:数组大小32,临界值24
- 这个扩容的临界值指的是数据的数量,不是数组被占用的数量,扩容则扩容的是数组
创建HashSet:
![image-20210715145517745](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210715145517745.png)
![image-20210717133636812](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210717133636812.png)
第一次添加数据:
为了让HashSet使用到HashMap,没有value,要用PRESENT占位:
![image-20210715150010327](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210715150010327.png)
![image-20210715150818176](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210715150818176.png)
调用putVal,第一个参数是hash值:
![image-20210715150230949](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210715150230949.png)
先查看hash方法,观察到实际上得到的是key的hashCode无符号右移16位(防止hash冲突)的值,这个值并不等价于hashCode
![image-20210715151216926](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210715151216926.png)
再仔细分析putVal的工作流程:(最终是返回null,并且再一路返回true,添加成功)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//定义了辅助变量
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
//table是Node类型的数组,第一次添加数据时,table还为null
if ((tab = table) == null || (n = tab.length) == 0)
//resize方法见下方代码块,总之得到了tab是大小为16的数组,且n=16
n = (tab = resize()).length;
//根据hash值得到一个tab[i]并赋值给p(这是一种求余数的方法)
//如果p为null,表示还没有存放过数据,就创建一个Node(key="123",value="object..."),直接放在tab[i]
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//当tab[i]不为空时
else {
//辅助变量
Node<K,V> e;
K k;
//如果p索引位置对应的链表的第一个结点的hash值和准备添加的key的hash值相同,
//如果p索引位置对应的链表的第一个结点的key对象和准备添加的key对象是同一个对象,
//或者不是同一个对象,但是equals的比较结果相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//再判断p索引位置是不是一棵红黑树,如果是,就按照红黑树的方式去执行后续结点的比较和添加操作
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//p索引位置还不是一个红黑树,就按照链表的方式去执行后续结点的比较和添加操作
else {
for (int binCount = 0; ; ++binCount) {
//比较结束,满足条件进行添加
if ((e = p.next) == null) {
//添加到链表的末端
p.next = newNode(hash, key, value, null);
//判断链表是否达到8个结点,若是,则进行树化操作,
//但treeifyBin内部还会判断table的大小是否大于64
//若不满足,则先扩容;若满足,就树化
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,则进入这个if
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold; //threshold默认是0
int newCap, newThr = 0;
//oldCap和oldThr都为0
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;
}
else if (oldThr > 0)
newCap = oldThr;
else {
newCap = DEFAULT_INITIAL_CAPACITY; //默认值是16(位左移4位相当于乘以2的4次方)
//默认加载因子大小是0.75,在这一步设置临界值:0.75*16=12,是为了到达临界值12时直接进行扩容
//(起到一个缓冲的作用)
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; //12
@SuppressWarnings({"rawtypes","unchecked"})
//在这里真正的创建了大小为16的table数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
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;//大小为16
}
LinkedHashSet的底层结构和源码分析
一些结论
- LinkedHashSet底层是一个LinkedHashMap,LinkedHashMap底层维护了一个:数组+双向链表
- LinkedHashSet根据元素的hashCode值来决定元素的顺序位置,同时使用链表维护元素的次序,导致存取顺序可以一致
- next结点的链接只是放在同一个数组中,有特殊的before和after结点来描述顺序
源码分析
- 在添加元素时,先得到hash值,再根据hash值得到索引,确定该元素在table中的位置
- 将元素加入到双向链表中(处理逻辑和HashSet一致)
- LinkedHashSet维护了head和tail属性,分别指向头结点和尾结点
- 每一个结点有before和after属性,这样可以形成双向链表
- table是一个Node数组,但是存放的数据类型是Entry,继承自HashMap.Node,有before和after属性**(多态)**
TreeSet的底层结构和源码分析
一些结论(TreeSet的特性很特殊,我更建议单独分析,不作为Set体系来分析)
- TreeSet的底层是TreeMap
- TreeSet(TreeMap)最大的特性就是可以排序
- TreeSet(TreeMap)的其中一个构造方法参数是比较器(Comparator<? super E>)
- TreeSet(TreeMap)的底层是:红黑树
- 若是使用无参构造方法,则添加的元素必须实现Comparable接口来重写CompareTo方法
源码分析
我这里是JDK15的源码,与JDK1.8略有不同
TreeSet treeSet = new TreeSet(new Comparator<String>() {
@Override
//前者在前,表示升序
//前者在后,表示降序
public int compare(String o1, String o2) {
return o2.compareTo(o1);
}
});
![image-20210718090027060](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210718090027060.png)
![image-20210718090106727](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210718090106727.png)
![image-20210718090136477](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210718090136477.png)
![image-20210718090155886](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210718090155886.png)
在添加第二个元素时开始比较:
![image-20210718090845731](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210718090845731.png)
进入到我们实现的匿名内部类中方法:
![image-20210718091050992](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210718091050992.png)
要么手写比较逻辑,要么使用CompareTo方法
CompareTo方法需要相比较的元素在自己的类中是实现Comparable接口来重写,此处是String:
然后根据比较结果进行判断排序,若相等情况下,会替换value:
这种相等情况很容易迷惑人,比如添加“abc”和“tom”两个字符串并且按照长度来比较,尽管两个对象不一样,还是会进行相等替换value
![image-20210718091643548](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210718091643548.png)
Map接口体系
特点(多以HashMap为例)
- 用于保存具有映射关系的数据(key-value),key-value是一一对应的,一个key对应一个value
- key和value可以是任意类型的数据,会封装到HashMap.Node对象中
- Map中的key不允许重复,原因和HashSet一样,不过看起来有区别:Map会替换value,Set的value是Present,所以看不出来
- Map中的value可以重复,(1.他们都不一定在同一个链表上,2.类比一下Set中的Present)
- Map中的key最多只能有一个null,而value可以有多个null
- HashMap底层还会创建一个HashMap.EntrySet,封装这个Map的数据,数据类型是Map.Entry,(实际上是HashMap.Node,这是因为HashMap.Node实现了Map.Entry):Entry的key引用自Map中Node的key,value引用自Map中Node的value,这样做的目的是方便遍历:**缕一缕:**Node转换成了Entry,并且封装到了EntrySet,为什么要这样,因为Entry有getKey和getValue方法。我这样理解:Map是根据参数生成的Node对象存放在table中,自身并不能直接使用Node对象,所以需要第三方的容器来收集这些Node对象,然后遍历使用
- 除此之外,还有KeySet,KeySet就是(举例:HashMap和HashSet)的桥梁
![image-20210717110111734](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210717110111734.png)
常用方法
![image-20210717110847027](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210717110847027.png)
遍历方式
HashMap的底层结构和源码分析
一些结论
- HashMap是使用频率最高的实现类
- HashMap是线程不安全的
源码分析
扩容机制结论:
-
和HashSet一摸一样,一个差异在于HashMap的替换重复value是可见的
Hashtable底层结构和源码分析
一些结论
- Hashtable的key和value都不能为null,否则会空指针异常
- Hashtable是线程安全的
- 底层是:数组+链表,维护了一个Hashtable.Entry类型的数组,Hashtable.Entry实现了Map.Entry
- 这个数组的大小(table)为11,threshold大小为8(11$*$0.75)
- 与HashMap添加在链表最后面不同,Hashtable添加数据是添加在链表的最前面
源码分析
我这里是JDK15的源码,与JDK1.8略有不同
扩容机制
根据无参构造创建一个Hashtable,在创建时就会得到一个大小为11的table,且临界值为8:
![image-20210717151653025](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210717151653025.png)
![image-20210717151722515](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210717151722515.png)
添加数据时调用put方法:
public synchronized V put(K key, V value) {
//判断value是否为空
if (value == null) {
throw new NullPointerException();
}
//判断key是否存在在table中
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
//根据hash得到索引,再根据索引得到entry
Entry<K,V> entry = (Entry<K,V>)tab[index];
//顺着这个链表一个一个进行比较
for(; entry != null ; entry = entry.next) {
//比较的逻辑
if ((entry.hash == hash) && entry.key.equals(key)) {
//通过比较发现key存在的话,就替换key对应的value
V old = entry.value;
entry.value = value;
return old;
}
}
//比较完毕key不存在table中,执行addEntry方法
addEntry(hash, key, value, index);
return null;
}
添加Entry的addEntry方法:
private void addEntry(int hash, K key, V value, int index) {
Entry<?,?> tab[] = table;
//当操作次数大于等于临界值时,因为这里判断在前,count还没有进行自加操作,所以要有=,
//要明白仍然是添加的数据大于临界值才行,第一次也就是9,不能是8
if (count >= threshold) {
//扩容操作,11*2+1=23
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
//还没达到扩容临界值
@SuppressWarnings("unchecked")
//Hashtable添加到链表中是添加到最前面的
Entry<K,V> e = (Entry<K,V>) tab[index];
//从这里看出来,e是null或者是原来的Entry
tab[index] = new Entry<>(hash, key, value, e);
count++;
modCount++;
}
![image-20210717154613526](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210717154613526.png)
Properties的底层结构和源码分析
- Properties继承自Hashtable,Hash的一些特性Properties也必须满足
- Properties多用于xxx.properties配置文件中,在IO流中发挥很大的作用
TreeMap的底层结构和源码分析
一些结论(TreeMap的特性很特殊,我更建议单独分析,不作为Set体系来分析)
- 和TreeSet几乎一摸一样,不过有具体的value
- 补充(同样适用于TreeSet):第一次添加的数据也会调用compare方法,不过只是为了判断TreeMap是否为空
- 补充(同样适用于TreeSet):第一添加的数据会放入root中
![image-20210718102838638](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210718102838638.png)
Collections工具类
常用方法
![image-20210718103617149](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210718103617149.png)
![image-20210718104804540](https://gitee.com/zxldegitee/drawing-bed/raw/master/img/image-20210718104804540.png)