1.容器
Java中提供了一套完整的容器存储体系来供开发人员进行多种类型对象的存储需求。
1.容器体系
Java中的容器体系为两大部分:Collection集合体系、Map容器体系
Collection接口:
List接口:Vector、ArrayList、LinkedList
Queue接口:
Set接口:TreeSet、HashSet
Map接口:
HashTable、HashMap、TreeMap
2.四者区别
List(顺序):元素存储有序、可重复;
Queue(队列):元素存储有序、可重复;
Set(去重):元素存储无序、不可重复;
Map(映射):元素存储无序、key-value均不可重复、key只能存在单个null值。
注意点:
无序性:无序性指的不是随机性、而是说底层不是按照数组索引的方式进行存储,是由Hash散列值决定
唯一性:通过元素的equals()和hashcode()两者一同决定。
3.迭代器
任何容器,都必须提供某种插入元素后可以取出的方法。不同的容器底层具有不同的实现,需要提供各自的方法进行操作。存在的问题是对于之间写过的代码无法重用于新的容器。
迭代器(是一种设计模式):将上层容器的操作与底层的具体实现解耦。迭代器中提供了hasNext()、next()方法来进行通用的容器操作。
public class Demo {
public static void main(String[] args) {
Collection collection = new ArrayList();
collection.add("Java");
collection.add("Python");
// 获取迭代器进行遍历
Iterator iterator = collection.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
2.List
1.体系&特点
体系:List接口下提供了Vector、ArrayList、LinkedList三种子类实现
特点:List接口中,元素存储有序、可重复
2.Vector
Vector是一个较为古老的List容器、是List接口体系中唯一一个线程安全的容器、底层为Object[]数组。
3.ArrayList
ArrayList是List接口体系中最为常用的数组容器、是线程不安全的,底层为Object[]数组。
4.ArrayList-扩容机制
1.ArrayList中提供了三种构造方式
空参构造:不给定初始容量、在初始化创建时,底层指定容量为0;
有参构造:给定初始化容量、在初始化创建时,底层直接赋值该合法容量。
/**
* 默认容量大小
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空元素数组
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 默认容量空元素数组
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* Object元素数组
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* 数组元素个数
*/
private int size;
/**
* 空参构造
*/
public ArrayList() {
// 空参构造,赋值空数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 有参构造
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 合法,则直接指定该合法容量
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 为0,则赋值空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 不合法,抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
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 static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
2.ArrayList添加元素操作
a.add():该方法中将size+1后调用ensureCapacityInternal()确保容量足够;然后将元素加入elementData[]元素数组中。
b.ensureCapacityInternal()方法中会根据构造器容量初始化,赋值minCapacity最小容量。
如果elementData[]为空,则最小容量初始化为10,然后再次调用ensureExplicitCapacity(minCapacity)确保第一次初始化后容量是否足够;
如果elementData[]不为空,则直接调用ensureExplicitCapacity(minCapacity)确保第一次初始化容量足够。如果minCapacity(增加元素后的大小) > elementData[](提供的容量大小)则调用grow()扩容。
c.grow()方法中会在第一次舒适化容量的基础之上,通过 >> 1(有符号右移一位)来进行扩容,也就是在原有
容量基础之上增加1/2。扩容到原有的1.5倍。
d.每次扩容之后,会调用Arrays.copyOf()方法将旧数组复制到新数组中。而copyOf()方法中又调用了System.arraycopy()方法进行数组复制。
注意点:由于ArrayList中每进行一次数组扩容,则需要进行数组的拷贝动作。所以,建议在初始化时就指定容量大小,以减少复制次数,提高系统性能。
public boolean add(E e) {
// size + 1 后确保容量是否足够
ensureCapacityInternal(size + 1);
// 赋值元素到Object[]中
elementData[size++] = e;
return true;
}
/**
* size + 1 后确保容量足够
*/
private void ensureCapacityInternal(int minCapacity) {
// 如果数组为空,则直接初始化默认容量为10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 最小容量 = Max( 默认容量10, 新增元素后的最小容量=1)
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 再次确保容量是否足够
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 需要的最小容量,能提供的容量 > 0则说明不够,需要进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* grow()方法是ArrayList中用来进行扩容的方法
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// 获取能提供的容量大小
int oldCapacity = elementData.length;
// 第一次扩容:old向右移动一位,相当于old / 2 = 0.5;再原有容量的基础上增加1/2 = 1.5倍 (奇偶不同)
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);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
5.Vector&ArrayList差异
ArrayList和Vector之间最大的区别就是Vector是线程安全的。
6.LinkedList
LinkedList也是List接口体系中的一个子类实现,也是线程不安全容器。底层为双向链表。
7.ArrayList&LinkedList差异
访问操作:ArrayList支持随机访问、且改查快速;LinkedList不支持随机访问,且增删快速。
线程安全:两者均为线程不安全性容器。
底层机制:ArrayList底层为Object[]数组;LinkedList在JDK6之前为循环链表、之后为双向链表。
空间占用:ArrayList需要预留空间;LinkedList需要空间存储指针。
3.Set
1.体系&特点
2.HashSet
HashSet是Set接口下的一个子类实现,也是线程不安全的,底层由HashMap[数组+链表]实现。
3.HashSet如何去重
1、HashSet调用add()时,会先通过元素的hashcode()计算出hash索引(元素在table中所处的位置)
2、当前位置无元素,则直接插入;有元素,则比较hashcode();
3、hashcode()不同,则认为不重复,进行插入;hashcode()相同,则再比较equals(),相同则直接覆盖。
4.Set中元素顺序
不同的Set底层实现具有不同的顺序保障机制
1、HashSet中,通过hashCode()来保障元素的口顺序;
2、TreeSet中,实现了Comparable接口,通过compareTo()方法对元素比较来保障元素顺序;
3、LinkedhashSet中,通过元素插入来保障元素顺序。
// hashCode() 异或 (hashCode() 无符号右移16位)
int hash = key.hashcode() ^ (key.hashCode() >>> 16)
4.Map
1.体系&特点
特点:元素存储无序、key-value双列存储、key中只能存在一个null值,value中可以重复。
实际存储:key-value实际上是存储在hsahMap中的一个静态内部类Node(hash, key, value, next)中。
2.HashMap
HashMap也是Map接口下的一个子类实现,也是线程不安全的容器存储。
底层机制:
JDK7~哈希表[数组 + 链表]
JDK8~哈希表[数组 + 链表] + 红黑树
3.HashMap-扩容机制
散列计算:
HashMap中通过(n - 1) & hash来计算散列位置。在JDK7和JDK8中具有不同的一个实现
JDK7:JDK7中HashMap的底层实现为 [数组 + 链表]
计算Hash方式:该hash()方法中进行了四次扰动,性能较为低效。
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
**JDK8:**JDK8中HashMap的底层实现为[数组 + 链表] + 红黑树<br />计算Hash方式:该hash()方法中进行了两次扰动,性能有了很大提升。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
扩容机制流程图:
put方法扩容:
相关参数:
/**
* 加载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 单个链表结点长度
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 哈希表中的结点总数大小
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 最大容量,2的幂次方 2^30
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认初始容量,2的幂次方 = 16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
**构造方法:**
/**
* 空参构造:默认初始容量16,加载因子0.75
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* 有参构造:指定初始容量、加载因子还是0.75
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_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);
}
**添加元素:**
/**
* put方法添加元素:先调用hash()方法计算key所在的hash索引
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* key == null ? 直接返回0 : (哈希 异或 哈希无符号右移16位)
* 目的是为了降低hash碰撞的情况
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 3、添加元素方法:计算出的hash索引、key、value(为了双列存储)
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1.空参构造:resize()中初始化容量为16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2.计算hash索引:获取元素插入的数组下标
if ((p = tab[i = (n - 1) & hash]) == null)
// 为空,则直接加入
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// key相同:hashCode()、equals()相同,则直接覆盖旧元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 3.key不同:判断是链表还是红黑树
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;
// threshold为扩容阈值
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
/**
* 4、扩容方法:初始容量扩容为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
// 初始化容量为16
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;
}
**扩容总结:**<br />1.根据构造初始化判断table是否为空,空参构造则直接初始化扩容为16,否则初始化为合法容量;<br />2.添加元素,通过hashCode()计算hash索引位置;<br />3.根据hash索引,判断索引处是否为空;为空,则直接加入;<br />4.不为空,判断索引处key是否相同;相同,则直接覆盖;<br />5.不相同,判断是红黑树、还是链表;<br />6.红黑树则按照红黑树插入;链表则对链表循环,然后插入。<br />7.后续插入扩容,**容量 * 加载因子 = 阈值,**达到阈值则进行扩容(初始为12)。<br />**8.当单条链表结点大于8,且整个table总结点数大于64,则进行树化。**<br />**9.单链表结点大于8,但table总结点数小于64,则进行数组扩容,不进行树化。**<br />**rehash问题:**<br />每次扩容时,都需要重新分配hash、也就是rehash()。<br />HashMap扩容,每次都是之前的两倍。通过(n -1) & hash时,只是比之前多了一个**bit位**。原结点位置要么是原先的索引位置,要么是“原位置 + 旧容量”。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/27397677/1653962078073-4732ba6d-e291-4f0d-a932-176d8595c84f.png#clientId=u85a0d0c9-6fcc-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u51561b84&margin=%5Bobject%20Object%5D&name=image.png&originHeight=568&originWidth=1296&originalType=url&ratio=1&rotation=0&showTitle=false&size=131406&status=done&style=none&taskId=uff3449a8-3a44-4820-8e6a-6d20763dc35&title=)<br />这样的设计,在进行扩容重新分配时,则只需要查看新增bit为是1,还是0即可快速重新分配。
4.HashMap-7&8差异
JDK7:
底层~数组+链表;新增元素~采用头插法(存在死循环);扩容机制~插入前扩容、hash需要四次扰动;
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
JDK8:
底层~数组+链表+红黑树;新增元素~采用尾插法;扩容机制~插入后扩容、hash需要两次扰动;
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
5.负载因子为何0.75f?
作用:负载因子决定了HashMap中数据密度。是一种时间与空间的折衷。
过大:负载因子过大,则会导致数据密度过大、单表链表更长、插入&查询元素需要比较的次数更多;
节省了空间、浪费了时间。
过小:负载因子过小,则会导致数据密度过小、单条链表更短、插入&查询元素需要比较的次数更少;
节省了时间、浪费了空间。
6.树化为何选择红黑树?
二叉查找树、红黑平衡二叉查找树的一种选择问题。
二叉查找树在某种情况下也是一种线性查找,这种线性查找其实和链表是没有区别的。
红黑树是一种平衡二叉树,查询时采用二分查找法,查询更加快速高效。
7.红黑树?
1、根结点总是黑色、其他结点不是黑色就是红色;
2、红结点的子节点必定是黑色,反之不成立;
3、叶子结点均为黑色的空结点;
4、根结点到叶子结点的每条路径,必须包含相同数量的黑色结点。
8.HashMap-数组总为2的幂次方?
HashMap中需要通过取模获取余数运算来降低Hash冲突、采用2的幂次方会更加方便进行取模运算,降低了hash冲突、元素存取更加高效可用。
9.HashTable&HashMap差异?
操作效率:HashMap效率更到;HashTable由于线程安全,则效率较低。
线程安全:HashTable线程安全,通过synchronized锁方法进行修饰。
Null值:HashTable中的key-value均不允许为Null。
底层结构:HashTable中无树化机制。
扩容机制:无初始容量,则HashTable初始值为11。
5.线程安全容器
经典案例:单词统计中,多个线程同时进行统计,使用HashMap存储,结果都不一致。
1.ConHashMap-体系&特点
体系:ConcurrentHashMap implements ConcurrentMap接口 extends Map接口
特点:并发安全容器,底层采用了Synchronized锁对象来保障线程安全。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 此处,添加了锁对象
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
2.HashMap循环链表问题?
JDK7中采用了头插法插入元素,头插法中在并发情况下扩容重新分配时,则会出现循环队列问题。
resize()源码
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 创建新数组进行扩容
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
先创建新数组、然后按照链表顺序,逐条链表赋值到新数组中。在重新计算链表结点并进行修改的过程中会出现循环链表问题。
过程演示:
1、初始插入元素:顺序为A->B->C
2、线程T1、T2并发进行扩容。T1、T2同时指向A结点,.next均指向下一个结点B。
3、此时,线程T2由于阻塞,停止扩容;T1正常进行扩容。
T1扩容之后,顺序为C->B->A
此时:T1中,顺序是由C->B->A的。但在T2中,顺序是A->B的,是相反的,则A和B之间就出现了循环。
3.ConHashMap-底层实现
Segments数组 + HashEntry数组+链表,采用分段锁保证线程安全。
默认Segments的个数为16.