模块 02

Java 语言

从「能写」到「写得对、跑得稳」。这一页重点覆盖集合、并发与 JVM 三块面试与实战都绕不开的内容。

8 个章节 JDK 17+ 示例可在容器内验证

01运行模型与工具链

先理解「一次编写,到处运行」是怎么实现的。

Java 源码由 javac 编译为字节码 .class,再由 JVM 加载执行。JVM 屏蔽了操作系统差异,字节码因此可以跨平台;真正的性能来自 JIT 在运行时把热点代码编译成本地机器码。

JDK

开发工具包,含 javac、jconsole、jmap 等,开发与排障都用它。

JRE

运行时环境,只含运行所需类库与 JVM。JDK 9 之后不再单独分发。

JVM

执行引擎,负责类加载、内存管理、垃圾回收与即时编译。

常用命令

java -version                 # 查看运行时版本
javac -encoding UTF-8 Main.java
java -jar app.jar             # 运行可执行 jar

# 打包(无构建工具时的最小方案)
jar --create --file app.jar --main-class com.demo.Main -C classes .

# 模块与依赖信息
jdeps --summary app.jar
jlink --list-modules | head

Maven 最小可用 pom

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.demo</groupId>
  <artifactId>demo-app</artifactId>
  <version>1.0.0</version>

  <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.5.1</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals><goal>shade</goal></goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

02语法基础与面向对象

把「值传递」「equals/hashCode」这两个高频坑说透。

基本类型与包装类

类型字节默认值对应包装类
byte / short1 / 20Byte / Short
int / long4 / 80 / 0LInteger / Long
float / double4 / 80.0f / 0.0dFloat / Double
char2'\u0000'Character
booleanJVM 相关falseBoolean
// 缓存陷阱:Integer 默认缓存 -128 ~ 127
Integer a = 127, b = 127;
System.out.println(a == b);        // true(同一缓存对象)

Integer c = 128, d = 128;
System.out.println(c == d);        // false!

System.out.println(c.equals(d));   // true,比较对象永远用 equals

// 金额不要用 double
System.out.println(0.1 + 0.2);              // 0.30000000000000004
System.out.println(new java.math.BigDecimal("0.1")
        .add(new java.math.BigDecimal("0.2")));   // 0.3

值传递:Java 只有值传递

public class PassDemo {
    static class User { String name; }

    static void changePrimitive(int x) { x = 100; }        // 不影响调用方

    static void changeReference(User u) { u.name = "Bob"; }  // 影响(改的是对象内容)

    static void reassign(User u) { u = new User(); u.name = "Carol"; } // 不影响

    public static void main(String[] args) {
        int n = 1;
        changePrimitive(n);
        System.out.println(n);          // 1

        User u = new User();
        u.name = "Alice";
        changeReference(u);
        System.out.println(u.name);     // Bob

        reassign(u);
        System.out.println(u.name);     // Bob,引用副本被重新指向,不影响原引用
    }
}

equals 与 hashCode 的契约

  • 两个对象 equals 为 true,则 hashCode 必须相等。
  • hashCode 相等,equals 不一定相等(哈希冲突)。
  • 重写 equals 必须同时重写 hashCode,否则 HashMapHashSet 会找不到数据。
import java.util.Objects;

public final class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) { this.x = x; this.y = y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point p)) return false;   // JDK 16+ 模式匹配
        return x == p.x && y == p.y;
    }

    @Override
    public int hashCode() { return Objects.hash(x, y); }
}
💡

record 可以省掉这段样板代码:public record Point(int x, int y) {},编译器自动生成构造器、equalshashCodetoString

继承与多态的三个关键词

关键字作用注意
abstract抽象类 / 抽象方法,必须被继承实现不能实例化
interface定义契约,支持多实现与 default 方法字段默认 public static final
final类不可继承、方法不可重写、变量不可改不可变对象是线程安全的基石

03集合框架

选错集合类型,是性能问题最常见的来源之一。

选型速查

