Skip to content

JavaSE 从零到精通验收清单

这页用来验收 JavaSE 是否真正学懂。不是“我知道有 ListMap、线程池、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 阻塞
面试能答面试页回答标准清晰,原理能跳回知识点页展开

总路线

mermaid
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 编译成机器码。

mermaid
flowchart TD
    A["Hello.java 源码"] --> B["javac 编译"]
    B --> C["Hello.class 字节码"]
    C --> D["类加载器加载"]
    D --> E["字节码验证"]
    E --> F["解释执行或 JIT 编译"]
    F --> G["CPU 执行机器指令"]

Demo

java
package com.example.basic;

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello Java");
    }
}

编译运行:

bash
javac -d out src/com/example/basic/Hello.java
java -cp out com.example.basic.Hello

为什么要懂

很多新手报错不是语法不会,而是不懂包名和 classpath:

错误原因
ClassNotFoundException主动加载某个类时 classpath 找不到
NoClassDefFoundError编译时有类,运行时缺类,或类初始化失败
主类找不到-cp 指错了,或运行时没写全限定类名

详细知识点:基础语法

阶段 2:变量、类型、值传递和金额精度

核心概念

Java 类型分为基本类型和引用类型。

类型保存内容例子
基本类型值本身intlongdoubleboolean
引用类型对象引用StringUserList<Order>
mermaid
flowchart TD
    A["栈帧局部变量 int count"] --> B["保存值 10"]
    C["栈帧局部变量 User user"] --> D["保存引用"]
    D --> E["堆中的 User 对象"]

Java 是值传递

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 是二进制浮点,很多十进制小数无法精确表示。

java
System.out.println(0.1 + 0.2); // 0.30000000000000004

商业项目里金额要用:

java
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 关系,复用父类能力
多态面向接口编程,运行时选择具体实现

反例:贫血且无保护的订单

java
class Order {
    public String status;
}

Order order = new Order();
order.status = "PAID";
order.status = "CREATED"; // 已支付订单又被改回待支付

正例:用方法保护状态

java
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。如果内容能被修改,集合查找和安全边界都会被破坏。

java
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 常用 DateCalendar,API 可读性差且易变。JDK 8 引入 LocalDateLocalDateTimeInstantDateTimeFormatter,更适合表达日期、时间、时间戳和格式化。

java
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);

详细知识点:String详解异常处理日期时间API

阶段 5:集合、泛型和 HashMap

集合怎么选

需求推荐结构原因
按顺序保存列表ArrayList查询快,尾部追加快
频繁头部插入删除LinkedList 或队列但业务中要谨慎,缓存局部性差
去重HashSet底层基于 HashMap
key-value 映射HashMap根据 hash 定位
保持插入顺序LinkedHashMap维护双向链表
排序TreeMapTreeSet红黑树,有序
并发映射ConcurrentHashMap桶级并发控制

HashMap 为什么要求 equals 和 hashCode 一起重写

HashMap 先用 hash 定位桶,再用 equals 判断桶内 key 是否相同。

mermaid
flowchart TD
    A["put key"] --> B["计算 hash"]
    B --> C["定位数组下标"]
    C --> D["桶为空则放入"]
    C --> E["桶不为空则比较 equals"]
    E --> F["相同 key 覆盖 value"]
    E --> G["不同 key 追加链表或红黑树"]

如果 equals 相等但 hashCode 不同,两个对象可能落到不同桶里,HashMap 就查不到原来的数据。

java
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 7JDK 8
冲突结构数组 + 链表数组 + 链表 + 红黑树
插入方式头插法尾插法
高冲突性能链表退化为 O(n)树化后接近 O(log n)
扩容迁移重新计算位置利用 hash & oldCap 拆分

不要只背“JDK 8 有红黑树”。要知道为什么:当 hash 冲突严重时,链表查找会退化。红黑树是为了避免极端冲突下查找太慢。

详细知识点:集合框架HashMap全过程ArrayList全过程

阶段 6:泛型、枚举、注解、反射和 SPI

泛型解决什么问题

泛型让集合和方法在编译期获得类型检查,减少强制转换和运行时 ClassCastException

java
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 等框架都要在不知道具体业务类的情况下创建对象、读取注解、调用方法、设置字段。

java
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);
    }
}

注解怎么和反射配合

注解本身只是元数据,必须被编译器、框架或运行时反射读取后才有行为。

java
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一个连接通常对应一个线程,读写阻塞简单低并发、文件读写
NIOChannel + Buffer + Selector,一个线程管理多个连接高并发网络服务
mermaid
flowchart TD
    A["客户端连接"] --> B["Selector 监听事件"]
    B --> C["Accept 事件"]
    B --> D["Read 事件"]
    B --> E["Write 事件"]
    D --> F["Channel 读取到 Buffer"]
    F --> G["解码并交给业务线程池"]

文件分批读取 Demo

大文件不能一次性 readAllBytes 放进内存。应该分块读取,分批处理。

java
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 保证可见性和一定的有序性,不保证复合操作原子性。

java
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:

java
import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger count = new AtomicInteger();
count.incrementAndGet();

synchronized 和 ReentrantLock

synchronizedReentrantLock
使用方式关键字类库 API
释放锁自动释放必须 finally unlock
可中断不支持等待中断支持 lockInterruptibly
公平锁不支持显式公平可选公平
条件队列一个 wait set多个 Condition

详细知识点:JMMsynchronized全过程volatile与Atomic全过程AQS与JUC全过程

阶段 9:线程池和 CompletableFuture

为什么不能无限 new Thread

线程不是越多越好。每个线程都有栈内存、调度成本和上下文切换成本。无限创建线程会导致 CPU 切换、内存上涨、连接池打满、下游被压垮。

