Java 语言
从「能写」到「写得对、跑得稳」。这一页重点覆盖集合、并发与 JVM 三块面试与实战都绕不开的内容。
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 / short | 1 / 2 | 0 | Byte / Short |
int / long | 4 / 8 | 0 / 0L | Integer / Long |
float / double | 4 / 8 | 0.0f / 0.0d | Float / Double |
char | 2 | '\u0000' | Character |
boolean | JVM 相关 | false | Boolean |
// 缓存陷阱: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,否则HashMap、HashSet会找不到数据。
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) {},编译器自动生成构造器、equals、hashCode 与 toString。
继承与多态的三个关键词
| 关键字 | 作用 | 注意 |
|---|---|---|
abstract | 抽象类 / 抽象方法,必须被继承实现 | 不能实例化 |
interface | 定义契约,支持多实现与 default 方法 | 字段默认 public static final |
final | 类不可继承、方法不可重写、变量不可改 | 不可变对象是线程安全的基石 |
03集合框架
选错集合类型,是性能问题最常见的来源之一。
选型速查
| 接口 | 实现 | 底层结构 | 特点 |
|---|---|---|---|
| List | ArrayList | 动态数组 | 随机访问 O(1),中间插入 O(n) |
| LinkedList | 双向链表 | 头尾增删快,随机访问 O(n) | |
| CopyOnWriteArrayList | 写时复制数组 | 读多写少的并发场景 | |
| Map | HashMap | 数组 + 链表/红黑树 | 无序,O(1) 平均查找 |
| TreeMap | 红黑树 | 按 key 有序,O(log n) | |
| Set | HashSet | 基于 HashMap | 去重,无序 |
| LinkedHashSet | HashMap + 链表 | 去重且保留插入顺序 | |
| Queue | ArrayDeque | 循环数组 | 栈/队列首选,优于 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/remove;List.of() 返回完全不可变集合。需要可变集合时用 new ArrayList<>(...) 包一层。
04异常与资源管理
异常设计的核心是:分清「能恢复」和「不能恢复」。
| 类型 | 是否受检 | 典型场景 |
|---|---|---|
Error | 否 | OOM、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 是惰性的:没有终端操作(collect、forEach、count 等)就不会执行。
一个 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
异步编排:thenApply、thenCombine、allOf。
// 并行调用两个下游服务并合并结果
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 GC | jstat -gcutil 的 O 与 FGC 列 | 堆太小、内存泄漏、大对象 |
| CPU 100% | top -H -p + jstack | 死循环、正则回溯、频繁 GC |
| 接口偶发卡顿 | jstack 多次采样对比 | 锁竞争、下游超时、线程池耗尽 |
| OOM: Java heap space | heap dump + MAT 分析 | 集合无界增长、缓存未淘汰 |
| OOM: Metaspace | jstat -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 应用真正装进容器。