Android第四次面试总结之Java基础篇(补充)
一、设计原则高频面试题(附大厂真题解析)
1. 单一职责原则(SRP)在 Android 开发中的应用(字节跳动真题)
- 真题:“你在项目中如何体现单一职责原则?举例说明。”
- 考点:结合实际场景说明职责拆分,避免贫血模型。
- 满分答案:
在开发网络模块时,原代码将 “网络请求逻辑”“数据解析”“错误处理” 耦合在一个NetworkManager
类中,违反 SRP。
重构方案:- 拆分为
OkHttpHelper
(负责底层网络请求)、DataParser
(解析 JSON/Proto 数据)、ErrorHandler
(统一处理网络异常); - 高层模块(如
UserRepository
)依赖这三个抽象组件,通过构造器注入协作。
优势:每个类仅一个修改原因(如网络库升级只需改OkHttpHelper
),降低维护成本。
- 拆分为
2. 里氏替换原则(LSP)的经典反例及修正(腾讯真题)
- 真题:“为什么 Square 不能直接继承 Rectangle?如何正确设计?”
- 考点:理解继承的约束条件,区分 “Is-a” 与 “Can-do”。
- 满分答案:
反例分析:Rectangle
定义setWidth(int w)
和setHeight(int h)
,允许宽高独立变化;Square
重写这两个方法时,强制宽高相等,破坏父类 “宽高可独立设置” 的契约,违反 LSP(子类不能替换父类)。
修正方案:
- 放弃继承,让
Square
和Rectangle
实现共同接口Quadrilateral
,提供getWidth()
和getHeight()
,但不强制宽高可独立设置; - 或引入
MutableRectangle
接口,明确 “可修改宽高” 的能力,Square
不实现该接口。
3. 依赖倒置原则(DIP)在 MVP 中的应用(阿里真题)
- 真题:“MVP 架构如何体现依赖倒置原则?”
- 考点:区分高层模块与低层模块,抽象接口解耦。
- 满分答案:
MVP 分层:- 高层模块(Presenter):依赖抽象接口
View
(如UserView
)和Repository
(如UserRepository
),而非具体实现(Activity
或RetrofitImpl
); - 低层模块(Model/View 实现):实现这些抽象接口,如
UserActivity implements UserView
,UserRetrofit implements UserRepository
。
依赖关系:
Presenter 与具体 Activity / 网络库解耦,可通过依赖注入(如 Dagger)切换实现(如单元测试时用 MockView),符合 “高层模块依赖抽象” 的 DIP 原则。
- 高层模块(Presenter):依赖抽象接口
二、DCL 单例模式大厂真题解析
1. 为什么 DCL 单例需要 volatile?(美团真题)
- 考点:理解指令重排对单例的影响,volatile 的内存语义。
- 满分答案:
指令重排风险:
instance = new DCLSingleton()
可分解为:- 分配内存空间(
memory = allocate()
); - 初始化对象(
ctorInstance(memory)
); - 将内存地址赋给
instance
(instance = memory
)。
JVM 可能重排为 1→3→2,若线程 A 执行到 3 时(instance
非空但未初始化),线程 B 调用getInstance()
返回未初始化的对象,导致 NPE。
volatile 作用:
- 禁止指令重排,确保 1→2→3 的顺序;
- 保证可见性,线程 A 修改
instance
后,线程 B 立即看到最新值。
JDK 版本关键:JDK 1.5 + 修复了 volatile 的语义,此前版本 DCL 可能失效,因此现代 Java 必须使用 volatile。
- 分配内存空间(
2. 单例模式的线程安全实现有哪些?对比优缺点(百度真题)
- 考点:掌握不同单例实现的适用场景,反序列化安全。
- 满分答案:
实现方式 | 线程安全 | 优点 | 缺点 | 大厂应用场景 |
---|---|---|---|---|
DCL | 是 | 延迟初始化,性能高 | 需 volatile,实现较复杂 | 高并发且内存敏感场景 |
静态内部类 | 是 | 简洁,利用类加载机制安全 | 类加载后立即初始化 | 通用场景(推荐) |
枚举单例 | 是 | 反序列化安全,防止反射攻击 | 不支持延迟初始化 | 需严格防止实例化场景 |
- 反序列化安全:
枚举单例天然支持(Java 规范保证反序列化返回枚举常量),其他方式需重写readResolve()
返回单例实例:protected Object readResolve() { return instance; }
三、HashMap 高频面试题(附大厂真题解析)
1. JDK8 HashMap 为什么引入红黑树?链表转红黑树的条件?(字节跳动真题)
-
考点:理解哈希冲突优化,阈值设计原理。
-
满分答案:
引入红黑树原因:
JDK7 及以前用链表处理哈希冲突,当链表长度为 n 时,查找时间复杂度 O (n)。数据倾斜时(如大量键哈希值相同),链表可能很长,性能下降。
红黑树将查找、插入、删除的时间复杂度降至 O (logn),提升极端场景下的性能。转换条件(两个同时满足):
- 链表长度≥8(
TREEIFY_THRESHOLD=8
); - 数组容量≥64(
MIN_TREEIFY_CAPACITY=64
)。
原因:
- 链表长度 8 的概率极低(泊松分布计算,概率仅 0.0000006),若出现则认为是哈希冲突严重;
- 若数组容量小(如 16),直接扩容比转红黑树更高效(减少树节点维护开销)。
- 链表长度≥8(
2. 自定义类作为 HashMap 的 Key 需要注意什么?(阿里真题)
- 考点:正确重写 hashCode 和 equals,不可变性。
- 满分答案:
- 必须重写
hashCode()
和equals()
:- 若只重写
equals
,不同对象可能哈希值相同,导致存入 HashMap 后无法正确查找; - 示例:
class Person { String id; @Override public boolean equals(Object o) { ... } // 必须同时重写hashCode,保证相等对象哈希值相同 @Override public int hashCode() { return Objects.hash(id); } }
- 若只重写
- Key 建议为不可变类:
- 若 Key 可变,修改后哈希值变化,导致存入的键值对无法通过新值查找(如
String
是不可变类,推荐作为 Key); - 若必须用可变类,修改前先从 HashMap 中删除旧 Key。
- 若 Key 可变,修改后哈希值变化,导致存入的键值对无法通过新值查找(如
- 必须重写
四、ConcurrentHashMap 大厂真题解析
1. JDK7 与 JDK8 的 ConcurrentHashMap 实现有何区别?(腾讯真题)
- 考点:分段锁 vs 细粒度锁,数据结构演进。
- 满分答案:
特性 | JDK7(分段锁) | JDK8(CAS + 细粒度锁) |
---|---|---|
数据结构 | Segment 数组(每个 Segment 是小 HashMap) | 数组 + 链表 + 红黑树(同 HashMap 结构) |
锁机制 | 对 Segment 加 ReentrantLock(锁粒度大) | 对链表头节点或红黑树根节点加 synchronized(锁粒度小) |
插入逻辑 | 锁 Segment 后遍历链表 | 先 CAS 无锁插入,失败后加锁 |
并发度 | 受限于 Segment 数量(默认 16) | 理论并发度更高(锁竞争更小) |
内存效率 | 每个 Segment 有独立数组,内存占用略高 | 共享数组,内存更紧凑 |
- 典型场景:
JDK8 在高并发写入场景(如秒杀系统的计数器)性能提升显著,因锁粒度从 “段” 细化到 “节点”,减少线程竞争。
2. ConcurrentHashMap 为什么不允许 Key 和 Value 为 null?(美团真题)
- 考点:线程安全与 null 值的歧义性。
- 满分答案:
历史原因:- HashMap 允许 null Key(唯一)和 null Value,ConcurrentHashMap 为避免与 Hashtable(不允许 null)行为不一致,选择不允许 null;
- 更重要的是,null 值在多线程场景下存在歧义:
- 当
get(key)
返回 null 时,无法区分 “Key 不存在” 和 “Value 为 null”; - 若允许 null Value,多线程插入时可能出现 “Key 存在但 Value 为 null” 的中间状态,导致后续读取误判。
对比 HashMap:
HashMap 单线程下可明确处理 null(Key 只能有一个 null,Value 可为多个 null),但 ConcurrentHashMap 作为线程安全类,需避免这种歧义性,保证语义清晰。
- 当
五、面试真题陷阱与避坑指南
1. 设计原则陷阱题:“所有类都应该遵守单一职责原则吗?”(字节跳动)
- 陷阱:考察对原则的灵活应用,而非教条主义。
- 正确回答:
不是。单一职责原则的 “职责” 是 “变化的原因”,若多个职责不会同时变化(如 “用户校验” 和 “日志记录” 在项目中始终一起修改),可暂时合并以减少类数量。原则需结合项目规模和变化频率权衡,避免过度设计。
2. HashMap 扩容陷阱:“初始容量设为 10,实际数组长度是多少?”(阿里)
- 陷阱:HashMap 会将容量自动调整为≥给定值的最小 2 的幂(10→16)。
- 正确回答:
实际长度为 16。HashMap 的构造函数会调用tableSizeFor(int cap)
方法,将容量向上取整为 2 的幂,确保(n-1)&hash
的计算正确性。
面试扩展:
1. 设计原则综合题:“MVC 架构是否符合开闭原则?为什么?”
- 考点:架构与设计原则的结合,扩展性分析。
- 预测答案:
部分符合:- View 层(如 Activity)常因 UI 变化直接修改,违反 “对修改关闭”;
- Model 层(数据模型)和 Controller 层(逻辑处理)可通过抽象接口扩展(如新增数据源时实现新 Model 接口),符合 “对扩展开放”。
改进建议:
引入接口隔离,让 View 依赖抽象(如 MVP 中的 View 接口),减少对具体实现的修改,更贴近开闭原则。
2. HashMap 深度陷阱题:“键的 hashCode () 返回 0,会发生什么?如何优化?”
- 考点:极端哈希冲突处理,红黑树阈值。
- 预测答案:
- 所有键存入数组的 0 号位置,形成长链表(或红黑树);
- 若数组容量≥64 且链表长度≥8,转为红黑树,查询时间复杂度 O (logn);
- 优化:重写 hashCode (),让键的哈希值更分散(如结合多个字段计算哈希)。
六、总结
知识点 | 高频问题示例 | 核心考点 | 满分答案关键要素 |
---|---|---|---|
单一职责原则 | 如何拆分 Android 中的网络模块? | 职责定义(变化原因)、实际案例 | 拆分前后对比,说明每个类的独立变化原因 |
DCL 单例 | 为什么需要两次检查和 volatile? | 线程安全、指令重排、可见性 | 结合源码解释两次检查的作用,volatile 的必要性 |
HashMap 红黑树 | 链表转红黑树的条件是什么? | 阈值设计、概率分析、性能权衡 | 同时满足长度≥8 和容量≥64,避免小树维护开销 |
ConcurrentHashMap | 与 Hashtable 的区别? | 锁机制、null 支持、并发度 | 细粒度锁 vs 全表锁,弱一致性设计 |
设计原则与集合类核心考点
├─ 设计原则(6大原则)
│ ├─ SRP:职责=变化原因,拆分类/模块
│ ├─ OCP:通过抽象扩展,避免修改原有代码
│ ├─ LSP:子类可替换父类,不破坏契约
│ ├─ ISP:接口细化,客户端不依赖无用方法
│ ├─ DIP:高层模块依赖抽象,而非具体实现
│ └─ LoD:仅与直接朋友交互,减少耦合
├─ DCL单例
│ ├─ 双重检查+volatile:防指令重排,线程安全
│ ├─ 防御反射/反序列化:构造函数检查+readResolve
│ └─ 最佳实践:枚举单例(最简、最安全)
├─ HashMap
│ ├─ 底层:数组+链表(≥8转红黑树,容量≥64)
│ ├─ 哈希计算:高位异或,减少冲突
│ ├─ 扩容:容量翻倍,重新哈希(初始容量设为2的幂)
│ └─ Key要求:重写hashCode/equals,不可变类最佳
└─ ConcurrentHashMap ├─ 线程安全:JDK8细粒度synchronized+CAS ├─ 与HashMap区别:不允许null,弱一致性 └─ 适用场景:高并发读写,替代Hashtable/同步HashMap