JavaSE 从零到生产级掌握
JavaSE 不能只学成“会写语法”。真正做后端项目时,你要能解释:对象为什么在堆上、引用为什么在线程栈里、HashMap 为什么要重写 equals/hashCode、线程池为什么不能乱配、ThreadLocal 为什么会泄漏、NIO 为什么能一个线程管多个连接、OOM 到底怎么查、JDK 7 和 JDK 8 常见差异在哪里。
一句话建立主线:
JavaSE 是 Java 后端所有框架的地基。Spring、MyBatis、Netty、RocketMQ、Dubbo、JVM 调优,本质上都离不开类型系统、面向对象、集合、泛型、反射、IO/NIO、并发、线程池和 JVM。
学习目标
学完这一页,你要能做到:
- 从零写出清晰的 Java 程序,理解类、对象、方法、构造器、包、访问控制。
- 解释基本类型、引用类型、String、数组、异常、泛型和集合为什么这样设计。
- 解释
ArrayList、HashMap、ConcurrentHashMap的核心结构和常见坑。 - 解释反射、注解、SPI 为什么是 Spring/MyBatis 等框架的基础。
- 解释 BIO、NIO、Buffer、Channel、Selector、零拷贝和字符编码。
- 解释 JMM、
volatile、synchronized、CAS、AQS、JUC、线程池、CompletableFuture。 - 解释 JVM 组成、内存结构、类加载、对象创建、GC、JIT、逃逸分析和 OOM 排查。
- 说清 JDK 7、JDK 8 的关键差异,尤其是 Lambda、Stream、日期时间、接口默认方法、HashMap 红黑树。
- 说清 JDK 8、11、17、21 的LTS演进、核心API用法、运行时变化和生产升级风险。
学习路线
flowchart TD
A["基础语法<br/>变量、流程控制、方法"] --> B["面向对象<br/>封装、继承、多态"]
B --> C["常用类型<br/>String、数组、异常、日期"]
C --> D["集合泛型<br/>List、Map、Set、泛型擦除"]
D --> E["反射注解SPI<br/>框架扩展基础"]
E --> F["IO/NIO<br/>文件、网络、Selector"]
F --> G["并发<br/>JMM、锁、CAS、AQS"]
G --> H["线程池<br/>参数、队列、拒绝、监控"]
H --> I["JVM<br/>内存、类加载、GC、OOM"]
I --> J["项目落地<br/>排查、性能、面试"]不要一上来就背 JVM 参数,也不要只会刷语法题。JavaSE 的学习重点是:每个语法点背后解决了什么问题,项目里不理解会造成什么故障。
如果你希望把基础语法、集合、泛型、反射、IO/NIO、线程池和 JVM 排查放进真实后端项目里串起来,可以配合学习:JavaSE 商业场景训练营。
如果你想检查自己是否真的从零基础学懂到能面试、能落地、能排查,按 JavaSE 从零到精通验收清单 逐项验收。它会要求你把每个知识点的“是什么、为什么、怎么工作、不这样会怎样、Demo、商业场景、排查和面试回答”都讲完整。
第一步:变量、类型和内存位置
Java 变量分两类:基本类型保存值,引用类型保存对象引用。
int age = 18;
User user = new User("Tom");简化理解:
flowchart TD
A["线程栈中的局部变量 age"] --> B["值 18"]
C["线程栈中的局部变量 user"] --> D["引用地址"]
D --> E["堆中的 User 对象"]为什么要区分:
| 类型 | 保存什么 | 常见问题 |
|---|---|---|
| 基本类型 | 值本身 | 自动装箱可能产生额外对象 |
| 引用类型 | 对象引用 | == 比较的是引用是否同一个对象 |
| 局部变量 | 在线程栈帧中 | 方法结束后局部变量消失 |
| 对象 | 通常在堆中 | 生命周期由 GC 管理 |
示例:
Integer a = 128;
Integer b = 128;
System.out.println(a == b); // false,可能不是同一个对象
System.out.println(a.equals(b)); // true,值相等项目里 ID、金额、状态比较不能乱用 ==。对象内容比较应使用 equals,金额计算应用 BigDecimal,不要用 double 处理钱。
第二步:面向对象到底解决什么
面向对象不是为了把代码写复杂,而是为了把数据和行为组织在一起,让系统能扩展。
public class Asset {
private final String assetNo;
private AssetStatus status;
public Asset(String assetNo) {
this.assetNo = assetNo;
this.status = AssetStatus.IDLE;
}
public void markUsed() {
if (this.status != AssetStatus.IDLE) {
throw new IllegalStateException("Only idle asset can be used");
}
this.status = AssetStatus.USED;
}
}封装的意义:
| 做法 | 结果 |
|---|---|
| 字段直接 public | 任意地方可改状态,状态机失控 |
| 方法封装状态流转 | 状态变更规则集中,容易维护 |
| 构造器保证必填 | 对象创建后天然合法 |
继承和多态适合“稳定抽象、多种实现”的场景:
public interface AssetExporter {
void export(List<Asset> assets);
}
public class ExcelAssetExporter implements AssetExporter {
public void export(List<Asset> assets) {
// export excel
}
}
public class CsvAssetExporter implements AssetExporter {
public void export(List<Asset> assets) {
// export csv
}
}不理解面向对象会怎样:业务规则散落在 Controller、Service、工具类里,后期改状态、改校验、加渠道会牵一堆代码。
第三步:String、数组和异常
String 为什么不可变
String 不可变意味着创建后内容不能被改。
String s = "abc";
s = s + "d";这里不是原字符串变成 abcd,而是创建新字符串,再让变量 s 指向新对象。
不可变的好处:
| 好处 | 说明 |
|---|---|
| 线程安全 | 多线程共享字符串不用担心内容被改 |
| 可缓存 hash | HashMap key 查询更稳定 |
| 字符串常量池 | 相同字面量可复用 |
| 安全性 | 文件路径、类名、URL 等不被中途篡改 |
拼接大量字符串应用 StringBuilder:
StringBuilder builder = new StringBuilder();
for (String part : parts) {
builder.append(part);
}
String result = builder.toString();异常怎么分层处理
异常不是“哪里都 catch 然后打印一下”。
flowchart TD
A["底层 IO/SQL 异常"] --> B["DAO 或 Gateway 捕获并转换"]
B --> C["Service 判断业务能否恢复"]
C --> D["Controller 统一异常响应"]
D --> E["日志、告警、错误码"]建议:
- 能恢复的异常,当前层处理。
- 不能恢复的异常,抛给统一异常处理。
- 不要吞异常。
- 日志要有业务 ID、请求 ID、关键参数。
finally负责释放资源,优先使用try-with-resources。
try (InputStream in = Files.newInputStream(path)) {
return in.readAllBytes();
}大文件不要 readAllBytes,要分块读,否则容易 OOM。
第四步:集合和泛型
集合是项目里最常用也最容易出错的 JavaSE 基础。
| 集合 | 底层特点 | 适合 | 不适合 |
|---|---|---|---|
| ArrayList | 数组 | 随机访问、尾部追加 | 频繁中间插入删除 |
| LinkedList | 双向链表 | 频繁首尾操作 | 随机访问 |
| HashMap | 数组 + 链表/红黑树 | key-value 查询 | 多线程并发写 |
| HashSet | 基于 HashMap | 去重 | 需要有序 |
| TreeMap | 红黑树 | 有序 key | 极致查询性能 |
| ConcurrentHashMap | CAS + 桶级锁 | 并发读写 | 复合业务原子性 |
HashMap 为什么依赖 hashCode 和 equals
flowchart TD
A["put(key,value)"] --> B["计算 hashCode"]
B --> C["定位桶 index"]
C --> D{"桶里是否已有节点"}
D -- "没有" --> E["直接放入"]
D -- "有" --> F["用 equals 比较 key"]
F --> G["相等覆盖,不等挂链表/树"]如果重写 equals 不重写 hashCode,两个业务上相等的对象可能落到不同桶里,导致去重失败。
public record AssetKey(String hospitalCode, String assetNo) {
}JDK 16 才有 record。JDK 8 项目中可手写不可变 key,并重写 equals/hashCode。
泛型的本质是编译期类型约束,运行期有类型擦除:
List<String> names = new ArrayList<>();
names.add("Tom");
String first = names.get(0);泛型避免大量强制类型转换,但不能在运行期直接判断 List<String> 和 List<Integer> 的泛型实参。
第五步:JDK 7 和 JDK 8 为什么重要
很多企业项目仍以 JDK 8 为核心,面试也常问 JDK 7/8 差异。
| 版本 | 重点能力 | 影响 |
|---|---|---|
| JDK 7 | try-with-resources、diamond 语法、switch 支持 String、NIO.2 | 资源释放和文件 API 更好用 |
| JDK 8 | Lambda、Stream、Optional、新日期时间、接口默认方法、CompletableFuture、HashMap 红黑树 | 函数式编程、集合处理、异步编排更常见 |
JDK 8 Lambda 示例:
List<Asset> usedAssets = assets.stream()
.filter(asset -> asset.getStatus() == AssetStatus.USED)
.sorted(Comparator.comparing(Asset::getUpdatedAt).reversed())
.toList(); // JDK 16+JDK 8 中没有 Stream.toList(),应使用:
List<Asset> usedAssets = assets.stream()
.filter(asset -> asset.getStatus() == AssetStatus.USED)
.sorted(Comparator.comparing(Asset::getUpdatedAt).reversed())
.collect(Collectors.toList());JDK 8 日期时间:
LocalDateTime now = LocalDateTime.now();
ZonedDateTime beijing = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));不要继续滥用 Date、Calendar 处理复杂时间逻辑。
第六步:反射、注解和 SPI
Spring、MyBatis、JUnit 都大量依赖反射和注解。
反射可以在运行期拿到类信息并调用方法:
Class<?> clazz = Class.forName("com.example.AssetService");
Object service = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getDeclaredMethod("syncAsset", Long.class);
method.invoke(service, 1001L);注解用于给代码加元数据:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AuditLog {
String action();
}框架流程:
flowchart TD
A["扫描 classpath"] --> B["读取类和方法"]
B --> C["检查注解"]
C --> D["反射创建对象或代理"]
D --> E["运行时增强逻辑"]为什么不能滥用反射:
- 编译期类型检查变弱。
- 性能和可读性不如直接调用。
- 私有成员反射会破坏封装。
- 模块化和安全限制下可能失败。
SPI 用于“接口由调用方定义,实现由第三方提供”:
ServiceLoader<AssetExporter> loader = ServiceLoader.load(AssetExporter.class);
for (AssetExporter exporter : loader) {
exporter.export(assets);
}JDBC 驱动加载、日志门面、部分框架扩展都能看到 SPI 思路。
第七步:IO 与 NIO
BIO 线程模型:
flowchart TD
A["客户端连接1"] --> B["线程1 read 阻塞"]
C["客户端连接2"] --> D["线程2 read 阻塞"]
E["客户端连接N"] --> F["线程N read 阻塞"]连接多时线程数暴涨,线程栈和上下文切换成本高。
NIO 模型:
flowchart TD
A["多个 Channel"] --> B["注册到 Selector"]
B --> C["一个线程 select 事件"]
C --> D["有 read/write 事件再处理"]三大组件:
| 组件 | 作用 |
|---|---|
| Buffer | 数据缓冲区,有 position、limit、capacity |
| Channel | 文件或网络通道 |
| Selector | 多路复用器,监听多个 Channel 事件 |
Buffer 常见方法:
| 方法 | 作用 |
|---|---|
flip() | 写模式切读模式 |
clear() | 清空指针,准备重新写 |
compact() | 保留未读数据,继续写 |
生产建议:普通文件处理用 Files、缓冲流和分块;大文件避免一次性读入内存;高并发网络通信用 Netty,不要手写完整 NIO 框架。
第八步:并发与 JMM
并发问题本质是三个词:可见性、原子性、有序性。
flowchart TD
A["线程A 修改变量"] --> B["线程工作内存"]
B --> C["主内存"]
C --> D["线程B 工作内存"]
D --> E["线程B 读取变量"]volatile 解决可见性和部分有序性,不解决复合操作原子性:
private volatile boolean running = true;
public void stop() {
running = false;
}count++ 不是原子操作:
count = count + 1;包含读、加、写三步,多线程下会丢更新。可以用 AtomicInteger:
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();CAS 流程:
flowchart TD
A["读取旧值 expect"] --> B["计算新值 update"]
B --> C{"内存值仍等于 expect"}
C -- "是" --> D["写入 update"]
C -- "否" --> E["重试或失败"]CAS 的问题:ABA、自旋消耗、只能保护单变量。复杂业务仍然需要锁、事务或状态机。
第九步:锁、AQS 和 JUC
synchronized 和 ReentrantLock 都能做互斥,但能力不同。
| 锁 | 特点 |
|---|---|
| synchronized | JVM 内置,自动释放,简单可靠 |
| ReentrantLock | 可中断、可超时、公平锁、多个 Condition |
| ReentrantReadWriteLock | 读多写少场景,读读并发 |
| Semaphore | 控制同时进入的许可数 |
| CountDownLatch | 等待多个任务完成 |
| CyclicBarrier | 多线程互相等待到齐 |
AQS 的核心:
flowchart TD
A["线程尝试获取资源"] --> B{"state 是否可获取"}
B -- "可以" --> C["CAS 修改 state 成功"]
B -- "不可以" --> D["进入 FIFO 等待队列"]
D --> E["阻塞等待唤醒"]
E --> F["被唤醒后再次竞争"]AQS 不是业务类,而是 JUC 锁和同步器的底层框架。理解它能帮你看懂 ReentrantLock、Semaphore、CountDownLatch 为什么都像“状态 + 队列”。
第十步:线程池生产级理解
不要无限 new Thread。线程池用来控制并发、复用线程、管理队列和拒绝策略。
执行流程:
flowchart TD
A["提交任务"] --> B{"核心线程数是否满"}
B -- "未满" --> C["创建核心线程执行"]
B -- "已满" --> D{"队列是否满"}
D -- "未满" --> E["任务入队等待"]
D -- "已满" --> F{"最大线程数是否满"}
F -- "未满" --> G["创建非核心线程执行"]
F -- "已满" --> H["执行拒绝策略"]核心参数:
| 参数 | 作用 | 配错后果 |
|---|---|---|
| corePoolSize | 常驻核心线程 | 太小吞吐低,太大浪费 |
| maximumPoolSize | 最大线程数 | 太大压垮下游,太小容易拒绝 |
| workQueue | 等待队列 | 无界队列可能 OOM |
| keepAliveTime | 非核心线程存活时间 | 回收过快或过慢 |
| RejectedExecutionHandler | 拒绝策略 | 静默丢任务或放大故障 |
示例:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
8,
16,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("asset-sync-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);生产监控要看:
- 活跃线程数。
- 队列长度。
- 任务执行耗时。
- 拒绝次数。
- 下游数据库连接池、HTTP 连接池是否被打满。
线程池不是越大越好。IO 任务多也要看下游承载能力,不能用线程池把数据库和第三方接口打垮。
第十一步:JVM 组成和 OOM 排查
JVM 运行时数据区:
flowchart TD
A["JVM Runtime Data Areas"] --> B["线程私有"]
A --> C["线程共享"]
B --> D["程序计数器"]
B --> E["Java 虚拟机栈"]
B --> F["本地方法栈"]
C --> G["堆 Heap"]
C --> H["方法区 / 元空间"]常见 OOM 类型:
| OOM | 常见原因 | 排查 |
|---|---|---|
| Java heap space | 大对象、集合无限增长、缓存不清理 | heap dump、MAT、对象直方图 |
| GC overhead limit | GC 很频繁但回收少 | GC 日志、存活对象分析 |
| Metaspace | 动态生成类太多、类加载器泄漏 | 类加载统计 |
| Direct buffer memory | NIO/Netty 堆外内存泄漏 | DirectMemory、ByteBuf 泄漏 |
| unable to create new native thread | 线程太多 | jstack、线程池、系统限制 |
排查流程:
flowchart TD
A["发生 OOM"] --> B["确认 OOM 类型"]
B --> C["保留日志和 dump"]
C --> D["查看 GC 日志"]
D --> E["分析对象占用和引用链"]
E --> F["定位代码路径"]
F --> G["修复并压测验证"]常用命令:
jps -l
jstat -gc <pid> 1000
jstack <pid> > thread.txt
jmap -histo:live <pid> | head
jmap -dump:format=b,file=heap.hprof <pid>线上不要乱执行重命令。heap dump 可能导致进程停顿和磁盘暴涨,要按应急流程操作。
第十二步:JIT 和逃逸分析
JIT 会把热点字节码编译成本地机器码,提高运行性能。
逃逸分析会判断对象是否逃出方法或线程。如果没有逃逸,JIT 可能做优化:
| 优化 | 解释 |
|---|---|
| 栈上分配 | 对象不真正分配到堆上 |
| 标量替换 | 对象拆成几个字段变量 |
| 锁消除 | 对不会被多线程访问的锁去掉 |
示例:
public int sum() {
Point p = new Point(1, 2);
return p.x + p.y;
}如果 p 不逃出方法,JIT 可能不在堆上创建完整 Point 对象,而是直接用两个标量值计算。
注意:这不是 Java 语义保证,而是 JVM 优化。写代码不要依赖“对象一定在栈上分配”,排查内存也要结合实际 JVM 参数和 JIT 行为。
第十三步:项目落地 Demo
医疗资产批量同步任务:
public class AssetSyncService {
private final ExecutorService executor;
private final AssetRepository repository;
private final RemoteAssetClient remoteClient;
public AssetSyncService(AssetRepository repository, RemoteAssetClient remoteClient) {
this.repository = repository;
this.remoteClient = remoteClient;
this.executor = new ThreadPoolExecutor(
8,
16,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(500),
r -> new Thread(r, "asset-sync-worker"),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
public void sync(List<Long> assetIds) {
for (Long assetId : assetIds) {
executor.submit(() -> syncOne(assetId));
}
}
private void syncOne(Long assetId) {
AssetRemoteDTO remote = remoteClient.getAsset(assetId);
Asset asset = Asset.from(remote);
repository.saveOrUpdate(asset);
}
}这个 Demo 背后涉及:
- 集合:
List<Long>批量任务。 - 面向对象:
Asset.from封装转换规则。 - 线程池:控制同步并发。
- IO:
remoteClient是网络 IO。 - 异常:单个资产失败不能吞异常,要记录和重试。
- JVM:批量太大或队列太大可能造成 OOM。
- 生产排查:要监控线程池队列、下游耗时、失败率。
线上排查总流程
flowchart TD
A["Java 线上问题"] --> B{"表现是什么"}
B -- "接口慢" --> C["线程栈、慢 SQL、慢 IO、锁等待"]
B -- "CPU 高" --> D["top 找线程、jstack 定位代码"]
B -- "内存高/OOM" --> E["GC 日志、heap dump、对象引用链"]
B -- "线程很多" --> F["线程池、无限建线程、阻塞 IO"]
B -- "任务堆积" --> G["线程池队列、下游连接池、拒绝策略"]
B -- "文件/网络异常" --> H["IO、编码、连接超时、句柄"]排查清单:
| 问题 | 优先看 |
|---|---|
| CPU 高 | 高 CPU 线程、死循环、频繁 GC、加解密/序列化 |
| 接口慢 | 线程栈、数据库、Redis、HTTP、锁 |
| OOM | OOM 类型、GC 日志、heap dump、直接内存 |
| 线程池满 | 活跃线程、队列、拒绝、下游耗时 |
| ThreadLocal 串数据 | 是否 finally remove |
| 大文件 OOM | 是否一次性读入、是否分批处理 |
常见坑
| 坑 | 后果 | 正确做法 |
|---|---|---|
对象比较乱用 == | 业务判断错误 | 对象内容用 equals |
| HashMap key 不重写 hashCode | 查不到或去重失败 | equals/hashCode 同时重写 |
大量字符串拼接用 + | 创建很多临时对象 | 循环用 StringBuilder |
| 捕获异常只打印 | 上层以为成功 | 记录上下文并抛出或补偿 |
| 无界线程池/队列 | OOM 或压垮下游 | 有界队列 + 监控 + 拒绝策略 |
| ThreadLocal 不 remove | 内存泄漏或串数据 | finally remove |
| 一次性读大文件 | 堆 OOM | 分块读取、流式处理 |
| 只调大 JVM 内存 | 掩盖泄漏 | 先分析对象和引用链 |
面试标准回答
JavaSE 怎么从零学到生产可用
JavaSE 要按基础语法、面向对象、常用类型、集合泛型、反射注解、IO/NIO、并发、线程池、JVM 和生产排查这条线学习。基础语法解决代码怎么写,面向对象解决业务规则怎么组织,集合和泛型解决批量数据和类型安全,反射注解是 Spring/MyBatis 等框架基础,IO/NIO 解决文件和网络,JUC 和线程池解决并发控制,JVM 解决内存、类加载、GC 和性能排查。真正生产可用不是会背 API,而是能解释原理、知道不这样做的风险,并能排查 OOM、CPU 高、线程池堆积、IO 阻塞等问题。JDK 7 和 JDK 8 常见区别
JDK 7 常见重点包括 try-with-resources、diamond 语法、switch 支持 String、NIO.2 文件 API。JDK 8 是企业项目最常见基础版本,重点包括 Lambda、Stream、Optional、新日期时间 API、接口默认方法、CompletableFuture,以及 HashMap 在链表过长时树化为红黑树。学习时不要只背版本特性,要知道它们解决了资源释放、集合处理、异步编排、日期时间和集合性能问题。OOM 怎么排查
OOM 先判断类型,是堆、元空间、直接内存、线程数还是 GC overhead。然后保留 GC 日志、错误日志和必要的 dump,用 jstat 看 GC 趋势,用 jmap 或 heap dump 分析对象占用和引用链,用 jstack 看线程是否堆积。堆 OOM 常见是集合无限增长、缓存不清理、大文件一次性读入;直接内存 OOM 常见于 NIO/Netty;线程 OOM 常见于无限创建线程或线程池配置错误。修复后要压测验证。关联知识点
| 知识点 | 说明 |
|---|---|
| JavaSE 从零到精通验收清单 | 检查每个 JavaSE 核心点是否真正学懂 |
| JavaSE 基础总览 | 语法、OOP、集合、反射、JDK 差异 |
| JDK 8/11/17/21 LTS演进 | 四个版本新增能力、代码实战、选型与升级 |
| 集合框架 | ArrayList、HashMap、equals/hashCode |
| 反射 | 框架运行时能力基础 |
| IO 与 NIO | BIO、NIO、Selector、零拷贝 |
| 多线程 | JMM、锁、ThreadLocal、JUC |
| 线程池 | 参数、执行流程、拒绝、监控 |
| 锁 | CAS、AQS、ReentrantLock |
| JVM 组成 | JVM 各模块和运行时区域 |
| JIT 逃逸分析 | JIT 优化和对象分配 |
| JavaSE 面试 | 标准回答和追问 |
本章小结
JavaSE 从零到生产级掌握,重点是把语法、对象、集合、反射、IO、并发、线程池、JVM 和排查连成一条线。你不是为了背 API,而是为了看懂框架为什么这样设计,项目为什么这样写,线上出了问题为什么会发生,以及应该从哪一层定位。
