Skip to content

JavaSE 从零到生产级掌握

JavaSE 不能只学成“会写语法”。真正做后端项目时,你要能解释:对象为什么在堆上、引用为什么在线程栈里、HashMap 为什么要重写 equals/hashCode、线程池为什么不能乱配、ThreadLocal 为什么会泄漏、NIO 为什么能一个线程管多个连接、OOM 到底怎么查、JDK 7 和 JDK 8 常见差异在哪里。

一句话建立主线:

JavaSE 是 Java 后端所有框架的地基。Spring、MyBatis、Netty、RocketMQ、Dubbo、JVM 调优,本质上都离不开类型系统、面向对象、集合、泛型、反射、IO/NIO、并发、线程池和 JVM。

学习目标

学完这一页,你要能做到:

  1. 从零写出清晰的 Java 程序,理解类、对象、方法、构造器、包、访问控制。
  2. 解释基本类型、引用类型、String、数组、异常、泛型和集合为什么这样设计。
  3. 解释 ArrayListHashMapConcurrentHashMap 的核心结构和常见坑。
  4. 解释反射、注解、SPI 为什么是 Spring/MyBatis 等框架的基础。
  5. 解释 BIO、NIO、Buffer、Channel、Selector、零拷贝和字符编码。
  6. 解释 JMM、volatilesynchronized、CAS、AQS、JUC、线程池、CompletableFuture
  7. 解释 JVM 组成、内存结构、类加载、对象创建、GC、JIT、逃逸分析和 OOM 排查。
  8. 说清 JDK 7、JDK 8 的关键差异,尤其是 Lambda、Stream、日期时间、接口默认方法、HashMap 红黑树。
  9. 说清 JDK 8、11、17、21 的LTS演进、核心API用法、运行时变化和生产升级风险。

学习路线

mermaid
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 变量分两类:基本类型保存值,引用类型保存对象引用。

java
int age = 18;
User user = new User("Tom");

简化理解:

mermaid
flowchart TD
    A["线程栈中的局部变量 age"] --> B["值 18"]
    C["线程栈中的局部变量 user"] --> D["引用地址"]
    D --> E["堆中的 User 对象"]

为什么要区分:

类型保存什么常见问题
基本类型值本身自动装箱可能产生额外对象
引用类型对象引用== 比较的是引用是否同一个对象
局部变量在线程栈帧中方法结束后局部变量消失
对象通常在堆中生命周期由 GC 管理

示例:

java
Integer a = 128;
Integer b = 128;
System.out.println(a == b);      // false,可能不是同一个对象
System.out.println(a.equals(b)); // true,值相等

项目里 ID、金额、状态比较不能乱用 ==。对象内容比较应使用 equals,金额计算应用 BigDecimal,不要用 double 处理钱。

第二步:面向对象到底解决什么

面向对象不是为了把代码写复杂,而是为了把数据和行为组织在一起,让系统能扩展。

java
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任意地方可改状态,状态机失控
方法封装状态流转状态变更规则集中,容易维护
构造器保证必填对象创建后天然合法

继承和多态适合“稳定抽象、多种实现”的场景:

java
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 不可变意味着创建后内容不能被改。

java
String s = "abc";
s = s + "d";

这里不是原字符串变成 abcd,而是创建新字符串,再让变量 s 指向新对象。

不可变的好处:

好处说明
线程安全多线程共享字符串不用担心内容被改
可缓存 hashHashMap key 查询更稳定
字符串常量池相同字面量可复用
安全性文件路径、类名、URL 等不被中途篡改

拼接大量字符串应用 StringBuilder

java
StringBuilder builder = new StringBuilder();
for (String part : parts) {
    builder.append(part);
}
String result = builder.toString();

异常怎么分层处理

异常不是“哪里都 catch 然后打印一下”。

mermaid
flowchart TD
    A["底层 IO/SQL 异常"] --> B["DAO 或 Gateway 捕获并转换"]
    B --> C["Service 判断业务能否恢复"]
    C --> D["Controller 统一异常响应"]
    D --> E["日志、告警、错误码"]