接口实现底层结构特点
ListArrayList动态数组随机访问 O(1),中间插入 O(n)
LinkedList双向链表头尾增删快,随机访问 O(n)
CopyOnWriteArrayList写时复制数组读多写少的并发场景
MapHashMap数组 + 链表/红黑树无序,O(1) 平均查找
TreeMap红黑树按 key 有序,O(log n)
SetHashSet基于 HashMap去重,无序
LinkedHashSetHashMap + 链表去重且保留插入顺序
QueueArrayDeque循环数组栈/队列首选,优于 Stack

HashMap 关键机制

  • 默认容量 16,负载因子 0.75。元素数超过 容量 × 0.75 时扩容为 2 倍。
  • 下标计算:(n - 1) & hash,n 为 2 的幂时等价于取模但更快。
  • JDK 8 起,单个桶链表长度 ≥ 8 且表容量 ≥ 64 时转为红黑树,避免极端哈希冲突导致 O(n)。
  • 非线程安全。并发写可能导致数据丢失甚至结构损坏,多线程请用 ConcurrentHashMap
import java.util.*;

Map<String, Integer> counter = new HashMap<>(16);   // 预估容量,减少扩容

// 统计词频的惯用写法
for (String word : List.of("a", "b", "a", "c", "a")) {
    counter.merge(word, 1, Integer::sum);
}
System.out.println(counter);            // {a=3, b=1, c=1}

// 遍历中修改的正确做法
Map<String, Integer> safe = new HashMap<>(counter);
safe.entrySet().removeIf(e -> e.getValue() < 2);

// 排序:按 value 降序
List<Map.Entry<String, Integer>> list = new ArrayList<>(counter.entrySet());
list.sort(Map.Entry.<String, Integer>comparingByValue().reversed());

// 只读视图,防止调用方误改
Map<String, Integer> readOnly = Collections.unmodifiableMap(safe);
⚠️

Arrays.asList() 返回的是固定长度视图,不能 add/removeList.of() 返回完全不可变集合。需要可变集合时用 new ArrayList<>(...) 包一层。

04异常与资源管理

异常设计的核心是:分清「能恢复」和「不能恢复」。

类型是否受检典型场景
ErrorOOM、StackOverflow,程序无法处理
RuntimeException参数错误、空指针,属于编程错误
Exception(其他子类)IO、网络,调用方必须处理
// try-with-resources:自动关闭实现了 AutoCloseable 的资源
public static String readFirstLine(Path path) throws IOException {
    try (BufferedReader reader = Files.newBufferedReader(path)) {
        return reader.readLine();
    }
}

// 自定义业务异常,保留原始异常链
public class OrderNotFoundException extends RuntimeException {
    public OrderNotFoundException(Long id, Throwable cause) {
        super("订单不存在: " + id, cause);   // 不要丢掉 cause,否则排查时断链
    }
}

// 转换异常并保留上下文
try {
    orderRepository.findById(id);
} catch (DataAccessException e) {
    throw new OrderNotFoundException(id, e);
}
🚫

反模式:catch (Exception e) {} 空捕获、只打印 e.getMessage() 丢掉堆栈、用异常做流程控制。这三种写法会让线上问题变得无法定位。

05泛型、Lambda 与 Stream

现代 Java 的日常写法,写得好能显著减少循环与临时变量。

泛型要点

// 泛型擦除:运行时无法获得 T 的真实类型
public <T extends Comparable<T>> T max(List<T> list) {
    return list.stream().max(Comparator.naturalOrder()).orElseThrow();
}

// PECS 原则:生产者用 extends,消费者用 super
void copy(List<? extends Number> src, List<? super Number> dst) {
    for (Number n : src) dst.add(n);
}

Lambda 与方法引用

Runnable r = () -> System.out.println("run");
Function<String, Integer> len = String::length;        // 实例方法引用
Supplier<ArrayList<String>> factory = ArrayList::new;  // 构造器引用
BiFunction<Integer, Integer, Integer> sum = Integer::sum;

// 闭包捕获的局部变量必须是 effectively final
int base = 10;
IntUnaryOperator add = x -> x + base;   // base 不可再赋值

Stream 流水线

record Order(String customer, String status, double amount) {}

