视频教程:韩顺平 https://www.bilibili.com/video/BV1YA411T76k
参考笔记:https://www.wkliu.top/index.php/archives/23/
一、List接口
1. ArrayList:Object数组
- ArrayList 是
List
的主要实现类,底层使用Object[ ]
存储,适用于频繁的查找工作,效率高,但线程不安全;
a. 线程不安全
- ArrayList 线程不同步,即不保证线程安全;
// ArrayList 是线程不安全,没有用synchronized修饰
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
b. 扩容机制
- ArrayList 中维护了一个
Object
类型的数组elementData
,用于保存ArrayList数据; transient
关键字表示该属性不会被序列化。
transient Object[] elementData; // non-private to simplify nested class access
- 当创建ArrayList对象时,若使用的是无参构造器,则初始
elementData
容量为0; - 添加元素时使用
ensureCapacityInternal()
方法来保证容量足够,如果不够时,需要使用grow()
方法进行扩容;【每次add()
均需要判断ensureCapacityInternal()
】 - 第一次添加,则扩容
elementData
为10,即DEFAULT_CAPACITY = 10
;如需再次扩容,则新容量大约为旧容量的1.5倍,即newCapacity = oldCapacity + (oldCapacity >> 1)
。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private static final int DEFAULT_CAPACITY = 10;
public ArrayList() {
this.elementData = DEFAULTCAPACITY_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) {
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);
}
- 若使用的是指定大小的构造器,则初始
elementData
容量为指定大小,如果需要扩容,则直接扩容elementData
为1.5倍。
private static final Object[] EMPTY_ELEMENTDATA = {};
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);
}
}
c. 序列化
- ArrayList 实现了
writeObject()
和readObject()
来控制元素的序列化和反序列化; - 根据
size
序列化真实的元素,而不是根据数组的长度序列化元素,减少了空间占用
2. Vector:Object数组
- Vector 是
List
的古老实现类,底层使用Object[ ]
存储,线程安全,但效率低; - 实现与ArrayList类似。
a. 线程安全
- 在多线程使用共享资源的时候, 我们可以使用
synchronized
来锁定共享资源。通过该方式得到实例对象锁,锁定的方法或者代码块为互斥访问区。如果一个对象有多个synchronized
方法(即互斥访问区),只要一个线程访问了其中的一个synchronized
方法(即获得了实例对象锁),其它线程就不能同时访问这个对象中任何一个synchronized
方法(即互斥访问区的访问阻塞),直到第一个线程释放锁。 - 该实例对象没有用
synchronized
修饰的方法,其他线程仍旧可以访问。 - Vector 实现与 ArrayList 类似,但是使用了
synchronized
进行同步,因此线程安全。
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
b. 扩容机制
- 若调用无参构造器,默认容量为10(本质调用有参构造器)
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
// 无参构造器
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector() {
this(10);
}
- 若容量不够,就按2倍扩容(不考虑自定义扩容增量)。其中,
capacityIncrement
表示自定义的扩容增量,默认为0。
private void ensureCapacityHelper(int minCapacity) {
// 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 + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
3. LinkedList:双向链表
- LinkedList 底层维护了一个双向链表,使用 Node 存储链表节点信息;
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
- LinkedList 中维护了两个属性 first 和 last 分别指向 首节点和尾节点;
transient Node<E> first;
transient Node<E> last;
- LinkedList的元素添加
add()
本质是尾插法,源码如下:
public LinkedList() {
}
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++;
}
- LinkedList 元素删除
remove()
本质是头删法,源码如下:
public E remove() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
常考问题:
二、Set接口
1. HashSet:HashMap(数组+链表/红黑树)
- 无序(添加和取出的顺序不一致),没有索引,但每次取出的元素顺序固定一致
- 不允许重复元素,所以最多包含一个
null
- 遍历方式:可使用迭代器、增强for,但不能使用索引的方式遍历
- 底层为
HashMap<K,V>
,其中K
为存放得对象,V
为常量present
。
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
HashMap
底层是 数组+链表/红黑树(其中,数组+链表即为哈希表)
- 元素较少:数组 + 链表 结构
2. 元素较多:数组 + 红黑树 结构
a. add() 底层原理
HashSet
底层是HashMap
- 添加一个元素时,先得到
hash
值 会转成 -> 索引值 - 找到存储数据表table,看这个索引位置是否已经存放的有元素
- 如果没有,直接加入
- 如果有,调用
equals()
比较, 如果想同,就放弃添加(实则替换),如果不相同,则添加到最后 - 在 java8 中,如果一条链表的元素个数超过
TREEIFY_THRESHOLD
(默认是8),并且table (数组)的大小 >=MIN_TREEIFY_CAPCAITY
(默认64),就会进行树化(红黑树)
确实是要超过8,因为binCount >= TREEIFY_THRESHOLD - 1这个条件的binCount=0对应的是第一个节点p.next,所以阈值就要多加1
- 关键源码:
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//hashcode不等于hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
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 {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
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;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
b. 扩容机制
HashSet
底层是HashMap
,第一次添加时,table数组扩容到16,临界值(threshold)是 16 × \times ×加载因子(loadFactor 0.75) = 12。如果table中所有元素数量size
(包括在table表,与表中链表)到了临界值12,table数组大小就会扩容到 16 × \times × 2 = 32,新的临界值就是 32 × \times × 0.75 = 24,依次类推;- 在Java8中,如果一条链表的元素个数到 TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制
注意:
- 每添加一个元素(包括在table表,与表中链表)即添加一个节点,会执行一次 ++size,当size > threshold 时就会执行扩容。
- table表扩容并不是表的16个大小被添加完才执行,当所有元素的个数大于临界值时就会执行扩容。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
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;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
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;
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;
}
常考问题:
-
hash
函数结果是否等于hashcode
不相等,
(h = key.hashCode()) ^ (h >>> 16)
-
hash
函数如何实现?将
hashcode
无符号右移16位,从而高16位和低16位同时参与异或运算,实现hash函数,减少哈希碰撞。 -
hashmap
转成红黑树的两个条件?链表长度超过8;数组长度到64:
a.TREEIFY_THRESHOLD=8
,则binCount>=7
时调用treeifyBin
函数。binCount
从0开始计数,而且是从数组上头结点的下一位开始(即p.next
),因此当binCount>=7
时,该节点为链表上的第9个元素,即链表长度超过8。
b. 当链表长度超过8时,分为两种情况:
1)当数组长度未达到64时,继续动态扩展数组长度,即newCap = oldCap << 1
且newThr = oldThr << 1
;
2)当数组长度达到64时,链表转为红黑树
2. LinkedHashSet:LinkedHashMap(数组+双向链表+单链表/红黑树)
- 有序 + 不重复性,最多包含一个
null
LinkedHashSet
是HashSet
的子类;LinkedHashSet
底层是一个LinkedHashMap
,底层维护了一个 哈希表 + 双向链表,即数组+单链表+双向链表。同时该数据结构也存在树化现象。LinkedHashSet
根据元素的hashCode
值来决定元素的存储位置(即(n - 1) & hash
),使用单链表解决哈希冲突,同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存。本质上,LinkedHashSet
的结构在LinkedSet
的基础上引入了双向链表。LinkedHashSet
不允许添加重复元素
注意
-
在
LinkedHashSet
中维护了一个哈希表(数组+单链表)和双向链表(LinkedHashSet
有head
和tail
); -
HashMap$Node
类型的数组中存放的元素不再是Node
类型,而是LinkedHashMap$Entry
类型。Entry
是Node
的子类,具有继承关系(这里体现了多态性,即父类引用可以指向子类对象);
-
除了
next
属性(继承)外,每一个节点还有before
和after
属性,这样可以形成双向链表/** * HashMap.Node subclass for normal LinkedHashMap entries. */ static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
-
在添加一个元素时,先求
hash值
,在求索引,确定该元素在table
的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加【原则和hashset
一样】)。这样遍历LinkedHashSet
也能确保插入顺序和遍历顺序一致。 -
底层原理和扩容机制和
HashSet
相同。
3. TreeSet:TreeMap(红黑树)
-
底层为
TreeMap
,详见TreeMap
原理public TreeSet() { this(new TreeMap<E,Object>()); }
-
不允许添加重复元素,不允许添加
null
-
无序(没有按照输入顺序进行输出)
-
遍历结果有顺序(排序)
-
底层为二叉树,且采用中序遍历得到结果 (左节点,根节点,右节点)
三、Map接口
基本概念
-
Map
和Collection
并列存在。用于保存具有映射关系的数据:key-value
(键值对)。Map
中的key
和value
可以是任何引用类型的数据,会封装到HashMap$Node
对象中; -
Map
中的Key
不允许重复,原因和HashSet
一样;Map
中的value
可以重复。Map
的key
可以为null
,value
也可以为null
,注意key
为null
,只能有一个; -
key
和value
之间存在单向一对一关系,即通过指定的key
总能找到对应的value
。常用String
类作为Map
的key
; -
Map
存放数据的key-value
示意图,一对k-v
是放在一个Hash Map$Node
中的。又因为Node
实现了Map.Entry
接口,有些书上也说,一对k-v
是一个Entry
。理解如下:为了方便程序员的遍历,
k-v
创建HashMap$Node
的同时,还会创建EntrySet
集合。该集合存放的元素类型为Entry
,每个Entry
对象都有key
和value
,即transient Set<Map.Entry<K,V>> entrySet;
。尽管在EntrySet
中元素定义的类型为Map.Entry
,但实际上存放的还是HashMap$Node
(多态性)。实际上,每个Entry
中分别存放的是key
和value
的引用地址,而非创建新对象。而Map.Entry
提供了getKey()
和getValue()
,方便遍历。
遍历方式
-
第一类方法:先利用
map.keySet()
取出所有key
,再通过map.get(key)
取出value
(两种方法:迭代器或增强for); -
第二类方法:利用
map.values()
取出所有的value
(两种方法:迭代器或增强for); -
第三类方法(推荐):利用
map.entrySet()
获取EntrySet
对象,再通过getKey()
和getValue()
获取key-value
(两种方法:迭代器或增强for)。Set entrySet = map.entrySet(); for (Object entry : entrySet) { // Map.Entry 提供 getKey()和 getValue() // entry类型为 HashMap$Node,向下转型为 Map.Entry Map.Entry m = (Map.Entry) entry; System.out.println(m.getKey()+"-"+m.getValue()); }
1. HashMap
- 如果添加相同的
key
,则会覆盖原来的value
; key-value
键和值均可为null
;- 与
HashSet
一样,不保证映射的顺序(即无序),因为底层是以(数组+链表或红黑树 )的方式来存储的; HashMap
没有实现同步,因此是线程不安全的,方法上没有做同步互斥操作,没有synchronized
。
a. put() 底层原理【尾插法】
-
HashMap
底层维护了HashMap$Node
类型的数组table
,默认为null
-
当创建对象时,将加载因子
(loadfactor)
初始化为0.75 -
当添加
key-val
时,通过key
的哈希值得到在table
的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key
是否和准备加入的key
相同。如果相等,则直接替换value
;如果不相等需要判断是树结构还是链表,做出相应处理。如果添加时发现容量不够,则需扩容。
-
第一次添加,则需要扩容
table
容量为16,临界值(threshold)
为12 (16 *0.75)。以后再扩容(即size > threshold
),则需要扩容table
容量为原来的2倍(即32),临界值为原来的2倍(即24),依次类推 -
在Java8中,如果一条链表的元素个数超过
TREEIFY_THRESHOLD
(默认是8),也就是第9个元素,并且table.length
>=MIN_TREEIEF_CAPACITY
(默认是64),就会进行树化(红黑树)。
2. Hashtable:数组 + 链表
- 存放的元素是键值对:
K-V
Hashtable
的键和值都不能为null
,否则会抛出NullPointerException
Hashtable
使用方法基本上和hashMap
一样Hashtable
是线程安全的(synchorized
),hashMap
是线程不安全的- 底层结构:数组 + 链表
a. put() 底层原理【头插法】
- 底层有一个数组
Hashtable$Entry[]
,初始化大小为11,而加载因子(loadfactor)
初始化为0.75。
- 当添加
key-val
时,通过key
的哈希值得到在table
的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key
是否和准备加入的key
相同。如果相等,则直接替换value
;如果不相等,则以头插法的方式添加元素。如果添加时发现容量不够,则需扩容。 - 注意:
hash
值和索引值的计算与HashMap
不同。
b. 扩容机制
- 临界值
threshold = newCapacity * loadFactor
; - 当
if (count >= threshold)
满足时,就进行扩容:
按照int newCapacity = (oldCapaticy << 1 ) + 1;
的大小扩容
3. TreeMap【红黑树】
- 无序(输入和取出顺序不一致);
- 有序(按照升序或降序排列);
- 底层:二叉树,
key
遵照二叉树特点; key
不能为null
,但value
可以;key
对应的类型内部一定要实现比较器(默认内部比较器,也可以定义外部比较器)。
a. put() 原理
- 若重写比较器,则按照重写后的比较规则进行排序(二叉树结构); 若比较结果(
cmp
)为 0,则替换value
。
- 若采用默认比较器,则以
String
类型比较为例,按照ASCII码值先小到大排序(二叉树结构)。 若比较结果(cmp
)为 0,则替换value
。