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; } 12345678910
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); } 123456789101112131415161718192021222324
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++; } 123456789101112131415
注意:在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; } } 12345678910111213141516
1.1.2.4 源码解析
LinkedList的一些基本属性
//first在LinkedList中永远指向链表的头节点,如果没有元素时,就为null transient Node<E> first; //last在LinkedList中永远指向链表的尾节点,如果没有元素时,就为null transient Node<E> last; 12345
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++; } 12345678910111213141516171819202122232425
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; } } 12345678910111213141516171819202122232425
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++; } 1234567891011121314151617181920212223242526
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 红黑树(简单介绍)
什么是红黑树?
红黑树本身是一种特殊的二叉树,是用于快速查询的数据结构
什么是二叉搜索树?
在一颗二叉树中,任意节点的左子树节点都比当前节点要小,右子树节点都比当前节点要大,那么这颗二叉树就称之为二叉搜索树
二叉搜索树的缺点:如果插入的元素有一定的顺序,那么就可以导致树的失衡,从而严重降低树的查询能力
红黑树就是为了解决二叉搜索树,失衡问题而设计的一颗二叉搜索平衡树