建议:

  1. 能恢复的异常,当前层处理。
  2. 不能恢复的异常,抛给统一异常处理。
  3. 不要吞异常。
  4. 日志要有业务 ID、请求 ID、关键参数。
  5. finally 负责释放资源,优先使用 try-with-resources
java
try (InputStream in = Files.newInputStream(path)) {
    return in.readAllBytes();
}

大文件不要 readAllBytes,要分块读,否则容易 OOM。

第四步:集合和泛型

集合是项目里最常用也最容易出错的 JavaSE 基础。

集合底层特点适合不适合
ArrayList数组随机访问、尾部追加频繁中间插入删除
LinkedList双向链表频繁首尾操作随机访问
HashMap数组 + 链表/红黑树key-value 查询多线程并发写
HashSet基于 HashMap去重需要有序
TreeMap红黑树有序 key极致查询性能
ConcurrentHashMapCAS + 桶级锁并发读写复合业务原子性

HashMap 为什么依赖 hashCode 和 equals

mermaid
flowchart TD
    A["put(key,value)"] --> B["计算 hashCode"]
    B --> C["定位桶 index"]
    C --> D{"桶里是否已有节点"}
    D -- "没有" --> E["直接放入"]
    D -- "有" --> F["用 equals 比较 key"]
    F --> G["相等覆盖,不等挂链表/树"]

如果重写 equals 不重写 hashCode,两个业务上相等的对象可能落到不同桶里,导致去重失败。

java
public record AssetKey(String hospitalCode, String assetNo) {
}

JDK 16 才有 record。JDK 8 项目中可手写不可变 key,并重写 equals/hashCode

泛型的本质是编译期类型约束,运行期有类型擦除:

java
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 7try-with-resources、diamond 语法、switch 支持 String、NIO.2资源释放和文件 API 更好用
JDK 8Lambda、Stream、Optional、新日期时间、接口默认方法、CompletableFuture、HashMap 红黑树函数式编程、集合处理、异步编排更常见

JDK 8 Lambda 示例:

java
List<Asset> usedAssets = assets.stream()
    .filter(asset -> asset.getStatus() == AssetStatus.USED)
    .sorted(Comparator.comparing(Asset::getUpdatedAt).reversed())
    .toList(); // JDK 16+

JDK 8 中没有 Stream.toList(),应使用:

java
List<Asset> usedAssets = assets.stream()
    .filter(asset -> asset.getStatus() == AssetStatus.USED)
    .sorted(Comparator.comparing(Asset::getUpdatedAt).reversed())
    .collect(Collectors.toList());

JDK 8 日期时间:

java
LocalDateTime now = LocalDateTime.now();
ZonedDateTime beijing = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));

不要继续滥用 DateCalendar 处理复杂时间逻辑。

第六步:反射、注解和 SPI

Spring、MyBatis、JUnit 都大量依赖反射和注解。

反射可以在运行期拿到类信息并调用方法:

java
Class<?> clazz = Class.forName("com.example.AssetService");
Object service = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getDeclaredMethod("syncAsset", Long.class);
method.invoke(service, 1001L);

注解用于给代码加元数据:

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AuditLog {
    String action();
}

框架流程:

mermaid
flowchart TD
    A["扫描 classpath"] --> B["读取类和方法"]
    B --> C["检查注解"]
    C --> D["反射创建对象或代理"]
    D --> E["运行时增强逻辑"]

为什么不能滥用反射:

  1. 编译期类型检查变弱。
  2. 性能和可读性不如直接调用。
  3. 私有成员反射会破坏封装。
  4. 模块化和安全限制下可能失败。

SPI 用于“接口由调用方定义,实现由第三方提供”:

java
ServiceLoader<AssetExporter> loader = ServiceLoader.load(AssetExporter.class);
for (AssetExporter exporter : loader) {
    exporter.export(assets);
}

JDBC 驱动加载、日志门面、部分框架扩展都能看到 SPI 思路。

第七步:IO 与 NIO

BIO 线程模型:

