集合
ArrayList
基于动态数组实现的一个容器,具有数组的特性,增删改很快,删除稍微慢。
几个属性
//第一次扩容时默认的扩容容量
private static final int DEFAULT_CAPACITY = 10;
//空的数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认的空的数组,和上边其实一样的
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//官方定义为元素缓存,这个是真正存放数据的数组
transient Object[] elementData; // non-private to simplify nested class access
//elementData数组中的元素个数
private int size;
构造函数
//这个是初始时自定义一个初始容量的数组
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//最常用的构造
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
扩容的关键代码
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
//这玩意是list修改的一个计数器,用于迭代器中,防止在迭代器遍历时元素修改
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//这里是关键的扩容代码,其实就是老数组容量扩大一半
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
注意点
1、数组定义时如果使用 List li = new ArrayList();这种,那么数组其实是{},一个空的,只有当第一个元素添加时,才会扩容;
2、扩容时,这里就用到上面属性中说的:DEFAULTCAPACITY_EMPTY_ELEMENTDATA 了,还有一个属性也表示一个空的数组,就是EMPTY_ELEMENTDATA ,它们的区别在这里就体现出来了:标识是否初始时有没有自定义容量。如果没有,那么第一次扩容就是10咯!
3、关于ArrayList的三个构造,我其实觉得没什么区别,如果在知道要存入的元素个数时,最好手动指定容器大小,这样就避免了不必要的扩容,但是性能优化并不是很明显
4、它不是线程安全的,如果想要线程安全,官方建议是使用:List list = Collections.synchronizedList(new ArrayList(…)); 这种
LinkedList
使用双向链表实现的容器,具有双向链表的特性。LinkedList的底层维护了一个双向链表,它同时实现了List接口和Deque接口,因此它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(stack),因为它实现了deque接口
属性
transient int size = 0;
//指向链表的第一个节点的指针
transient Node<E> first;
//指向链表的最后一个节点的指针
transient Node<E> last;
双向链表结构实现
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;
}
}
增加操作主要代码块
//在链表的最后追加一个元素
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
注意点
1、当第一次添加元素前,last和first指针都是null,添加后首指针和尾指针都指向了第一个元素
2、在链表尾添加元素其实就两个动作:将最后一个节点的next指针指向新节点;新节点的pre指针指向最后一个节点
删除操作主要代码块
//删除链表这个位置的元素
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
//返回这个位置的节点
Node<E> node(int index) {
// assert isElementIndex(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;
}
}
//将这个节点从链表中删掉,其实就是把这个节点的pre和next指针置空,它的前节点的next指针指向它的后一个节点,它的后一个节点的pre指针指向它的前一个节点
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
注意点
1、传入的这个index并不是LinkedList的下标,而是从first指针开始的第几个元素。
2、所有的操作都是基于双向链表实现。
3、LinkedList也不是线程安全的,如果想线程安全,官方建议是使用List list = Collections.synchronizedList(new LinkedList(…));
ArrayList和LinkedList的区别
因为ArrayList底层是动态数组,是顺序存储,对于访问操作,可以直接定位到指定位置,但是增加有可能要扩容,需要时间,删除会影响数据下标,需要重排,需要时间,它是随机访问,时间复杂度为O(1)
LinkedList底层是双向链表,是线性存储,对于访问操作,需要指针从前往后遍历查找,不能直接定位。
对于删除和增加操作,只需要找到指定位置添加即可,它是顺序访问,时间复杂度是线性的,随着元素的增多,时间可能会延长
Stack & Queue
Java中的deque接口是Statck和queue的接口。实现了deque,就能实现栈或者队列。
当使用栈或者队列时,可以使用LinkedList或者更高效的ArrayDeque。
Deque是继承Queue接口的接口,它代表了双向队列,即可以在头和尾进行操作 它的两个实现类是ArrayDeque和LinkedList
ArrayDeque
实现了Deque接口,所以,可以实现栈或者队列,同时也是栈或者队列的首选
特性:
1、和ArrayList一样,也是动态数组实现
2、线程不安全
3、比较适合作为双向队列使用
HashMap
1、HashMap类与HashTable非常相似,不同的是HashMap是非线程安全的,并且允许为null。
2、HashMap对于get和put操作的时间复杂度是常数级别的
3、HashMap有两个参数会影响它的性能:initial capacity 和 load factor。
initial capacity 是哈希表中buckets的数量
load factor是一种方法,涉及到自动扩容,负载系数用来指定自动扩容的临界值 ,当entry的数量超过capacity*load_factor时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。
当哈希表中的条目超过了负载系数 和当前容量时,哈希表被重新格式化(即内部数据结构)
load factor默认是 .75,它是时间复杂度和空间复杂度的折中,它的值越高,空间复杂度越低,时间复杂度越高。反之亦然。
不能将初始容量(initial capacity)设太高或负载系数 (load factor)设太低
4、与TreeMap相比,它不保证元素的顺序
5、根据对冲突的处理方式不同,哈希表有两种实现方式,一种开放地址方式(Open addressing),另一种是冲突链表方式(Separate chaining with linked lists)。Java7 HashMap采用的是冲突链表方式。
6、将对象放入到HashMap或HashSet中时,有两个方法需要特别关心: hashCode()和equals()。hashCode()方法决定了对象会被放到哪个bucket里,当多个对象的哈希值冲突时,equals()方法决定了这些对象是否是“同一个对象”。所以,如果要将自定义的对象放入到HashMap或HashSet中,需要@OverridehashCode()和equals()方法。
7、有两个参数可以影响HashMap的性能: 初始容量(inital capacity)和负载系数(load factor)。初始容量指定了初始table的大小,负载系数用来指定自动扩容的临界值。当entry的数量超过capacity*load_factor时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。
冲突链表
根据对冲突的处理方式不同,哈希表有两种实现方式,一种开放地址方式(Open addressing),另一种是冲突链表方式(Separate chaining with linked lists)
冲突链表中,哈希表的每一个地址存放一个指针指向一个链表,这个链表包含了所有的被哈希为这个地址的值。
key其实就是指针数组的下标,value对应的是这个下标指针指向的链表,这个链表里面存储的就是这个key,也就是下标对应的value值!
冲突链表的优缺点:
优点:
1、当被存储的key value的数量大于哈希表的存储数量时,依然是有效的,可利用的
2、性能优越,有效的算法来处理冲突碰装
缺点:
1、当存储的key的数量不断增加,哈希表的性能会越来越低。换句话说,就是指针数组的存储位置越大,哈希表的性能越低。也就是其他人说的bucket的大小,bucket就是指针数组,entity对应的是链表
例如:冲突链表有1000个内存地址,存储了10000个keys,它比存储位置为10000的冲突链表的性能要小5到6倍
2、遗传了链表的缺点,当存储value时,创建指针开销很大,其次,遍历链表的缓存性能较差,使处理器缓存无效,我也不懂,书上这么说的,大概意思是说,在查找和删除操作中,需要线性遍历链表,直到找到对应的value,随着链表的长度越大,时间复杂度越高(从最坏的情况考虑)。
结论:
冲突链表对于添加操作是O(1),对于查找和删除操作是O(n),n为链表的长度,所以,它适合添加操作,对于迭代比较频繁的场景,不宜将HashMap的初始大小设的过大
java8
Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成
根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。
为了降低这部分的开销,在 Java8 中,当链表中的元素达到了 8 个时,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)
Java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。
我们根据数组元素中,第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树的
//默认初始容量,必须为2的指数
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大的容量,必须为2的指数且小于2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//
static class Node<K,V> implements Map.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;
}
//链表节点结构
static class Node<K,V> implements Map.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;
}
//红黑树节点结构
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
map.put过程描述:
1、map.put(”暴躁“,”小刘“);
2、获取”暴躁“字符串的hash值
3、经过hash值扰动函数,使hash值更加散列
4、构造出node对象(hash->1122 key->”暴躁“ value->”小刘“) next->null)
5、路由算法,找出node对应的数组的位置
HashMap源码分析
静态常量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当链表长度超过这个阈值时,且哈希表中存储的节点超过64,链表转为红黑树
static final int TREEIFY_THRESHOLD = 8; 树型阈值
//当红黑树的节点个数小于6的时候,变为链表
static final int UNTREEIFY_THRESHOLD = 6; 非树型阈值
//当哈希表中的节点个数超过64时,才能有机会将链表转为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
总结几点:容量必须为2的倍数,且最大容量不超过2的30次方
默认的负载因子是0.75
字段
transient Node<K,V>[] table; 哈希表
transient Set<Map.Entry<K,V>> entrySet;
transient int size; 当前哈希表中元素个数
transient int modCount; 结构修改计数器
int threshold; 扩容阈值,当你的哈希表中的元素超过阈值时,触发扩容 翻译为:门槛
final float loadFactor; 负载因子
总结几点:负载因子就是一个常量0.75,这个值是最有效的,我们不用去改
threshold = capacity * load factor
构造函数
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);
}
//下边这两个静态方法用于将cap转为2的倍数
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
@IntrinsicCandidate
public static int numberOfLeadingZeros(int i) {
// HD, Count leading 0's
if (i <= 0)
return i == 0 ? 32 : 0;
int n = 31;
if (i >= 1 << 16) { n -= 16; i >>>= 16; }
if (i >= 1 << 8) { n -= 8; i >>>= 8; }
if (i >= 1 << 4) { n -= 4; i >>>= 4; }
if (i >= 1 << 2) { n -= 2; i >>>= 2; }
return n - (i >>> 1);
}
为啥 threshold 不直接等于 initialCapacity * loadFactor呢?
因为哈希表的容量必须是2的倍数,你传进来的initialCapacity通过了校验,但是不能确定是2的倍数,所以,需要tableSizeFor方法返回一个大于等于当前initialCapacity的数字,且这个数字是2的倍数!
put方法
public V put(K key, V value) {
//这个hash函数就是路由寻址,找到key对应的散列表的下标
return putVal(hash(key), key, value, false, true);
}
//tab:引用当前的hashmap的散列表
//p:表示当前散列表的元素
//n:表示散列表数组长度
//i:表示路由寻址 结果
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//延迟初始化逻辑,第一次调用putval时会初始化hashmap中的最耗费内存的散列表
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//最简单的一种情况:寻址找到的桶位,刚好是null,这个时候,直接将当前的k-v=>node扔进去即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//e:不为null的话,找到了一个与当前要插入的k-v一致的元素
//k:表示临时的key
Node<K,V> e; K k;
//表示桶位中的该元素,与你当前插入的元素的key一致,后续需要替换
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);
//链表时,而且链表的头元素与我们要插入的key不一致
else {
for (int binCount = 0; ; ++binCount) {
//表示遍历到了链表最后也没找到与你要插入的key一致的元素,需要加到链表后面
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//说明当前链表的长度,达到了树化标准,需要树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//条件成立的话,说明找到了相同key的node元素,需要进行替换操作
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//条件成立说明找到了一个与你插入元素key完全一致的数据,需要替换
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//插入新元素,size自增,如果自增后的值大于扩容阈值,则扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
resize扩容机制
final Node<K,V>[] resize() {
//oldTab:引用扩容前的哈希表
Node<K,V>[] oldTab = table;
//oldCap:表示扩容前table数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//oldThr:表示扩容前的扩容阈值
int oldThr = threshold;
//newCap:扩容后的数组大小
//newThr:扩容后触发扩容的阈值
int newCap, newThr = 0;
//条件成立的话,表示hashmap中的散列表已经初始化过,是正常扩容
if (oldCap > 0) {
//表示扩容之前的table数组大小已经达到了最大阈值,不能扩容,直接返回即可
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//oldCap左移一位,翻倍赋值给newCap,扩容阈值也翻一倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//条件成立,表示oldCap为0,说明hashmap中的散列表是null
//1、new HashMap(initCap,loadFactor);
//2、new HashMap(initCap);
//3、new HashMap(map); 且这个map有数组
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//条件成立,表示oldCap为0且oldThr为0
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY; //16
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;
@SuppressWarnings({"rawtypes","unchecked"})
//创建一个新的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//说明hashmap扩容前table不为null,即有数据
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//说明当前桶位中有数据,但是数据类型不知道,可能是链表或树
if ((e = oldTab[j]) != null) {
//方便jvm GC垃圾回收
oldTab[j] = null;
//第一种情况:如果这个节点不存在下一个节点,即不存在哈希碰撞,直接使用路由算法hash&(length-1)得到新哈希表中的桶下标进行存储
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;
}
get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(key)) == null ? null : e.value;
}
//tab:引用当前hashmap的散列表
//first:桶位中的头元素
//e:临时弄得元素
//n:table数组长度
final Node<K,V> getNode(Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & (hash = hash(key))]) != null) {
//第一种情况:定位出来的桶位元素即为我们要的数据
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//第二种情况:当前桶位不止一个,可能是链表,可能是红黑树
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//第三种情况:桶位形成链表
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}