List<Order> orders = List.of(
    new Order("alice", "PAID", 120.0),
    new Order("bob",   "PAID", 80.5),
    new Order("alice", "CANCELLED", 30.0),
    new Order("carol", "PAID", 260.0)
);

// 统计已支付订单中每位客户的消费总额,按金额降序
Map<String, Double> paidByCustomer =
    orders.stream()
          .filter(o -> "PAID".equals(o.status()))
          .collect(Collectors.groupingBy(
                  Order::customer,
                  Collectors.summingDouble(Order::amount)));

paidByCustomer.entrySet().stream()
    .sorted(Map.Entry.<String, Double>comparingByValue().reversed())
    .forEach(e -> System.out.printf("%s -> %.2f%n", e.getKey(), e.getValue()));

// 常用终端操作
orders.stream().map(Order::amount).mapToDouble(Double::doubleValue).average();
orders.stream().collect(Collectors.partitioningBy(o -> o.amount() > 100));
orders.stream().map(Order::customer).distinct().toList();   // JDK 16+
⚠️

Stream 是惰性的:没有终端操作(collectforEachcount 等)就不会执行。

一个 Stream 只能消费一次,重复使用会抛 IllegalStateException: stream has already been operated upon or closed

06并发编程

线程安全的三要素:原子性、可见性、有序性。

线程池:必须手动创建

import java.util.concurrent.*;

ThreadPoolExecutor executor = new ThreadPoolExecutor(
        4,                                  // 核心线程数
        8,                                  // 最大线程数
        60L, TimeUnit.SECONDS,              // 空闲回收时间
        new ArrayBlockingQueue<>(200),      // 有界队列,避免无界导致 OOM
        new ThreadFactory() {
            private final java.util.concurrent.atomic.AtomicInteger seq = new AtomicInteger();
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r, "biz-pool-" + seq.incrementAndGet());
                t.setDaemon(false);
                return t;
            }
        },
        new ThreadPoolExecutor.CallerRunsPolicy()   // 饱和策略:由提交者执行,起到反压作用
);

Future<Integer> future = executor.submit(() -> 42);
Integer result = future.get(2, TimeUnit.SECONDS);   // 一定要设超时

executor.shutdown();
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
    executor.shutdownNow();
}
饱和策略行为适用
AbortPolicy(默认)抛 RejectedExecutionException可接受快速失败
CallerRunsPolicy提交线程自己执行希望反压、不丢任务
DiscardPolicy静默丢弃可丢的埋点类任务
DiscardOldestPolicy丢最老任务再重试只关心最新数据
🚫

不要用 Executors.newFixedThreadPool / newCachedThreadPool前者用无界队列,堆积任务会 OOM;后者最大线程数为 Integer.MAX_VALUE,高并发下会创建海量线程。生产环境手动 new ThreadPoolExecutor 并给出有界队列。

同步与可见性

// 复合操作必须保证原子性
private final AtomicLong counter = new AtomicLong();

public void increment() {
    counter.incrementAndGet();          // 而不是 count++
}

// 缓存场景用读写锁 / 并发容器
private final java.util.concurrent.ConcurrentHashMap<String, Object> cache =
        new ConcurrentHashMap<>();

public Object getOrLoad(String key) {
    return cache.computeIfAbsent(key, this::loadFromDbEventually);
}

// 双重检查单例:volatile 防止指令重排
public final class Singleton {
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) instance = new Singleton();
            }
        }
        return instance;
    }
}

并发工具一览

CountDownLatch

一次性的「等待 N 个任务完成」,适合启动前置检查。

CyclicBarrier

可循环使用,等待一批线程到齐后再同时继续。

Semaphore

限流、控制并发访问资源的数量。

CompletableFuture

异步编排:thenApplythenCombineallOf

// 并行调用两个下游服务并合并结果
CompletableFuture<String> userFuture  = CompletableFuture.supplyAsync(() -> queryUser(id), executor);
CompletableFuture<String> orderFuture = CompletableFuture.supplyAsync(() -> queryOrder(id), executor);

