JavaSE 从零到精通验收清单
这页用来验收 JavaSE 是否真正学懂。不是“我知道有 List、Map、线程池、JVM”就算会,而是要能说清:它是什么,为什么需要,底层怎么工作,不这样会出什么问题,商业项目里怎么用,线上故障怎么排查,面试时怎么组织答案。
JavaSE 学习不能只基于 JDK 21。企业项目和面试里 JDK 7、JDK 8 最重要:JDK 7 是大量老项目的基础写法,JDK 8 是 Lambda、Stream、Optional、java.time、CompletableFuture、HashMap 红黑树、ConcurrentHashMap 实现变化的分水岭。JDK 11、17、21 要作为演进补充,但不能跳过 JDK 7/8。
最终目标
| 能力 | 达标标准 |
|---|---|
| 零基础能学 | 能从源码、编译、运行、变量、对象、方法开始讲清楚 |
| 原理能懂 | 能解释对象内存、集合结构、泛型擦除、反射调用、IO 模型、JMM、JVM |
| Demo 能写 | 每个核心点能写最小可运行代码验证 |
| 项目能用 | 能把 JavaSE 能力放到订单、库存、支付、采集、批处理、搜索同步等场景 |
| 故障能查 | 能排查 NPE、集合错乱、线程池堆积、死锁、CPU 高、OOM、IO 阻塞 |
| 面试能答 | 面试页回答标准清晰,原理能跳回知识点页展开 |
总路线
flowchart TD
A["源码、编译和运行"] --> B["变量、类型和运算"]
B --> C["面向对象和封装边界"]
C --> D["String、数组、异常、日期"]
D --> E["集合、泛型和迭代"]
E --> F["枚举、注解、反射、SPI"]
F --> G["IO、NIO、网络编程"]
G --> H["并发、JMM、锁和 JUC"]
H --> I["线程池和异步编排"]
I --> J["JVM、GC、JIT、OOM 排查"]
J --> K["商业场景和面试闭环"]阶段 1:源码、编译、运行和 classpath
是什么
Java 程序不是源码直接运行。.java 文件先由 javac 编译成 .class 字节码,JVM 再加载字节码,验证安全性,解释执行或 JIT 编译成机器码。
flowchart TD
A["Hello.java 源码"] --> B["javac 编译"]
B --> C["Hello.class 字节码"]
C --> D["类加载器加载"]
D --> E["字节码验证"]
E --> F["解释执行或 JIT 编译"]
F --> G["CPU 执行机器指令"]Demo
package com.example.basic;
public class Hello {
public static void main(String[] args) {
System.out.println("Hello Java");
}
}编译运行:
javac -d out src/com/example/basic/Hello.java
java -cp out com.example.basic.Hello为什么要懂
很多新手报错不是语法不会,而是不懂包名和 classpath:
| 错误 | 原因 |
|---|---|
ClassNotFoundException | 主动加载某个类时 classpath 找不到 |
NoClassDefFoundError | 编译时有类,运行时缺类,或类初始化失败 |
| 主类找不到 | -cp 指错了,或运行时没写全限定类名 |
详细知识点:基础语法。
阶段 2:变量、类型、值传递和金额精度
核心概念
Java 类型分为基本类型和引用类型。
| 类型 | 保存内容 | 例子 |
|---|---|---|
| 基本类型 | 值本身 | int、long、double、boolean |
| 引用类型 | 对象引用 | String、User、List<Order> |
flowchart TD
A["栈帧局部变量 int count"] --> B["保存值 10"]
C["栈帧局部变量 User user"] --> D["保存引用"]
D --> E["堆中的 User 对象"]Java 是值传递
Java 只有值传递。基本类型传值副本,引用类型传引用值副本。方法里可以通过引用副本修改对象字段,但不能让调用方变量改指向。
class User {
String name;
User(String name) {
this.name = name;
}
}
public class PassByValueDemo {
public static void main(String[] args) {
User user = new User("old");
changeName(user);
changeRef(user);
System.out.println(user.name); // new
}
static void changeName(User user) {
user.name = "new";
}
static void changeRef(User user) {
user = new User("other");
}
}金额为什么不能用 double
double 是二进制浮点,很多十进制小数无法精确表示。
System.out.println(0.1 + 0.2); // 0.30000000000000004商业项目里金额要用:
import java.math.BigDecimal;
import java.math.RoundingMode;
BigDecimal price = new BigDecimal("19.90");
BigDecimal count = new BigDecimal("3");
BigDecimal total = price.multiply(count).setScale(2, RoundingMode.HALF_UP);
System.out.println(total);不这样会怎样:支付、退款、账单、结算会出现分钱误差,轻则对账失败,重则资损。
详细知识点:数据类型与运算。
阶段 3:面向对象不是 getter 和 setter
是什么
面向对象的核心是用对象表达业务概念,用方法保护状态变化。封装、继承、多态不是口号:
| 特性 | 真正作用 |
|---|---|
| 封装 | 把状态变化收敛到有业务含义的方法中 |
| 继承 | 表达稳定的 is-a 关系,复用父类能力 |
| 多态 | 面向接口编程,运行时选择具体实现 |
反例:贫血且无保护的订单
class Order {
public String status;
}
Order order = new Order();
order.status = "PAID";
order.status = "CREATED"; // 已支付订单又被改回待支付正例:用方法保护状态
class Order {
private String status = "CREATED";
public void pay() {
if (!"CREATED".equals(status)) {
throw new IllegalStateException("Only created order can be paid");
}
this.status = "PAID";
}
public void cancel() {
if ("PAID".equals(status)) {
throw new IllegalStateException("Paid order cannot be canceled directly");
}
this.status = "CANCELED";
}
public String getStatus() {
return status;
}
}为什么要这样:订单、支付、库存、审批都有状态机。如果字段完全暴露,任何代码都能绕过业务规则,线上数据会出现“不可能状态”。
详细知识点:面向对象。
阶段 4:String、数组、异常和日期
String 为什么不可变
String 不可变是为了安全、复用、线程安全和 hash 稳定。它常被用作类名、路径、URL、Map key、缓存 key。如果内容能被修改,集合查找和安全边界都会被破坏。
String a = "abc";
String b = "abc";
System.out.println(a == b); // true,通常复用常量池对象
String c = new String("abc");
System.out.println(a == c); // false
System.out.println(a.equals(c)); // true数组和集合区别
数组长度固定,适合底层连续存储;集合封装了扩容、查找、迭代、泛型等能力。业务项目大多数场景用集合,底层算法、缓冲区、固定格式数据常用数组。
异常怎么设计
| 异常类型 | 适合场景 |
|---|---|
| 受检异常 | 调用方必须处理的外部条件,如文件不存在 |
| 运行时异常 | 编程错误或业务规则失败,如参数非法、状态错误 |
| 业务异常 | 可预期业务失败,如余额不足、库存不足 |
商业项目不要到处 catch Exception 然后吞掉。异常要么恢复,要么转换成业务错误码,要么记录上下文后继续抛出。
JDK 8 日期时间为什么重要
JDK 7 常用 Date、Calendar,API 可读性差且易变。JDK 8 引入 LocalDate、LocalDateTime、Instant、DateTimeFormatter,更适合表达日期、时间、时间戳和格式化。
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
LocalDateTime now = LocalDateTime.now();
String text = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println(text);阶段 5:集合、泛型和 HashMap
集合怎么选
| 需求 | 推荐结构 | 原因 |
|---|---|---|
| 按顺序保存列表 | ArrayList | 查询快,尾部追加快 |
| 频繁头部插入删除 | LinkedList 或队列 | 但业务中要谨慎,缓存局部性差 |
| 去重 | HashSet | 底层基于 HashMap |
| key-value 映射 | HashMap | 根据 hash 定位 |
| 保持插入顺序 | LinkedHashMap | 维护双向链表 |
| 排序 | TreeMap、TreeSet | 红黑树,有序 |
| 并发映射 | ConcurrentHashMap | 桶级并发控制 |
HashMap 为什么要求 equals 和 hashCode 一起重写
HashMap 先用 hash 定位桶,再用 equals 判断桶内 key 是否相同。
flowchart TD
A["put key"] --> B["计算 hash"]
B --> C["定位数组下标"]
C --> D["桶为空则放入"]
C --> E["桶不为空则比较 equals"]
E --> F["相同 key 覆盖 value"]
E --> G["不同 key 追加链表或红黑树"]如果 equals 相等但 hashCode 不同,两个对象可能落到不同桶里,HashMap 就查不到原来的数据。
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
class OrderKey {
private final String orderNo;
OrderKey(String orderNo) {
this.orderNo = orderNo;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof OrderKey)) {
return false;
}
OrderKey orderKey = (OrderKey) o;
return Objects.equals(orderNo, orderKey.orderNo);
}
@Override
public int hashCode() {
return Objects.hash(orderNo);
}
}
public class HashMapDemo {
public static void main(String[] args) {
Map<OrderKey, String> map = new HashMap<>();
map.put(new OrderKey("O1001"), "PAID");
System.out.println(map.get(new OrderKey("O1001")));
}
}JDK 7 和 JDK 8 HashMap 区别
| 点 | JDK 7 | JDK 8 |
|---|---|---|
| 冲突结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
| 插入方式 | 头插法 | 尾插法 |
| 高冲突性能 | 链表退化为 O(n) | 树化后接近 O(log n) |
| 扩容迁移 | 重新计算位置 | 利用 hash & oldCap 拆分 |
不要只背“JDK 8 有红黑树”。要知道为什么:当 hash 冲突严重时,链表查找会退化。红黑树是为了避免极端冲突下查找太慢。
详细知识点:集合框架、HashMap全过程、ArrayList全过程。
阶段 6:泛型、枚举、注解、反射和 SPI
泛型解决什么问题
泛型让集合和方法在编译期获得类型检查,减少强制转换和运行时 ClassCastException。
import java.util.ArrayList;
import java.util.List;
List<String> names = new ArrayList<>();
names.add("Tom");
String first = names.get(0);泛型擦除表示运行时大多数泛型信息会被擦除成原始类型或边界类型。它保证了老版本兼容,但也导致不能直接 new T(),不能创建泛型数组,运行时无法区分 List<String> 和 List<Integer> 的普通实例类型。
反射为什么是框架基础
Spring、MyBatis、JUnit 等框架都要在不知道具体业务类的情况下创建对象、读取注解、调用方法、设置字段。
import java.lang.reflect.Method;
class PayService {
public String pay(String orderNo) {
return "paid:" + orderNo;
}
}
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("PayService");
Object target = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getMethod("pay", String.class);
Object result = method.invoke(target, "O1001");
System.out.println(result);
}
}注解怎么和反射配合
注解本身只是元数据,必须被编译器、框架或运行时反射读取后才有行为。
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
@Retention(RetentionPolicy.RUNTIME)
@interface Audit {
String value();
}
class OrderService {
@Audit("pay-order")
public void pay() {
}
}
public class AnnotationDemo {
public static void main(String[] args) throws Exception {
Method method = OrderService.class.getMethod("pay");
Audit audit = method.getAnnotation(Audit.class);
System.out.println(audit.value());
}
}SPI 是什么
SPI 是服务发现机制,调用方依赖接口,具体实现放在配置文件里。JDBC 驱动、日志框架、Dubbo 扩展点都大量使用类似思想。
详细知识点:泛型全过程、反射全过程、注解全过程、SPI全过程。
阶段 7:IO、NIO、网络和大文件
BIO 和 NIO 的核心区别
| 模型 | 特点 | 适合场景 |
|---|---|---|
| BIO | 一个连接通常对应一个线程,读写阻塞 | 简单低并发、文件读写 |
| NIO | Channel + Buffer + Selector,一个线程管理多个连接 | 高并发网络服务 |
flowchart TD
A["客户端连接"] --> B["Selector 监听事件"]
B --> C["Accept 事件"]
B --> D["Read 事件"]
B --> E["Write 事件"]
D --> F["Channel 读取到 Buffer"]
F --> G["解码并交给业务线程池"]文件分批读取 Demo
大文件不能一次性 readAllBytes 放进内存。应该分块读取,分批处理。
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
public class FileBatchDemo {
public static void main(String[] args) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(Paths.get("orders.csv"), StandardCharsets.UTF_8)) {
String line;
int batchSize = 0;
while ((line = reader.readLine()) != null) {
// parse and save line
batchSize++;
if (batchSize >= 1000) {
// flush batch to database
batchSize = 0;
}
}
}
}
}不这样会怎样:大文件导入时把所有行放入 List,很容易堆 OOM;如果在 Web 请求线程里做慢文件 IO,接口线程会被长期占用。
详细知识点:IO与NIO总览、IO与NIO全过程、零拷贝与大文件。
阶段 8:并发、JMM、锁和 JUC
为什么并发难
并发问题不是“多个线程一起跑”这么简单,而是同时涉及可见性、原子性、有序性。
| 问题 | 含义 | 例子 |
|---|---|---|
| 可见性 | 一个线程修改,另一个线程何时看见 | 停止开关不生效 |
| 原子性 | 一组操作不可被打断 | count++ 丢失更新 |
| 有序性 | 编译器和 CPU 可能重排序 | 双重检查锁要配合 volatile |
volatile 能做什么
volatile 保证可见性和一定的有序性,不保证复合操作原子性。
public class StopFlagDemo {
private static volatile boolean running = true;
public static void main(String[] args) throws Exception {
Thread worker = new Thread(() -> {
while (running) {
// do work
}
System.out.println("stopped");
});
worker.start();
Thread.sleep(1000);
running = false;
}
}count++ 不是一个原子操作,不能只靠 volatile:
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger count = new AtomicInteger();
count.incrementAndGet();synchronized 和 ReentrantLock
| 点 | synchronized | ReentrantLock |
|---|---|---|
| 使用方式 | 关键字 | 类库 API |
| 释放锁 | 自动释放 | 必须 finally unlock |
| 可中断 | 不支持等待中断 | 支持 lockInterruptibly |
| 公平锁 | 不支持显式公平 | 可选公平 |
| 条件队列 | 一个 wait set | 多个 Condition |
详细知识点:JMM、synchronized全过程、volatile与Atomic全过程、AQS与JUC全过程。
阶段 9:线程池和 CompletableFuture
为什么不能无限 new Thread
线程不是越多越好。每个线程都有栈内存、调度成本和上下文切换成本。无限创建线程会导致 CPU 切换、内存上涨、连接池打满、下游被压垮。
线程池执行流程
flowchart TD
A["提交任务"] --> B{"工作线程数 < corePoolSize"}
B -->|是| C["创建核心线程执行"]
B -->|否| D{"队列是否未满"}
D -->|是| E["放入工作队列"]
D -->|否| F{"工作线程数 < maximumPoolSize"}
F -->|是| G["创建非核心线程执行"]
F -->|否| H["执行拒绝策略"]线程池 Demo
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4,
8,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(200),
new ThreadPoolExecutor.CallerRunsPolicy()
);
for (int i = 0; i < 100; i++) {
int taskId = i;
executor.execute(() -> System.out.println("handle task " + taskId));
}
executor.shutdown();
}
}商业项目里线程池参数不能只看 CPU 核数,还要看数据库连接池、HTTP 连接池、Redis、MQ、下游限流能力。消费者扩容短暂有效后又堆积,常见原因就是下游瓶颈没有变,更多消费者只是在更快地把压力打到下游。
CompletableFuture 注意点
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
ExecutorService pool = Executors.newFixedThreadPool(8);
CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> "user", pool);
CompletableFuture<String> orderFuture = CompletableFuture.supplyAsync(() -> "orders", pool);
String result = userFuture.thenCombine(orderFuture, (user, orders) -> user + ":" + orders).join();
System.out.println(result);生产必须指定线程池、设置超时、处理异常、监控队列。不指定线程池可能占用公共 ForkJoinPool;没有超时会导致接口一直等;没有异常兜底会导致聚合接口整体失败。
详细知识点:线程池总览、线程池执行流程、参数估算与监控、CompletableFuture全过程。
阶段 10:JVM、GC、JIT 和 OOM
JVM 由哪些部分组成
flowchart TD
A["JVM"] --> B["类加载子系统"]
A --> C["运行时数据区"]
A --> D["执行引擎"]
A --> E["垃圾回收器"]
A --> F["JNI 和本地方法库"]
C --> G["堆"]
C --> H["虚拟机栈"]
C --> I["方法区或元空间"]
C --> J["程序计数器"]JDK 7 和 JDK 8 方法区差异
| 版本 | 常见实现 | 常见 OOM |
|---|---|---|
| JDK 7 | 永久代 PermGen | PermGen space |
| JDK 8 | 元空间 Metaspace,本地内存 | Metaspace |
JDK 8 移除永久代不代表不会元空间 OOM。动态代理、CGLIB、脚本引擎、热部署、类加载器泄漏都会撑爆 Metaspace。
TLAB 和逃逸分析
TLAB 是 Eden 区里给线程预留的一小段线程本地分配缓冲区,仍然属于堆。它减少多线程对象分配时对 Eden 指针的竞争。
逃逸分析是 JIT 优化:如果对象没有逃出方法或线程,JIT 可能做标量替换、锁消除、分配消除,让对象不以完整堆对象形式存在。它不是 Java 语法层面的“手动栈分配”,也不是所有对象都能优化。
OOM 排查流程
flowchart TD
A["出现 OOM 或 OOMKilled"] --> B["确认错误类型"]
B --> C["保留日志和 dump"]
C --> D["看 GC 趋势"]
D --> E["分析 heap dump 或类加载信息"]
E --> F["定位最大对象或类加载器"]
F --> G["结合代码和流量找根因"]
G --> H["修复泄漏、限流或分批处理"]| 类型 | 常见原因 | 排查重点 |
|---|---|---|
Java heap space | 集合无限增长、缓存无淘汰、大文件一次性读入 | heap dump、Dominator Tree、GC Roots |
Metaspace | 动态类太多、ClassLoader 泄漏 | classloader stats、动态代理、热部署 |
Direct buffer memory | NIO/Netty 堆外内存泄漏 | direct memory、ByteBuf release |
| unable to create native thread | 无限建线程、线程栈过大 | 线程数、线程池、-Xss |
| OOMKilled | 容器总内存超限 | Pod 事件、退出码 137、JVM 总预算 |
阶段 11:商业场景验收
场景 1:订单导入
要求:
- 使用
BufferedReader分批读取 CSV。 - 用
BigDecimal处理金额。 - 用
HashMap按订单号去重。 - 分批入库,不能一次性把所有数据放入内存。
- 异常要记录行号、订单号、原因。
验收问题:
- 为什么大文件不能
readAllBytes? - 为什么订单号适合做 Map key?
- 为什么金额不用 double?
- 如果导入过程中 OOM 怎么查?
场景 2:库存扣减并发控制
要求:
- JVM 内单机并发可以用锁或原子类保护内存状态。
- 数据库库存必须依赖数据库事务和乐观锁。
- 不能把
volatile int stock当作库存扣减方案。
验收问题:
volatile为什么不能保证stock--原子性?synchronized和ReentrantLock怎么选?- 单机锁为什么不能解决多实例并发?
场景 3:接口聚合
要求:
- 用
CompletableFuture并行查用户、订单、优惠券。 - 使用自定义线程池。
- 每个下游有超时和异常兜底。
- 监控线程池队列、活跃线程、拒绝次数。
验收问题:
- 为什么不能直接
parallelStream调 HTTP? - 为什么
orTimeout不一定停止底层 HTTP 调用? - 下游慢导致线程池堆积怎么查?
阶段 12:面试闭环
面试页只放标准回答,深入原理回到知识点页。
flowchart TD
A["面试问题"] --> B["标准回答"]
B --> C["知识点页理解原理"]
C --> D["Demo 验证"]
D --> E["商业场景复述"]
E --> F["线上排查补充"]| 面试题 | 必须答到的深度 |
|---|---|
| JDK 7 和 JDK 8 区别 | 语法、集合、并发、日期、JVM 方法区差异 |
| JDK 8、11、17、21演进 | 首次引入版本、正式与Preview边界、代表性API、运行时变化 |
| JDK 8升级11/17/21 | 构建链、移除模块、强封装、Agent、GC参数、TLS、灰度回滚 |
| JDK 21虚拟线程 | 适合IO、不加速CPU、每任务一线程、下游限流、pinning、ThreadLocal |
| HashMap 原理 | hash、数组、链表、红黑树、扩容、equals/hashCode |
| ArrayList 原理 | 数组、扩容、fail-fast、subList 坑 |
| 泛型原理 | 编译期检查、类型擦除、边界、通配符 |
| 反射原理 | Class、Constructor、Method、Field、注解读取、框架用途 |
| BIO 和 NIO | 阻塞模型、Buffer、Channel、Selector、适用场景 |
| volatile 和 synchronized | 可见性、原子性、有序性、锁、内存屏障 |
| 线程池参数 | core、max、queue、keepAlive、reject、执行流程 |
| CompletableFuture | 编排、线程池、超时、异常、监控 |
| JVM 组成 | 类加载、运行时数据区、执行引擎、GC、JNI |
| OOM 排查 | 堆、元空间、直接内存、线程、容器 OOMKilled |
面试页:JavaSE 面试知识点。
最终验收题
- Java 程序从源码到运行经历哪些步骤?
- classpath 指向包目录还是类文件目录?为什么?
- Java 为什么是值传递?引用类型参数为什么能改对象字段?
- 金额为什么不能用 double?
BigDecimal有什么坑? - 封装为什么不是给字段生成 getter/setter?
String为什么不可变?new String("abc")怎么理解?HashMap为什么容量常用 2 的幂?- JDK 7 和 JDK 8 的 HashMap 有什么区别?
- 为什么
equals和hashCode必须一起重写? - 泛型擦除带来哪些限制?
- 注解为什么必须配合解析器才有行为?
- 反射为什么能支撑 Spring 和 MyBatis?
- BIO 和 NIO 的线程模型有什么不同?
- 大文件导入为什么会 OOM?怎么改?
volatile能不能保证count++安全?为什么?- CAS 有哪些问题?ABA 怎么解决?
- AQS 为什么是 JUC 的基础?
- 线程池提交任务后的完整执行流程是什么?
execute和submit异常表现有什么不同?- CompletableFuture 为什么必须指定线程池?
- JDK 7 永久代和 JDK 8 元空间有什么区别?
- TLAB 属于堆吗?和逃逸分析有什么区别?
- JIT 逃逸分析可能做哪些优化?
- Heap OOM、Metaspace OOM、Direct Memory OOM 怎么区分?
- 容器 OOMKilled 和 Java OOM 有什么区别?
- JDK 8、11、17、21各自最重要的正式特性是什么?
List.of、var、Record、sealed、虚拟线程分别在哪个版本正式引入?- JDK 8升级到11为什么可能出现JAXB类缺失和旧GC参数失效?
- JDK 11升级17为什么可能出现
InaccessibleObjectException? - JDK 21虚拟线程为什么仍要限制数据库和下游并发?如何排查pinning?
版本专题和完整代码见:JDK 8、11、17、21 LTS版本演进。
本章小结
JavaSE 是后端工程的地基。Spring 的 IOC 依赖反射和注解,MyBatis 依赖反射和动态代理,Netty 依赖 NIO 和堆外内存,MQ 消费依赖线程池和并发控制,线上排查依赖 JVM 和操作系统知识。真正学会 JavaSE,不是背 API,而是把每个基础点和项目问题、框架原理、线上故障连起来。