mermaid
flowchart TD
    A["客户端连接1"] --> B["线程1 read 阻塞"]
    C["客户端连接2"] --> D["线程2 read 阻塞"]
    E["客户端连接N"] --> F["线程N read 阻塞"]

连接多时线程数暴涨,线程栈和上下文切换成本高。

NIO 模型:

mermaid
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

并发问题本质是三个词:可见性、原子性、有序性。

mermaid
flowchart TD
    A["线程A 修改变量"] --> B["线程工作内存"]
    B --> C["主内存"]
    C --> D["线程B 工作内存"]
    D --> E["线程B 读取变量"]

volatile 解决可见性和部分有序性,不解决复合操作原子性:

java
private volatile boolean running = true;

public void stop() {
    running = false;
}

count++ 不是原子操作:

java
count = count + 1;

包含读、加、写三步,多线程下会丢更新。可以用 AtomicInteger

java
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();

CAS 流程:

mermaid
flowchart TD
    A["读取旧值 expect"] --> B["计算新值 update"]
    B --> C{"内存值仍等于 expect"}
    C -- "是" --> D["写入 update"]
    C -- "否" --> E["重试或失败"]

CAS 的问题:ABA、自旋消耗、只能保护单变量。复杂业务仍然需要锁、事务或状态机。

第九步:锁、AQS 和 JUC

synchronizedReentrantLock 都能做互斥,但能力不同。

特点
synchronizedJVM 内置,自动释放,简单可靠
ReentrantLock可中断、可超时、公平锁、多个 Condition
ReentrantReadWriteLock读多写少场景,读读并发
Semaphore控制同时进入的许可数
CountDownLatch等待多个任务完成
CyclicBarrier多线程互相等待到齐

AQS 的核心:

mermaid
flowchart TD
    A["线程尝试获取资源"] --> B{"state 是否可获取"}
    B -- "可以" --> C["CAS 修改 state 成功"]
    B -- "不可以" --> D["进入 FIFO 等待队列"]
    D --> E["阻塞等待唤醒"]
    E --> F["被唤醒后再次竞争"]

AQS 不是业务类,而是 JUC 锁和同步器的底层框架。理解它能帮你看懂 ReentrantLock、Semaphore、CountDownLatch 为什么都像“状态 + 队列”。

第十步:线程池生产级理解

不要无限 new Thread。线程池用来控制并发、复用线程、管理队列和拒绝策略。

执行流程:

mermaid
flowchart TD
    A["提交任务"] --> B{"核心线程数是否满"}
    B -- "未满" --> C["创建核心线程执行"]
    B -- "已满" --> D{"队列是否满"}
    D -- "未满" --> E["任务入队等待"]
    D -- "已满" --> F{"最大线程数是否满"}
    F -- "未满" --> G["创建非核心线程执行"]
    F -- "已满" --> H["执行拒绝策略"]

核心参数:

参数作用配错后果
corePoolSize常驻核心线程太小吞吐低,太大浪费
maximumPoolSize最大线程数太大压垮下游,太小容易拒绝
workQueue等待队列无界队列可能 OOM
keepAliveTime非核心线程存活时间回收过快或过慢
RejectedExecutionHandler拒绝策略静默丢任务或放大故障

示例:

java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    8,
    16,
    60,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(1000),
    new ThreadFactoryBuilder().setNameFormat("asset-sync-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

生产监控要看:

  1. 活跃线程数。
  2. 队列长度。
  3. 任务执行耗时。
  4. 拒绝次数。
  5. 下游数据库连接池、HTTP 连接池是否被打满。

线程池不是越大越好。IO 任务多也要看下游承载能力,不能用线程池把数据库和第三方接口打垮。

第十一步:JVM 组成和 OOM 排查

JVM 运行时数据区:

mermaid
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 limitGC 很频繁但回收少GC 日志、存活对象分析
Metaspace动态生成类太多、类加载器泄漏类加载统计
Direct buffer memoryNIO/Netty 堆外内存泄漏DirectMemory、ByteBuf 泄漏
unable to create new native thread线程太多jstack、线程池、系统限制

排查流程:

mermaid
flowchart TD
    A["发生 OOM"] --> B["确认 OOM 类型"]
    B --> C["保留日志和 dump"]
    C --> D["查看 GC 日志"]
    D --> E["分析对象占用和引用链"]
    E --> F["定位代码路径"]
    F --> G["修复并压测验证"]

常用命令:

bash
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 可能做优化:

优化解释
栈上分配对象不真正分配到堆上
标量替换对象拆成几个字段变量
锁消除对不会被多线程访问的锁去掉

示例:

java
public int sum() {
    Point p = new Point(1, 2);
    return p.x + p.y;
}

如果 p 不逃出方法,JIT 可能不在堆上创建完整 Point 对象,而是直接用两个标量值计算。

注意:这不是 Java 语义保证,而是 JVM 优化。写代码不要依赖“对象一定在栈上分配”,排查内存也要结合实际 JVM 参数和 JIT 行为。

第十三步:项目落地 Demo

医疗资产批量同步任务:

java
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 背后涉及:

  1. 集合:List<Long> 批量任务。
  2. 面向对象:Asset.from 封装转换规则。
  3. 线程池:控制同步并发。
  4. IO:remoteClient 是网络 IO。
  5. 异常:单个资产失败不能吞异常,要记录和重试。
  6. JVM:批量太大或队列太大可能造成 OOM。
  7. 生产排查:要监控线程池队列、下游耗时、失败率。

线上排查总流程

mermaid
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、锁
OOMOOM 类型、GC 日志、heap dump、直接内存
线程池满活跃线程、队列、拒绝、下游耗时
ThreadLocal 串数据是否 finally remove
大文件 OOM是否一次性读入、是否分批处理

常见坑

后果正确做法
对象比较乱用 ==业务判断错误对象内容用 equals
HashMap key 不重写 hashCode查不到或去重失败equals/hashCode 同时重写
大量字符串拼接用 +创建很多临时对象循环用 StringBuilder
捕获异常只打印上层以为成功记录上下文并抛出或补偿
无界线程池/队列OOM 或压垮下游有界队列 + 监控 + 拒绝策略
ThreadLocal 不 remove内存泄漏或串数据finally remove
一次性读大文件堆 OOM分块读取、流式处理
只调大 JVM 内存掩盖泄漏先分析对象和引用链

面试标准回答

JavaSE 怎么从零学到生产可用

text
JavaSE 要按基础语法、面向对象、常用类型、集合泛型、反射注解、IO/NIO、并发、线程池、JVM 和生产排查这条线学习。基础语法解决代码怎么写,面向对象解决业务规则怎么组织,集合和泛型解决批量数据和类型安全,反射注解是 Spring/MyBatis 等框架基础,IO/NIO 解决文件和网络,JUC 和线程池解决并发控制,JVM 解决内存、类加载、GC 和性能排查。真正生产可用不是会背 API,而是能解释原理、知道不这样做的风险,并能排查 OOM、CPU 高、线程池堆积、IO 阻塞等问题。

JDK 7 和 JDK 8 常见区别

text
JDK 7 常见重点包括 try-with-resources、diamond 语法、switch 支持 String、NIO.2 文件 API。JDK 8 是企业项目最常见基础版本,重点包括 Lambda、Stream、Optional、新日期时间 API、接口默认方法、CompletableFuture,以及 HashMap 在链表过长时树化为红黑树。学习时不要只背版本特性,要知道它们解决了资源释放、集合处理、异步编排、日期时间和集合性能问题。

OOM 怎么排查

text
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 与 NIOBIO、NIO、Selector、零拷贝
多线程JMM、锁、ThreadLocal、JUC
线程池参数、执行流程、拒绝、监控
CAS、AQS、ReentrantLock
JVM 组成JVM 各模块和运行时区域
JIT 逃逸分析JIT 优化和对象分配
JavaSE 面试标准回答和追问

本章小结

JavaSE 从零到生产级掌握,重点是把语法、对象、集合、反射、IO、并发、线程池、JVM 和排查连成一条线。你不是为了背 API,而是为了看懂框架为什么这样设计,项目为什么这样写,线上出了问题为什么会发生,以及应该从哪一层定位。