容器
1 List
1.1 ArrayList
- 首先需要清楚:
- ArrayList的底层为一个elementData[]数组;
- 起始插入第一个值时,数组容量默认为10;
- 每次新增元素前会判断是否需要扩容,当插入元素后的长度大于数组容量时,触发扩容
- 首先获取原有数组elementData的长度;
- 新增oldCapacity的一半整数长度作为newCapacity的额外增长长度;
- 若新的长度newCapacity依然无法满足需要的最小扩容量minCapacity,则新的扩容长度为minCapacity;
- 扩展数组长度为newCapacity,并且将旧数组中的元素赋值到新的数组中!
1.2 LinkedList
- 首先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;
}
}
- 当初始化一个LinkedList时,内部生成指向第一个节点的指针first,和指向最后一个节点的指针last
- 当新添加一个元素e,并维护进链表
- 获取当前最后一个节点last
final Node l = last;
final Node newNode = new Node<>(l, e, null); - 将新添加进链表的节点作为last;
- 判断是否为第一个元素,并将其维护斤链表。
if (l == null) {
/** 如果是第一个添加的元素,则first指针指向该结点*/
first = newNode; // eg1: first指向newNode
} else {
/** 如果不是第一个添加进来的元素,则更新l的后置结点指向新添加的元素结点*/
l.next = newNode;
}
- 获取当前最后一个节点last
- 当移除一个一个元素e;
linkedList.remove(index);
1. 首先判断index是否越界;
2. 通过index找到该元素,如果需要获取的index小于总长度size的一半,则从头部开始向后遍历查找,否则从尾部开始向前遍历查找;
3. 断开该元素的连接。
2 HashMap
2.1 什么是哈希表?
- 在讨论哈希表之前,先看一下其他数据结构。
- 数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)。
- 线性表:对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)。
- 二叉树:对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。
- 为了高效率的查找元素,我们的主干使用数组的形式来存储数据。比如我们要新增一个元素时,我们通过某个函数将其映射到数组某个位置,这样我们在查找的时候,通过数组下标即可完成定位。
- 查找操作同理,先通过哈希函数计算出实际存储地址,然后从数组中对应地址取出即可。
- 然而,若在不断hash后插入是发现插入位置已经有元素该怎么办?这就是我们所谓的哈希冲突。观察源码后发现,代码中解决该问题的方法为链地址法,当我们插入时发现该地址已有一个其他元素时,将该位置转换成一个链表形势,然后连接其中。因此,hashMap采用的是数组+链表的形式构成的。
2.2 HashMap的成员变量
- 由上文可以了解到,HashMap的主干是一个(Node)数组的形式,而Node元素时HashMap的基本组成单元,每一个Node包含一个key-value键值对。
-
// 存储Node数组。
transient Node<K, V>[] table; - Node是HashMap中的一个静态内部类
static class Node<K, V> implements Entry<K, V> {
final int hash;
final K key;
V value;
Node<K, V> next;
Node(int hash, K key, V value, Node<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
- HashMap其他成员变量:
transient int size; //记录实际上Key-Value个数
int threshold; //阈值,用于判断是否需要扩容,默认值为16
final float loadFactor; //负载因子,默认值为0.75,用于计算阈值
transient int modCount; // HashMap被改变次数,用于在非线程安全下抛出异常
2.3 从一次put来理解HashMap:
- 首先初始化一个table数组存放Node;
// 如果是空的table,那么默认初始化一个长度为16的Node数组
if ((tab = table) == null || (n = tab.length) == 0) {
// eg1: resize返回(Node<K, V>[]) new Node[16],所以:tab=(Node<K, V>[]) new Node[16], n=16
n = (tab = resize()).length;
}
- 通过异或运算hash下标,如果此时tab中此下标没有数据,则新增在此位置,若此时tab数组中已存在数据,则…
-
p = tab[i = (n - 1) & hash]//异或运算
- 判断value是否与待插入value相同,若相同则直接覆盖
- 如果与已存在的Node是相同的key值,并且是普通节点,则循环遍历链式Node,并对比hash和key,如果都不相同,则将新的Node拼装到链表的末尾。如果相同,则进行更新。(当Node节点链表大于8个Node,并且table长度大于64,则转换为红黑树)
-
/** 如果Node链表大于8个Node,那么变为红黑树 */
if (binCount >= TREEIFY_THRESHOLD - 1) {
// eg6: tab={newNode(0, 0, “a0”, [指向后面1个链表中的7个node]), newNode(1, 1, “a1”, null)}, hash=128
treeifyBin(tab, hash);
} - 将元素插入hashMap后,size++并与阈值threshold对比,若大于threshold,则触发resize()扩容。容量扩大一倍后检查是否小于2^30,并且大于默认值16。扩容后将当前table数组的所有Node全部传输过去,扩容后的新数组是table数组的两倍大小。
2.4 hash函数
- 首先上源码:
static final int hash(Object key) {
int h;
/**
* 按位异或运算(^):两个数转为二进制,然后从高位开始比较,如果相同则为0,不相同则为1。
*
* 扰动函数————(h = key.hashCode()) ^ (h >>> 16) 表示:
* 将key的哈希code一分为二。其中:
* 【高半区16位】数据不变。
* 【低半区16位】数据与高半区16位数据进行异或操作,以此来加大低位的随机性。
* 注意:如果key的哈希code小于等于16位,那么是没有任何影响的。只有大于16位,才会触发扰动函数的执行效果。
* */
// egx: 110100100110^000000000000=110100100110,由于k1的hashCode都是在低16位,所以原样返回3366
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 从源码中可以看到核心为key.hashCode()) ^ (h >>> 16),即将key的hashCode右移16位后异或操作。那么可以得出:将key的哈希code一分为二。【高半区16位】数据不变。【低半区16位】数据与高半区16位数据进行异或操作,以此来加大低位的随机性,减少hash冲突的可能。
2.5 resize()扩容
- 当我们构造一个hashMap或者当hashMap中table数组的容量不足以存放我们所需要的值时,就会用到扩容,即扩充hashMap的容量,或者说是扩充hashMap中table数组的容量。
- 由上文得知,当申请一个hashMap或者当前table的size大于阈值threshold时,就会触发扩容。这里我们把扩容分成两个阶段:
- 重新解析计算table的容量newCap和重新计算阈值的大小newThr;
- 根据newCap和newThr重新构建一个新的数组,并将旧的table转移进去。
2.5.1 扩容第一阶段
- 首先我们现在需要知道当前触发扩容是由于申请了新的hashMap还是当前超过阈值引起的。因此我们用oldCap>0?去判断触发扩容的原因;
- 若是申请一个新的hashMap,则将newCap和newThr分别设置为默认值16和12;阈值=容量*负载因子;
- 若因超出阈值而触发扩容,则判断此时的容量是否已经达到最大值,若已达到最大值,则将阈值threshold设置为integer的最大值;反之,则容量翻倍、阈值翻倍
2.5.2 扩容第二阶段
- 扩容第二阶段为将原数组放入扩容后的数组。
- 这里涉及到一个链表转化后重新分配地址的内容,因我自身还没理清楚,暂不讨论。
2.6 HashMap一次put大致流程图
- 红黑树部分不太了解…后面补充