线程池执行流程

mermaid
flowchart TD
    A["提交任务"] --> B{"工作线程数 < corePoolSize"}
    B -->|是| C["创建核心线程执行"]
    B -->|否| D{"队列是否未满"}
    D -->|是| E["放入工作队列"]
    D -->|否| F{"工作线程数 < maximumPoolSize"}
    F -->|是| G["创建非核心线程执行"]
    F -->|否| H["执行拒绝策略"]

线程池 Demo

java
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 注意点

java
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 由哪些部分组成

mermaid
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永久代 PermGenPermGen space
JDK 8元空间 Metaspace,本地内存Metaspace

JDK 8 移除永久代不代表不会元空间 OOM。动态代理、CGLIB、脚本引擎、热部署、类加载器泄漏都会撑爆 Metaspace。

TLAB 和逃逸分析

TLAB 是 Eden 区里给线程预留的一小段线程本地分配缓冲区,仍然属于堆。它减少多线程对象分配时对 Eden 指针的竞争。

逃逸分析是 JIT 优化:如果对象没有逃出方法或线程,JIT 可能做标量替换、锁消除、分配消除,让对象不以完整堆对象形式存在。它不是 Java 语法层面的“手动栈分配”,也不是所有对象都能优化。

OOM 排查流程

mermaid
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 memoryNIO/Netty 堆外内存泄漏direct memory、ByteBuf release
unable to create native thread无限建线程、线程栈过大线程数、线程池、-Xss
OOMKilled容器总内存超限Pod 事件、退出码 137、JVM 总预算

详细知识点:JVM组成JIT逃逸分析JVM排查工具

阶段 11:商业场景验收

场景 1:订单导入

要求:

  • 使用 BufferedReader 分批读取 CSV。
  • BigDecimal 处理金额。
  • HashMap 按订单号去重。
  • 分批入库,不能一次性把所有数据放入内存。
  • 异常要记录行号、订单号、原因。

验收问题:

  • 为什么大文件不能 readAllBytes
  • 为什么订单号适合做 Map key?
  • 为什么金额不用 double?
  • 如果导入过程中 OOM 怎么查?

场景 2:库存扣减并发控制

要求:

  • JVM 内单机并发可以用锁或原子类保护内存状态。
  • 数据库库存必须依赖数据库事务和乐观锁。
  • 不能把 volatile int stock 当作库存扣减方案。

验收问题:

  • volatile 为什么不能保证 stock-- 原子性?
  • synchronizedReentrantLock 怎么选?
  • 单机锁为什么不能解决多实例并发?

场景 3:接口聚合

要求:

  • CompletableFuture 并行查用户、订单、优惠券。
  • 使用自定义线程池。
  • 每个下游有超时和异常兜底。
  • 监控线程池队列、活跃线程、拒绝次数。

验收问题:

  • 为什么不能直接 parallelStream 调 HTTP?
  • 为什么 orTimeout 不一定停止底层 HTTP 调用?
  • 下游慢导致线程池堆积怎么查?

阶段 12:面试闭环

面试页只放标准回答,深入原理回到知识点页。

mermaid
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 面试知识点

最终验收题

  1. Java 程序从源码到运行经历哪些步骤?
  2. classpath 指向包目录还是类文件目录?为什么?
  3. Java 为什么是值传递?引用类型参数为什么能改对象字段?
  4. 金额为什么不能用 double?BigDecimal 有什么坑?
  5. 封装为什么不是给字段生成 getter/setter?
  6. String 为什么不可变?new String("abc") 怎么理解?
  7. HashMap 为什么容量常用 2 的幂?
  8. JDK 7 和 JDK 8 的 HashMap 有什么区别?
  9. 为什么 equalshashCode 必须一起重写?
  10. 泛型擦除带来哪些限制?
  11. 注解为什么必须配合解析器才有行为?
  12. 反射为什么能支撑 Spring 和 MyBatis?
  13. BIO 和 NIO 的线程模型有什么不同?
  14. 大文件导入为什么会 OOM?怎么改?
  15. volatile 能不能保证 count++ 安全?为什么?
  16. CAS 有哪些问题?ABA 怎么解决?
  17. AQS 为什么是 JUC 的基础?
  18. 线程池提交任务后的完整执行流程是什么?
  19. executesubmit 异常表现有什么不同?
  20. CompletableFuture 为什么必须指定线程池?
  21. JDK 7 永久代和 JDK 8 元空间有什么区别?
  22. TLAB 属于堆吗?和逃逸分析有什么区别?
  23. JIT 逃逸分析可能做哪些优化?
  24. Heap OOM、Metaspace OOM、Direct Memory OOM 怎么区分?
  25. 容器 OOMKilled 和 Java OOM 有什么区别?
  26. JDK 8、11、17、21各自最重要的正式特性是什么?
  27. List.ofvar、Record、sealed、虚拟线程分别在哪个版本正式引入?
  28. JDK 8升级到11为什么可能出现JAXB类缺失和旧GC参数失效?
  29. JDK 11升级17为什么可能出现InaccessibleObjectException
  30. JDK 21虚拟线程为什么仍要限制数据库和下游并发?如何排查pinning?

版本专题和完整代码见:JDK 8、11、17、21 LTS版本演进

本章小结

JavaSE 是后端工程的地基。Spring 的 IOC 依赖反射和注解,MyBatis 依赖反射和动态代理,Netty 依赖 NIO 和堆外内存,MQ 消费依赖线程池和并发控制,线上排查依赖 JVM 和操作系统知识。真正学会 JavaSE,不是背 API,而是把每个基础点和项目问题、框架原理、线上故障连起来。