"Java 阶段面试"是很多候选人的第一个关口,也是最容易自我感觉良好的一关。问题在于:知识点都学过,但面试官问的从来不是"你知不知道 HashMap 是红黑树",而是"为什么是红黑树不是平衡二叉树""线程池七个参数分别在什么场景下调"。这篇不给知识点清单,给的是一张地图和每条线的接话方式。
先把结论放在前面:Java 阶段的高频考点可以归成五条线——集合与泛型(HashMap 全家出题密度最高)、并发(线程池、锁、内存模型是分水岭)、JVM(内存区域、GC、调优是高级岗门槛)、异常与断言(体现工程素养)、语言新特性(8/17 之后是加分项)。 Android 岗的额外一条线是语言与框架的边界:哪些是 Java 规范、哪些是 Android 特有行为。答任何一条线,都要落到"机制 + 代价 + 场景"。
机制拆解
面试回答有个通用结构,比知识本身更重要:先给一句结论,再给机制,再给一个真实场景,最后给一个代价或边界。 举例——问"HashMap 为什么线程不安全",不要只答"并发下会数据错乱",而是:①结论:非线程安全——并发写会数据覆盖与丢失;"链表成环"要分版本看:JDK 7 及之前扩容用头插法,并发扩容会成环死循环;JDK 8 已改尾插法,成环这个坑被修掉了,但覆盖与丢失依然存在,面试官追问"JDK 8 呢"就考这一句版本限定;②机制:put 没有并发保护、resize 转移过程中其他线程可能看到半成品链表;③场景:多个线程用同一个 Map 做本地缓存,线上出现过请求数据错乱;④边界:并发场景请用 ConcurrentHashMap,或给外部加锁但要清楚锁的粒度。
五条线的重心各不相同。集合线考深度:HashMap 的桶结构、扩容策略、树化阈值(链表长度 8 且数组长度至少 64)、为什么容量总是 2 的幂(用位运算取模)。并发线考机制与边界:synchronized 的锁升级、volatile 的可见性语义但无原子性、CAS 的 ABA 与版本号、线程池的队列选择与拒绝策略、ThreadLocal 的内存泄漏。 JVM 线考定位能力:内存区域划分、类加载与双亲委派、GC 算法与收集器选择、调优时看什么指标。新特性线考关注度:Lambda 与函数式接口、Stream 的惰性、Optional 的边界、java.time 的时区语义。
这些坑的正确绕法
最常见的坑是只刷概念不做手写题,代码环节一露怯就被降级。 表现是概念题答得头头是道,一让现场手写"手写一个 LRU 缓存"或"用 Synchronized 实现一个简单的阻塞队列"就卡壳,随后被判定为"背题党"。根因是概念记忆替代了动手理解。 Android 岗的手写题高频清单可以提前准备:手写 LRU(LinkedHashMap + removeEldestEntry)、单例(安全的双检锁)、字符串反转/找最长无重复子串、生产者消费者的简化版、两数之和、二叉树层序遍历。
其次是答概念题时不带场景,答到第三层就断了。 面试官追问"那你怎么用""遇到过什么问题",答不上来就断了继续深入的通道。养成习惯:每个概念准备一个自己项目里的真实案例(一个 bug、一段优化、一次误判),哪怕规模不大,但因果链要完整。
还有一个更隐蔽的坑:Java 规范和 Android 行为混淆,混着答。 典型是把 JVM 的 finalize、偏向锁、CMS 收集器说成 Android 也如此,而 ART 的实际行为不同(早期版本用 Dalvik,后续 ART 的 GC 模型与 JVM 有差异,HeapDump 与 dumpsys meminfo 的工具链也不同)。 这类错误在 Android 面试里很扣分。对策是每答一个 JVM 机制,条件反射式补一句"Android 侧对应的是……",显示你知道两套体系的关系。
代码里见真章
看一段能直接跑的代码,把上面的机制落到具体写法上:
// 手写题高频:手写 LRU 缓存(面试现场要能一次写出来)
public class LruCache<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;
public LruCache(int maxSize) {
super(16, 0.75f, true); // accessOrder=true,按访问排序
this.maxSize = maxSize;
}
@Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxSize;
}
}
// 手写题高频:线程安全的懒加载单例(volatile 不可省)
public final class Config {
private static volatile Config instance;
private final Map<String, String> data = new HashMap<>();
private Config() {
}
public static Config get() {
Config local = instance;
if (local == null) synchronized (Config.class) {
local = instance;
if (local == null) local = instance = new Config();
}
return local;
}
// 读多写少,读方法不加锁
public String get(String k, String def) {
Config local = instance; // 局部读一次,减少 volatile 读
if (local == null) return def;
return local.data.getOrDefault(k, def);
}
}
// 概念自检:线程池的七个参数,生产环境怎么给值
ExecutorService pool = new ThreadPoolExecutor(
2, // core:常驻线程数,CPU 密集约等于核数
8, // max:上限,按峰值流量定
30, TimeUnit.SECONDS, // keepAlive:非核心空闲回收时间
new ArrayBlockingQueue<>(200), // 有界队列,避免 OOM;换成 SynchronousQueue 则直接交给 max
new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略:回压到提交方线程
这段代码值得盯三处:第一处,LRU 靠 LinkedHashMap 的 accessOrder=true 与 removeEldestEntry 实现,这是 Android 侧最常问的手写题;第二处,单例的 volatile 与局部变量写法是"能写出来"与"写得出理由"的分界;第三处,线程池的 ArrayBlockingQueue 容量与 CallerRunsPolicy 组合,体现的是"用回压换稳定性"的工程思路,而不是拍脑袋给值。
这题在面试里怎么问、怎么答
"Java 阶段如果只准备一个月,优先级怎么排?"给排序:并发(线程池 + 锁 + 内存模型)> 集合(HashMap 系)> JVM(内存区域 + GC)> 语言新特性 > 异常与工程素养。理由是并发与集合在 Android 岗位的实际使用频率最高,且追问深度最大。
"HashMap 为什么容量是 2 的幂?"答:用位运算 (n - 1) & hash 代替取模,既保证分布均匀又比 % 快;扩容后容量翻倍,同一 key 在扩容前后会落在同一个相对位置,这是"扩容后不需要重新计算 hash、只需要拆分原链表"的前提。
"线程池的队列怎么选?"对比三种:有界数组队列(内存可控、需要拒绝策略配合、适合 CPU 密集)、SynchronousQueue(不缓冲,新任务直接交给线程,池满就触发拒绝,适合 IO 密集)、LinkedBlockingQueue无界(危险,占满内存会 OOM)。 再补一条:ScheduledThreadPoolExecutor 适合定时任务,Android 里替代方案是 Handler 或 WorkManager。
"Android 岗 Java 阶段的追问会往哪走?"给三例:Handler/Binder 消息机制背后有没有同步与锁 → 回到并发;Activity 启动时主线程做了什么 → 回到 JVM 内存与类加载;内存优化时怎么定位大对象 → 回到 GC 与可达性。Java 基础是骨架,Android 场景是外衣,面试官往往从外衣往里剥。
"使用中遇到过什么问题?"案例一:线上 OOM,堆栈指向线程池任务里持有大集合引用;定位到无界队列 + 任务未清理上下文;修复为有界队列 + 任务结束后清空引用。案例二:HashMap 在并发下出现脏读;定位到多个线程共享一个非线程安全 Map;修复为 ConcurrentHashMap 并注明为什么选 computeIfAbsent 而不是 putIfAbsent。
再补一个工程上值得讲清的点:Java 概念在 Android 上要落到"工具与指标"上。 内存问题用 dumpsys meminfo 看 PSS/RSS 分项、用 Debug.getMemoryInfo 与 ActivityManager.MemoryInfo 对比系统余量、用 hprof dump 配合 MAT 找大对象;ANR 用 traces.txt 与 ANR trace 看主线程栈;卡顿用 Choreographer 与 FrameMetrics。 能把概念连到这些工具上,面试官会认为你是"真的在项目里用过",而不是"背过书"。
给正在准备面试的你
把 Java 阶段的五条线画成一张"路线图":横轴是准备时间轴(1 个月 / 3 个月 / 6 个月),纵轴是五条线(集合、并发、JVM、异常工程、新特性)。 在每条线上标出"必须掌握"与"加分项"两个层级,并各挂一个 Android 侧的落点(如 集合→SparseArray/ArrayList 选型、并发→Handler/线程池、JVM→dumpsys meminfo、新特性→Stream 兼容)。图下面写一句"每条线准备 1 个真实案例 + 1 道手写题",这是这张图的使用方法。
再补工程案例与踩坑——应用落点是把项目里共享的 HashMap 缓存改成 ConcurrentHashMap、把无界线程池队列改成有界并配 CallerRunsPolicy、为热点集合类补充手写实现(LRU/单例)作为团队练习。
复习时别孤立刷题:建造者模式——AlertDialog.Builder、Retrofit 的请求构造器都是标准 Builder 场景,参数多、有默认值、需要跨参数校验。
划两句重点:五条线(集合/并发/JVM/异常工程/新特性),每条都要"结论 + 机制 + 场景 + 边界"四段答;概念必须连到 Android 的实际工具与指标;手写题至少准备 5 道高频(LRU 缓存、单例、阻塞队列、两数之和、二叉树层序遍历),与正文那份清单保持一致。
下一篇聊 Kotlin 与 Java 的关系:同一 JVM 上的两门语言——沿着今天这条主线继续往前走。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:建造者模式:链式调用为何无处不用在哪些地方
下一篇预告:Kotlin-与-Java-的关系:同一-JVM-上的两门语言
有任何问题欢迎在评论区留言交流。