概述
Java集合类
1. 前言
Java集合类是学习Java时一个很重要的知识点,同时也是面试时考核的重点。另外在实际工作中,如果不是很理解各个集合类的特点,很容易陷入到只会机械使用ArrayList,HashMap等集合类的怪圈中。所以通过debug源码的方式,来学习这些类之间的相互关系,以及他们各自的特点。
2. 类关系
在学习他们之前,需要先了解这些常用类之间的相互关系。
2.1 单列集合类
1)首先单列集合类的祖先接口是Iterable,这个接口类为所有单列集合类提供了遍历的方法,这个后面再说。
2)接下来是Collection,他继承了Iterable接口,规定了一些通用的方法,如size(),add(),remove(),clear()等方法。
3)之后又有两个接口继承了Collction接口,一个是List接口(有序可重复),一个是Set接口(无序不可重复)。
4)常用的实现List接口的集合类有三个,Vector(线程安全),LinkedList(双链表结构),ArrayList(数组结构)。
常用的实现Set接口的集合类也有三个,HashSet(底层是HashMap),它的子类LinkedHashSet(底层是LinkedHashMap);TreeSet(底层是TreeMap)。
2.2 双列集合类
1)祖先接口是Map,与Collection一样,规定了所有双列集合类要实现的基本方法,如put(),get(),remove()等常用方法。
2)常用的实现Map接口的集合类有四个,一个是Hashtable(线程安全),HashMap(jdk8为数组+链表+红黑树,jdk7为数组+链表,考虑到jdk已经发展到15,所以7就不做分析了),以及他们分别的子类Properties(常用于解析配置文件.properties),LinkedHashMap(在HashMap的基础上,实现了双链表)。
3. List集合
3.1 ArrayList
1.ArrayList底层维护的是一个数组,我将利用add方法,来展示它的底层结构以及相应代码。
在开始之前,将用到的属性先标注一下。
2.用下面的代码进行测试
public class ListMethod {
public static void main(String[] args) {
ArrayList arrayList = new ArrayList();
for(int i = 0; i < 100; i++){
arrayList.add(i);
}
}
}
3.进入debug环境
1)先经过构造函数,elementData被赋空数组作为初始值。
2)进入到add方法,有三个步骤,ensureCapacityInternal()方法确认数组容量,之后添加数据到数组中,最后返回true。
3)进入ensureCapacityInternal()方法,内部调用ensureExplicitCapacity方法,再debug,进入calculateCapacity方法。
4)进入calculateCapacity方法后,通过判断默认数组大小与添加元素后数组大小,决定后面是否需要扩充底层数组。
5)回到ensureExplicitCapacity方法,通过上一步计算出需要的数组容量,来判断是否需要扩充elementData数组。
6)第一次肯定需要数组扩容,进入grow方法
7)ensureCapacityInternal方法结束,数组扩容完毕,将对象放置到数组中,完成一次add操作。
8)看第11次add操作,体会数组扩容
4.小结
1)ArrayList底层是利用数组保存存入的对象,在创建一个ArrayList对象的时候,并没有给数组赋初始大小,而是在第一次add操作时,才给数组赋初始大小(如果addAll批量添加的时候,需要的容量大于默认初始值10,那么直接使用最大值作为初始容量值)。
2)由于底层为数组,具备索引,所以ArrayList在查询操作中效率很高,因此若在使用中需要频繁查询,首选ArrayList。
3)没有synchronized关键字,所以若程序涉及到多线程问题,不建议使用。
3.2 LinkedList
1.LinkedList底层双链表结构,由自定义的内部类Node形成。我还是通过add方法来展现底层结构。
同样,先说下用到的属性和静态内部类Node
2.用下面的代码进行测试
public class LinkedListMethod{
public static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<>();
list.add(5);
list.remove(new Integer(5));
}
}
3.进入debug环境
1)进入空构造函数,可以看到LinkedList除创建一个对象外,没有做任何操作。
2)进入add方法,里面分成两步。第一步,由于调用的是无指定位置的add方法,因此默认将结点插入到链表末尾,即调用linklast方法;第二步,插入成功后返回true。
3)进入linklast方法,将新结点添加到链表末端。
4)至此,完成一次add操作。
5)remove方法就比较简单,找到需要删除的结点x,将其前驱结点的next指针指向结点x的后驱结点,将x后驱结点指向x的前驱结点。解链完成后,该结点失去对象指针,由jvm的GC处理掉。
4.小结
与ArrayList不同,LinkedList底层是Node形成的双向链表,所以在批量增加的时候,并不会涉及到扩容的问题。
同样由于双向链表的结构,LinkedList在删除操作上比ArrayList的数组结构具有更高的效率,因此如果在更新操作较多的环境下,前者是更好的选择。
3.3 Vector
1.Vector底层是数组,与ArrayList在设计思路上基本类似,但由于方法上使用了synchronized关键字,实现了线程安全。
先说使用到的类属性,capacityIncrement这个参数的作用是在数组扩容的时候,可以由用户自定义扩容大小
2.用下面的代码进行测试
public class VectorMethod {
public static void main(String[] args) {
List vector = new Vector();
for(int i = 0; i < 100; i++){
vector.add(0);
}
}
}
3.进入debug环境
1)进入构造函数,这里要注意,Vector与ArrayList不同。前者在创建对象时,直接指定数组大小为10,同时capacityIncrement为0(没有自定义扩容基数,使用默认值0);而后者是在创建对象时,并没有给数组赋初始长度,是在第一次add操作时,才指定长度。
2)进入add方法,一共有四步。首先modCount自增,记录这是第几次操作该Vector对象;第二步判断数组是否需要扩容,与ArrayList的ensureCapacityInternal()方法作用相同;第三步将元素按顺序插入到数组里;第四步返回true。
3)进入ensureCapacityHelper()方法,因为数组长度为10,且只插入一个值,所以数组不需要扩容。
4)确认不需要扩容,将元素添加到数组中,返回true。一次add操作结束。
5)运行第11次,体会数组扩容。
6)进入grow方法,与ArrayList只能1.5倍扩容不同,Vector允许自定义扩容基数,若未指定基数,则按2倍扩容。
7)至此,add操作结束,底层数组完成扩容。
4.小结
Vector与ArrayList比较,需要保证线程安全的时候,可以考虑Vector。
Vector底层也是数组,但是默认扩容基数与ArrayList不同,并且可以根据实际情况自定义基数大小。
3.4 List小结
介绍了List的三种实现类,底层结构有不同,有数组,也有自定义的内部类。不过都能保证插入数据的顺序性,以及能够允许重复数据的插入。
4. Set集合
4.1 介绍
在学习Set集合三个实现类之前,需要提前说一下,HashSet的底层是HashMap,LinkedHashSet的底层是LinkedHashMap,TreeSet的底层是TreeMap,所以学Set其实就是学Map。
4.1 HashSet
1.HashSet底层是HashMap,还是先看下需要用到的类属性。
2.使用下面代码进行测试
public class HashSetMethod {
public static void main(String[] args) {
Set hashSet = new HashSet();
for(int i = 0; i < 100; i++){
hashSet.add(i);
}
}
}
3.进入debug源码
1)进入构造函数,内部创建一个空HashMap对象。
2)进入add方法,调用map.put()方法。注意这个时候map的table数组为null。
3)内部调用putVal方法。
4)进入hash方法。使用高低位异或解决hash碰撞的原因,我在网上查了一下:
1.当数组长度很短时,只有低位的hash值参与运算(这个地方是在HashMap源码第630行,属于putVal()方法),因此为了减少hash碰撞结果,让高16位hash值参与运算。
2.采用异或运算而不采用& ,| 运算的原因是 异或运算能更好的保留各部分的特征,如果采用&运算计算出来的值会向1靠拢,采用|运算计算出来的值会向0靠拢。
详细的可以看下面这个链接
https://blog.csdn.net/weixin_43842753/article/details/105927912
5)进入putVal方法,完成数据插入。里面涉及到两个方法,resize()完成链表扩容,treeifyBin()方法完成链表树化。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
/**
* 创建临时数组tab,Node结点p
*/
Node<K,V>[] tab; Node<K,V> p; int n, i;
/**
* 先将底层table赋给tab,判断是否为空
* 如果为空,说明table还没有初始化,进入resize()方法
*/
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/**
* 数组长度与hash值&运算,确定元素将放到的数组索引位置,判断数组当前索引位置是否已经存在元素
* 若没有值,则将元素放到数组当前位置
* 若数组该位置已经存在值,则进入后续判断
*/
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
/**
* 判断数组当前位置是否重复插入相同值
* 如果相同,只会将原值替换掉,保留新值,这也就是为什么Set会保证数据唯一性
*/
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
/**
* 判断数组当前位置保存是否是树形结构
* 如果是树形结构,按照数的方式添加元素
* 如果是链表结构,按照链表方式添加元素
*/
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
/**
* 循环遍历数组该位置的链表
* 没有跳出循环的临界条件,而是在程序中使用break结束循环
*/
for (int binCount = 0; ; ++binCount) {
/**
* 在这里一定要注意开始遍历的位置!!!p是数组当前位置元素,p.next是p所形成的链表的第二个元素!!!
* 判断是否已经到达了链表末端,如果已经到了末端,就先创建一个Node结点,保存对象数据
*/
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
/**
* 1.判断循环次数是否大于树化临界值
* binCount >= TREEIFY_THRESHOLD - 1 判断 binCount是否大于等于7
* 注意循环的起始位置是链表第二位,所以树化的阈值是8,这个地方一定要理解!!
* 2.但是树化还有另外一个条件,还需判断数组长度是否到达64
* 数组长度到达64,该链表树化
* 数组长度没到达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;
}
}
/**
* 如果元素重复,则替换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大于临界值,则调用rezise()方法
* 要注意只要新增数据,不管添加到哪个位置,只要成功添加,size都要自增
*/
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
6)resize()方法,完成数组扩容,以及数据从老数组到新数组的迁移。
final Node<K,V>[] resize() {
/**
* 创建临时数组oldTab保存table数据
* oldCap用于记录当前数组容量
* oldThr用于记录当前数组扩容临界值
*/
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
/**
* 先对容量进行判断
* 若当前数组容量大于0
* 判断其是否已经大于数组最大容量MAXIMUM_CAPACITY(1 << 30)
* 如果已经满足,则将临界值修改成Integer.MAX_VALUE,直接返回
* 如果不满足,将旧容量扩容一倍与最大容量比较且旧容量是否大于默认初始容量,都满足的话,将旧临界值扩容一倍
*/
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;
/**
* 只针对第一次扩容,即容量和临界值都为0
*/
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
/**
* 判断新临界值
* 若临界值为0,即初次加载
* 则用新容量*加载因子计算新临界值
*/
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];
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;
}
4.小结
HashSet底层是HashMap,在创建的时候,数据保存在Map的Key中,Value用Object对象占位。因此就能解释Set为什么能保证数据的唯一性。
由于时间原因,后续有空再慢慢补。
最后
以上就是难过冬瓜为你收集整理的Java集合类Java集合类的全部内容,希望文章能够帮你解决Java集合类Java集合类所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复