前端转Java面试备战 · SVG结构图+流程图 · 一图胜千言
List排队可重复,Set去重不排序,Queue先进先出队,Map键值成双对。
HashMap = 停车场:每个车位有编号(桶位index),同一车位的多辆车排成一列(链表),车太多了升级VIP区(红黑树)。
找车过程:拿车牌号算hash → 取模得到车位号 → 到车位找车。车位空就直接停,不空就排到后面。
JDK7 头插法 + 并发resize → 死循环:多线程同时扩容时,头插法导致链表形成环形引用,get操作陷入死循环。
JDK8 尾插法解决了死循环,但仍有数据覆盖问题:两个线程同时put,如果都判断tab[i]==null,后一个会覆盖前一个的写入。
AVL树要求严格平衡(左右子树高度差≤1),插入/删除时需要更多旋转操作。红黑树是弱平衡(最长路径不超过最短路径2倍),插入/删除时旋转次数更少。
HashMap的put/get/delete操作频繁,红黑树在写性能上更优,而查询性能差距可忽略(O(log n) vs O(log n))。
是时间和空间的折中:
• 太小(如0.5):频繁扩容,空间浪费大
• 太大(如1.0):hash冲突严重,链表过长,查询退化为O(n)
• 0.75在统计学上使得hash冲突概率在泊松分布下可控(每个桶平均0.5个元素时冲突率低)
JDK7 = 每层有独立门锁(Segment):停车场分16层,每层一把锁,不同层可同时进出,但同一层只能排队。
JDK8 = 每个车位有独立锁(CAS+synchronized):每个车位独立控制,不同车位完全并行,并发度 = 车位总数。
| 对比维度 | JDK7 Segment | JDK8 CAS+sync |
|---|---|---|
| 锁粒度 | Segment级别(段锁) | 桶头节点级别(行锁) |
| 并发度 | 固定=Segment数(默认16) | =数组长度(动态扩容) |
| 查询效率 | 两次hash定位 | 一次hash定位 |
| 内存开销 | Segment+HashEntry双层 | Node[]单层,更紧凑 |
| 红黑树 | 不支持 | 支持(链表>8树化) |
| 扩容 | Segment内阻塞 | 多线程协助迁移 |
| 读操作 | volatile读 | volatile读+ForwardingNode |
ArrayList = 电影院座位:连续编号,找第10排O(1)超快,但中间加座要整排往后挪O(n)。
LinkedList = 排队买票:不连续站位,找人要从头遍历O(n),但插队只需改前后人的手指方向O(1)。
LinkedList的add(i, e)并不是O(1)!需要先遍历到第i个位置O(n),然后才是O(1)的指针操作。所以中间插入/删除,LinkedList并不比ArrayList快。
实际开发中,ArrayList在绝大多数场景下优于LinkedList(CPU缓存行友好、内存连续)。
双亲委派 = 找领导签字:你收到一份文件,自己不能批→先找直属上级(App),上级不能批→找部门经理(Ext),经理不能批→找老板(Bootstrap)。老板批不了才一层层退回让你自己处理。
好处:核心类(如java.lang.Object)永远由Bootstrap加载,防止你自己写一个假的Object覆盖它。
| 知识点 | 生活类比 | 核心记忆 |
|---|---|---|
| HashMap | 🅿️ 停车场:车位=桶位,同车位排链表,多车升级VIP区(红黑树) | hash&n-1定位 / 链表>8且n≥64树化 / 扩容2倍 |
| ConcurrentHashMap | 🏢 多层停车场:JDK7每层一把锁,JDK8每个车位独立锁 | JDK8: 空桶CAS / 非空synchronized锁桶头 |
| ArrayList | 🎬 电影院座位:连续编号,找座快,加座要挪人 | O(1)随机访问 / 1.5倍扩容 / CPU缓存友好 |
| LinkedList | 🎫 排队买票:不连续,找人慢,插队快 | O(n)定位+O(1)操作 / 额外2指针内存 |
| 双亲委派 | 📝 找领导签字:自己批不了→找上级→找上上级 | 自底向上委派 / 自顶向下加载 / 保核心类安全 |
| 打破双亲委派 | 🔑 越级审批:特殊情况上级授权你直接办 | SPI上下文CL / Tomcat隔离 / OSGi网状 |
JDK8:数组 + 链表 + 红黑树。数组是主体(默认16),每个槽位可以挂链表(hash冲突时),链表长度超过8且数组长度≥64时转红黑树。红黑树查找O(log n)优于链表O(n)。
JDK7:并发resize + 头插法 → 链表成环 → 死循环。JDK8改尾插法解决了成环,但并发put仍有数据覆盖问题:两个线程同时判断tab[i]==null,都去写入,后者覆盖前者。
JDK7用Segment分段锁(默认16段,ReentrantLock),锁粒度粗,并发度固定。JDK8去掉Segment,改用CAS+synchronized锁桶头,并发度=数组长度,支持红黑树,扩容时多线程可协助迁移。
ArrayList基于数组,随机访问O(1),尾部增O(1)均摊,中间增删O(n)。LinkedList基于双向链表,随机访问O(n),头尾操作O(1),中间操作需要O(n)定位+O(1)指针操作。实际95%场景用ArrayList,CPU缓存行友好、内存紧凑。
类加载时先委派给父加载器,父加载器加载不了再自己加载。保证核心类(如java.lang.Object)只被Bootstrap CL加载一次,防止用户自定义同名类覆盖核心类,保证Java类型安全。
① SPI机制:Bootstrap CL加载接口,通过线程上下文CL(通常是App CL)加载classpath中的实现类。② Tomcat:每个WebApp用独立CL实现类隔离(不同应用可用不同版本库)。③ OSGi:网状依赖关系,按需委托,支持热部署。
ArrayList、HashMap等在知道大致容量时,初始化就指定大小。避免频繁扩容的Arrays.copyOf开销。如 new ArrayList<>(1000)、new HashMap<>(1024)。
synchronizedMap/List是对整个对象加锁,并发度极低。ConcurrentHashMap锁粒度细到桶级别,CopyOnWriteArrayList适合读多写少场景。选对并发集合比加锁重要得多。
如果key是自定义对象,必须同时重写hashCode()和equals()。hashCode决定桶位,equals决定是否覆盖。只重写equals不重写hashCode,两个"相等"的对象可能进不同桶,导致重复key。String和Integer已正确实现,可直接用。
集合与类加载三原则:① ArrayList 查快 LinkedList 增删快,HashMap 随机访问 O(1) → ② ConcurrentHashMap 分段锁/CAS 比 synchronized Map 快 → ③ 双亲委派保证核心类不被篡改,SPI 打破双亲委派