String merged = userFuture.thenCombine(orderFuture, (u, o) -> u + "|" + o)
        .exceptionally(ex -> "fallback")
        .get(3, TimeUnit.SECONDS);

07JVM 内存与调优

能看懂 GC 日志并给出参数建议,是从「会写」到「能扛」的分水岭。

运行时内存区域

区域线程存放内容是否 GC 管理
程序计数器私有当前字节码行号
虚拟机栈私有栈帧、局部变量表否(栈溢出抛 StackOverflowError)
本地方法栈私有Native 方法调用
共享对象实例是,主要战场
方法区 / 元空间共享类元信息、常量池是,JDK 8 起在本地内存

一个对象的一生

  • 新对象优先分配在新生代 Eden 区。
  • Minor GC 后存活对象进入 Survivor 区,年龄 +1,默认 15 岁晋升老年代。
  • 大对象可通过 -XX:PretenureSizeThreshold 直接进老年代(G1 下由 Humongous 区处理)。
  • 老年代空间不足触发 Full GC,停顿通常最长,是调优重点。

常用启动参数

java -jar app.jar \
  -Xms2g -Xmx2g \                        # 堆初始与最大设为相同,避免动态扩容抖动
  -XX:MetaspaceSize=256m \
  -XX:MaxMetaspaceSize=256m \
  -XX:+UseG1GC \                         # 大堆首选
  -XX:MaxGCPauseMillis=200 \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/opt/app/dumps/ \
  -Xlog:gc*:file=/opt/app/logs/gc.log:time,uptime:filecount=5,filesize=50M
💡

容器中运行必须让 JVM 感知内存上限:JDK 10+ 默认支持 cgroup 限制,但建议显式设置 -XX:MaxRAMPercentage=75,把堆控制在容器 limit 的 75% 左右,给元空间、线程栈和堆外内存留出余量。

排障工具

jps -l                               # 列出 Java 进程
jstat -gcutil <pid> 1000 10          # 每秒打印一次 GC 统计
jmap -histo:live <pid> | head -20    # 对象数量直方图
jmap -dump:format=b,file=/tmp/heap.hprof <pid>

jstack <pid> > /tmp/stack.txt        # 线程快照,查死锁与阻塞
jcmd <pid> VM.flags                  # 查看实际生效的 JVM 参数
jcmd <pid> GC.heap_info

# 死锁检测(jstack 输出中搜索)
grep -A 30 "Found one Java-level deadlock" /tmp/stack.txt

典型问题定位思路

现象首先查看常见根因
频繁 Full GCjstat -gcutil 的 O 与 FGC 列堆太小、内存泄漏、大对象
CPU 100%top -H -p + jstack死循环、正则回溯、频繁 GC
接口偶发卡顿jstack 多次采样对比锁竞争、下游超时、线程池耗尽
OOM: Java heap spaceheap dump + MAT 分析集合无界增长、缓存未淘汰
OOM: Metaspacejstat -gcutil 的 M 列动态生成类过多、热部署泄漏

08自测清单

勾选表示已经能独立讲清或写出代码。

  • 解释 Integer a = 128; Integer b = 128; a == b 为什么是 false
  • 用代码证明 Java 只有值传递,并说明「修改对象属性」为何看起来像引用传递
  • 说出重写 equals 必须重写 hashCode 的原因,并手写一个 record
  • 说明 HashMap 的扩容条件与链表转红黑树的阈值
  • 用 Stream 实现按客户分组求和并按金额倒序
  • 手写一个带命名、有界队列与拒绝策略的 ThreadPoolExecutor
  • 说明 volatile 保证了什么、没有保证什么
  • 用 CompletableFuture 并行调用两个接口并合并结果
  • 说出新生代、老年代、元空间各自存放什么
  • 给定一份 GC 日志,判断是否存在频繁 Full GC 并给出参数建议
  • 用 jstack 定位一次 CPU 100% 的热点方法
  • 为容器内的 Java 进程配置合理的内存参数
🎯

完成 9 项以上后,可以把项目打包成镜像了:前往 Docker 容器 模块,把这里的 Java 应用真正装进容器。