1.ArrayList和LinkedList的区别
面试官: "请说说ArrayList和LinkedList的区别,什么时候用哪个?"
面试者:
-
底层实现: ArrayList基于动态数组,LinkedList基于双向链表
-
随机访问: ArrayList支持O(1)随机访问,LinkedList需要O(n)遍历
-
插入删除: ArrayList中间插入删除需要移动元素O(n),LinkedList只需要O(1)
-
内存占用: ArrayList只存储元素,LinkedList每个节点额外存储前后指针
深度解析:
// ArrayList底层结构
public class ArrayList<E> {
private Object[] elementData; // 动态数组
private int size; // 实际元素个数
// 扩容机制
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
// LinkedList底层结构
public class LinkedList<E> {
Node<E> first; // 头节点
Node<E> last; // 尾节点
int size;
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;
}
}
}
2.HashMap的底层原理
面试官: "HashMap是怎么实现的?为什么线程不安全?"
面试者:
-
JDK1.7: 数组+链表,头插法
-
JDK1.8: 数组+链表+红黑树,尾插法,链表长度>8转红黑树
-
线程不安全: 多线程扩容时可能形成环形链表,导致死循环
深度解析:
// HashMap核心结构
public class HashMap<K,V> {
Node<K,V>[] table; // 哈希桶数组
int size; // 元素个数
int threshold; // 扩容阈值
final float loadFactor; // 负载因子,默认0.75
// 节点结构
static class Node<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
// 红黑树节点
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent;
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;
boolean red;
}
// put方法核心逻辑
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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. 初始化或扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 计算索引位置,如果为空直接插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 3. 处理哈希冲突
Node<K,V> e; K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 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) // 链表长度>=8
treeifyBin(tab, hash); // 转红黑树
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
}
// 4. 检查是否需要扩容
if (++size > threshold)
resize();
return null;
}
}
3.ConcurrentHashMap的实现原理
面试官: "ConcurrentHashMap是如何保证线程安全的?JDK1.7和1.8有什么区别?"
面试者:
-
JDK1.7: 分段锁(Segment),继承ReentrantLock
-
JDK1.8: CAS + synchronized,锁粒度更细,性能更好
深度解析:
// JDK1.7 分段锁实现
public class ConcurrentHashMap<K, V> {
final Segment<K,V>[] segments;
static final class Segment<K,V> extends ReentrantLock {
volatile HashEntry<K,V>[] table;
volatile int count;
V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock(); // 对segment加锁
try {
// 具体的put逻辑
} finally {
unlock();
}
}
}
}
// JDK1.8 CAS + synchronized实现
public class ConcurrentHashMap<K,V> {
private volatile Node<K,V>[] table;
final V putVal(K key, V value, boolean onlyIfAbsent) {
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // CAS初始化
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// CAS插入新节点
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 协助扩容
else {
V oldVal = null;
synchronized (f) { // 对头节点加锁
if (tabAt(tab, i) == f) {
if (fh >= 0) {
// 链表插入
} else if (f instanceof TreeBin) {
// 红黑树插入
}
}
}
}
}
}
// CAS操作
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
}
4.HashSet和HashMap的关系
面试官: "HashSet是怎么实现的?为什么可以保证元素唯一?"
面试者: HashSet底层就是HashMap,元素作为key存储,value是固定的PRESENT对象
深度解析:
public class HashSet<E> {
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT) == null; // 利用HashMap的key唯一性
}
public boolean contains(Object o) {
return map.containsKey(o);
}
public boolean remove(Object o) {
return map.remove(o) == PRESENT;
}
}
5.fail-fast和fail-safe机制
面试官: "什么是fail-fast?如何避免ConcurrentModificationException?"
面试者:
-
fail-fast: 快速失败,检测到并发修改立即抛异常
-
fail-safe: 安全失败,在副本上遍历,不会抛异常
深度解析:
// ArrayList的fail-fast实现
public class ArrayList<E> {
protected transient int modCount = 0; // 修改次数计数器
private class Itr implements Iterator<E> {
int expectedModCount = modCount; // 期望的修改次数
public E next() {
checkForComodification();
// ...
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException(); // 快速失败
}
}
public boolean add(E e) {
modCount++; // 每次修改都增加计数
// ...
}
}
// CopyOnWriteArrayList的fail-safe实现
public class CopyOnWriteArrayList<E> {
private volatile Object[] array;
public boolean add(E e) {
synchronized (this) {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1); // 复制数组
newElements[len] = e;
setArray(newElements); // 原子性替换
return true;
}
}
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0); // 在快照上遍历
}
}
6.TreeMap的实现原理
面试官: "TreeMap是怎么实现有序的?红黑树的特点是什么?"
面试者: TreeMap基于红黑树实现,保证O(logn)的查找、插入、删除时间复杂度
深度解析:
public class TreeMap<K,V> {
private transient Entry<K,V> root;
private final Comparator<? super K> comparator;
static final class Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK; // 红黑树颜色
}
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
root = new Entry<>(key, value, null);
size = 1;
return null;
}
// 二分查找插入位置
int cmp;
Entry<K,V> parent;
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
// 插入新节点并调整红黑树
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e); // 红黑树平衡调整
size++;
return null;
}
}
红黑树特点:
-
每个节点要么红色要么黑色
-
根节点是黑色
-
叶子节点(NIL)是黑色
-
红色节点的子节点必须是黑色
-
从任一节点到叶子节点的所有路径包含相同数目的黑色节点
7.集合的选择策略
面试官: "在实际项目中,如何选择合适的集合类?"
面试者:
|
场景 |
推荐集合 |
原因 |
|---|---|---|
|
频繁随机访问 |
ArrayList |
O(1)随机访问 |
|
频繁插入删除 |
LinkedList |
O(1)插入删除 |
|
需要排序 |
TreeMap/TreeSet |
自动排序 |
|
高并发读写 |
ConcurrentHashMap |
线程安全 |
|
读多写少 |
CopyOnWriteArrayList |
读性能好 |
|
去重 |
HashSet |
基于HashMap |
性能对比:
public class CollectionPerformanceTest {
public static void main(String[] args) {
int size = 100000;
// ArrayList vs LinkedList 随机访问测试
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
// 填充数据
for (int i = 0; i < size; i++) {
arrayList.add(i);
linkedList.add(i);
}
// 随机访问测试
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
arrayList.get(i);
}
System.out.println("ArrayList随机访问: " + (System.currentTimeMillis() - start) + "ms");
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
linkedList.get(i);
}
System.out.println("LinkedList随机访问: " + (System.currentTimeMillis() - start) + "ms");
}
}
651

被折叠的 条评论
为什么被折叠?



