Java集合、多线程、反射和Spring框架总结,源码解析
一、集合 - 通过不同的数据结构存储以及操作数据的工具
1.1 Collection
1.1.1 ArrayList、Vector
1.1.1.1 底层原理
ArrayList和Vector底层都是由动态数组实现的
1.1.1.2 ArrayList VS Vector
ArrayList是线程不安全的集合,而Vector是线程安全的集合。
Vector本质是JDK1.0的产物,但是集合体系是JDK1.2才推出的新特性。因此,JDK1.2之后sun公司强行的让Vector类实现了List接口,从而导致Vector之中有很多功能重复的方法。虽然现在为止Vector都没有过时,但是基本上已经不再使用Vector集合,哪怕是多线程环境,也不推荐直接使用Vector来保证线程安全。
1.1.1.3 什么是动态数组?
本质上数组是不能动态的,因为Java中数组一旦初始化好之后,就不能再改变数组长度。但是ArrayList和Vector底层是通过创建新的数组的方式,来达到数组动态扩展的目的。这种动态"改变"数组长度的方式,称之为动态数组
1.1.1.4 源码解析
ArrayList - 构造方法
构造方法中就是将一个长度为空的数组,赋值给一个Object数组的引用(elementData)。
JDK1.8之后,ArrayList初始化时,不再默认的初始化一个长度为10的数组(懒加载)。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
};
transient Object[] elementData;
....
/**
* 构造方法 - 初始化底层的数组
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
add - 添加元素方法
public boolean add(E e) {
//判断当前底层的数组是否需要扩容,如果需要扩容则调用grow方法,进行扩容
ensureCapacityInternal(size + 1);
//将元素设置到数组size的位置,并且size自增
elementData[size++] = e;
return true;
}
//核心扩容方法,参数minCapacity表示当前最小需要扩容的容量
private void grow(int minCapacity) {
//获得旧的数组容量
int oldCapacity = elementData.length;
//计算新的数组容量,根据旧的容量1.5倍扩容
//右移一位相当于除以2,左移一位相当于乘以2
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果新容量没有达到最小容量的要求,则直接用最小容量顶替新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//数组扩容
//Arrays.copyOf表示将根据参数二(newCapacity)的大小,创建一个新的空数组,并且将参数一(elementData)中的元素,全部拷贝过去,并且返回新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
add - 插入(往中间添加)元素的方法
//插入元素element到下标为index的位置
public void add(int index, E element) {
//检测index下标是否越界
rangeCheckForAdd(index);
//判断是否需要扩容
ensureCapacityInternal(size + 1);
//做一个index位置的元素整体后移,空出index位置来
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//将新的元素放到index位置处
elementData[index] = element;
//元素数量加1
size++;
}
**注意:**在ArrayList中,记性数组扩容或者元素移位时,底层都是调用的native方法实现,native本身没有方法体,方法实现是由C/C++实现的,以此来提高扩容的效率。
1.1.2 LinkedList
1.1.2.1 底层原理
Linked底层是由双向链表实现
1.1.2.2 什么是链表?什么是双向链表?
链表是一种非常常见的线性数据结构(和数组类似),由一个一个的节点组成,每个节点都有一个指针,指向链表的下一个节点,因为有"指针"的存在,所以链表在内存空间上可以地址不连续,因此链表没有长度的限制(数组在内存空间上地址必须连续,长度有限)。
双向链表就是普通链表的节点多了一个指向上一个节点的指针
1.1.2.3 Java如何实现一个双向链表?
链表的关键其实就是节点,链表由一个一个节点组成,指向实现了节点的结构,那么链表就能实现。
链表节点的实现:
//LinkedList中底层节点的实现
private static class Node<E> {
//数据部分,存储节点的元素
E item;
//尾部指针,指向下一个节点
Node<E> next;
//头部指针,指向上一个节点
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
1.1.2.4 源码解析
LinkedList的一些基本属性
//first在LinkedList中永远指向链表的头节点,如果没有元素时,就为null
transient Node<E> first;
//last在LinkedList中永远指向链表的尾节点,如果没有元素时,就为null
transient Node<E> last;
add添加元素的方法
//添加元素
public boolean add(E e) {
//调用该方法添加元素到链表的尾部
linkLast(e);
return true;
}
//添加元素e到链表的尾部
void linkLast(E e) {
//让l指向尾节点,第一次添加时,因为没有任何节点,所以l和last都是null
final Node<E> l = last;
//将新元素封装成新节点,并且新节点的头指针指向l
final Node<E> newNode = new Node<>(l, e, null);
//再将尾指针指向新节点
last = newNode;
//判断l是否为null,如果为null,表示新节点就是第一个节点
if (l == null)
//first再指向新节点
first = newNode;
else
//l节点的尾指针指向新节点
l.next = newNode;
size++;
modCount++;
}
get获取元素的方法
//获取下标index除外的元素
public E get(int index) {
//检查下标越界
checkElementIndex(index);
//找到index位置的节点,获得节点的数据部分
return node(index).item;
}
//node方法,返回index处的节点对象
Node<E> node(int index) {
//判断查找的下标是靠前还是靠后
if (index < (size >> 1)) {
//如果靠前,就从头节点开始,依次遍历往后查找
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//如果靠后,就从尾节点开始,依次遍历往前查找
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
add插入(从中间添加)元素的方法
//插入元素到index下标的位置
public void add(int index, E element) {
//检查下标是否越界
checkPositionIndex(index);
if (index == size)
//说明当前的插入位置为末尾,直接调用追加元素的方法
linkLast(element);
else
//插入元素到指定节点之前
linkBefore(element, node(index));
}
....
//插入数据e到succ节点的前面
void linkBefore(E e, Node<E> succ) {
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
1.1.4 ArrayList VS LinkedList
插入性能对比:
**尾部:**ArrayList和LinkedList性能相近
**中间:**ArrayList的速度 >> LinkedList的速度,ArrayList虽然存在移位,但是底层通过C++直接操作内存的方式进行了优化,而LinkedList每次插入都需要通过遍历的方式找到元素,所以性能有所下降。
**头部:**LinkedList的速度 >> ArrayList的速度,ArrayList往头部插入元素,也就意味着每次都需要位移整个数组,虽然有优化,但是也不等于没有耗时,但是LinkedList查找靠前/后的元素速度很快,同时插入性能也很快,所以相对ArrayList性能更优
查询性能对比:
**尾部:**ArrayList和LinkedList性能相近
**中间:**ArrayList >>>>>>>> LinkedList
**头部:**ArrayList和LinkedList性能相近
1.1.3 HashSet、LinkedHashSet、TreeSet
Set各个实现类的特点:
HashSet:无序、不可重复
**LinkedHashSet:**不可重复,但是元素有序(插入顺序)
**TreeSet:**不可重复,但是元素有序(字典顺序)
底层实现:
底层都是通过对应的Map集合实现
1.2 Map
1.2.1 HashMap、Hashtable
1.2.1.1 底层原理
HashMap和Hashtable底层都是由哈希表实现的,Hashtable和HashMap的关系与Vector和ArrayList之间的关系类似,Hashtable线程安全,HashMap线程不安全
1.2.1.2 哈希表(重要)
什么是哈希表?
哈希表是一种用于快速搜索的数据结构,精准查询(通过key查询value)效率极高,和元素的数量(理想型,实际过程中,多少还是有点关系)无关,所以时间复杂度为O(1)
什么是哈希碰撞(哈希冲突)?
哈希碰撞不是一件好事,但是不可避免,所以任何一张好的哈希表必须对哈希碰撞有良好的解决方案。
所谓的哈希碰撞就是指,不同的key,通过哈希函数,计算出的下标相同了。
哈希碰撞的解决方式(HashMap的解决方案)
链地址法:将发生碰撞的元素通过链表连接起来
(JDK1.8之后,是通过链表 + 红黑树的方式解决的哈希碰撞)
哈希表的扩容(底层数组的扩容)
一个哈希表,哈希碰撞是不可避免的,如果频繁的发生哈希碰撞,那么会导致大量的链表生成,对查询性能影响很严重。因此哈希表必须通过适当的扩容,来降低哈希碰撞发生的概率,以及优化查询性能。
扩容的好处:
1、降低后续发生哈希碰撞的概率
2、打散现有的碰撞的链表
什么时候扩容?
哈希表有一个参数,称之为填充因子(加载因子,HashMap默认为0.75),当添加的元素数量/数组长度时,一旦达到了填充因子的比例,就会触发一次扩容。
数组长度(100) * 填充因子(0.75) = 扩容阈值(75)
如果碰到一些精准定位、去重、判断是否存在等诸如此类的问题,都可以先考虑一下哈希表或者哈希表的一些变种结构(布隆过滤器)
1.2.1.3 红黑树(简单介绍)
什么是红黑树?
红黑树本身是一种特殊的二叉树,是用于快速查询的数据结构
什么是二叉搜索树?
在一颗二叉树中,任意节点的左子树节点都比当前节点要小,右子树节点都比当前节点要大,那么这颗二叉树就称之为二叉搜索树
二叉搜索树的缺点:如果插入的元素有一定的顺序,那么就可以导致树的失衡,从而严重降低树的查询能力
红黑树就是为了解决二叉搜索树,失衡问题而设计的一颗二叉搜索平衡树
1.2.1.4 HashMap的源码解析(JDK1.8)
HashMap中的一些属性设置
//默认的哈希表容量(数组长度)- 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//哈希表的最大容量(最大的数组长度) - 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的填充因子 - 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表转成树的默认长度
static final int TREEIFY_THRESHOLD = 8;
//树转回链表的默认长度
static final int UNTREEIFY_THRESHOLD = 6;
//当哈希表的容量达到该值时,才会发生链表转树的情况
static final int MIN_TREEIFY_CAPACITY = 64;
//当前的扩容阈值 - 当哈希表的元素数量达到这个阈值时,就会触发扩容 阈值 = 容量 * 填充因子
int threshold;
//当前有效的填充因子值
final float loadFactor;
//底层的哈希表
transient Node<K,V>[] table;
HashMap的构造方法
//无参构造
public HashMap() {
//设置当前的填充因子为默认填充因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
//2个参数的构造方法
//参数1 - 哈希表的初始化容量
//参数2 - 自定义填充因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//根据自定义的容量设置扩容阈值
this.threshold = tableSizeFor(initialCapacity);
}
HashMap元素的节点类
//一个key-value就对应一个Node对象
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//当前key的哈希值
final K key;//key
V value;//value
Node<K,V> next;//指向下一个节点的引用 - 链表
Node(int hash, K key, V value, Node<K,