关于 java-best-practices
面向 Java 开发者和编码 Agent 的实践技能,聚焦 Java 21+,并明确排除过时写法。内容涵盖设计、实现、错误处理与测试。
主要功能
- 01Java 21+ 实践指导
- 02组合方法与组合优先
- 03Record 与密封类型
- 04空值与异常处理
- 05流、集合与测试
适用场景
- 01按现代实践审查 Java 代码
- 02实现 Java 21+ 特性
- 03改进方法与类的设计
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ virtuslab/ agent-skills --skill java-best-practices安装后在 Claude Code 中输入/java-best-practices确认已加载。
附带文件examples/ImmutableUser.java
详细介绍
VirtusLab 的 Java 最佳实践
风格鲜明、现代化的 Java 技能。面向 Java 21+,并明确摒弃过时模式。编写或审查 Java 代码时,请严格遵循这些指南。
核心理念
- 组合式方法模式是 Java 中影响最大的实践。每个方法都应只做一件事,处于同一抽象层级,并且短到一眼就能读完。仅这一模式就能将难以阅读的代码转变为整洁的代码。
- **Joshua Bloch 的《Effective Java》**仍是权威参考。书中的原则至今依然适用。如有疑问,请查阅该书。
- **使用库代码。**不要重新实现标准库或成熟库已经提供的功能。这是最常被违反、影响也最大的原则之一。
- **优先使用组合而非继承。**始终如此。使用带默认方法的接口和委托,而不是深层类层次结构。
- GRASP 原则(通用职责分配软件模式)指导面向对象设计:信息专家、创建者、控制器、低耦合、高内聚、多态、纯虚构、间接层、防护性变化。
现代 Java(21+)——使用这些特性
积极使用现代 Java 特性。不要编写 Java 17 之前风格的代码。
数据对象使用记录类
所有不可变数据载体都使用 record。数据对象不要使用 Lombok 的 @Value、@Data、@Getter,也不要手写 POJO。
// WRONG — legacy style
@Value
public class User {
String username;
String email;
}
// WRONG — hand-written boilerplate
public class User {
private final String username;
private final String email;
// constructor, getters, equals, hashCode, toString...
}
// CORRECT
public record User(String username, String email) {}
Lombok 仅可用于非记录类上的 @Builder,且确实需要建造者模式时使用。其他情况都使用记录类或纯 Java。
使用密封类进行领域建模(代数数据类型)
使用 sealed 类和接口来建模代数数据类型。这样可以在 switch 表达式中进行穷尽性检查。
public sealed interface PaymentResult
permits PaymentSuccess, PaymentFailure, PaymentPending {
}
public record PaymentSuccess(String transactionId, Money amount) implements PaymentResult {}
public record PaymentFailure(String reason, ErrorCode code) implements PaymentResult {}
public record PaymentPending(String transactionId) implements PaymentResult {}
使用带模式匹配的 switch 表达式
使用带模式匹配的 switch 表达式(而非语句)。优先使用穷尽式 switch。
// CORRECT
String message = switch (result) {
case PaymentSuccess success ->
"Paid %s".formatted(success.amount());
case PaymentFailure failure ->
"Failed: %s".formatted(failure.reason());
case PaymentPending pending ->
"Pending: %s".formatted(pending.transactionId());
};
// WRONG — old-style switch statement with break
switch (result.getType()) {
case SUCCESS:
message = "Paid";
break;
// ...
}
在 switch 中处理 null(Java 21+)
// CORRECT — null is an explicit case
return switch (status) {
case null -> "unknown";
case ACTIVE -> "active";
case PAUSED -> "paused";
};
// WRONG — null handled outside the switch
if (status == null) return "unknown";
return switch (status) {
case ACTIVE -> "active";
case PAUSED -> "paused";
};
使用 instanceof 进行模式匹配
// CORRECT
if (obj instanceof String s && !s.isBlank()) {
process(s);
}
// WRONG
if (obj instanceof String) {
String s = (String) obj;
process(s);
}
文本块
多行字符串(SQL、JSON、HTML 等)使用文本块。
String query = """
SELECT u.id, u.name
FROM users u
WHERE u.active = true
ORDER BY u.name
""";
局部变量使用 var
当右侧表达式能明显体现类型时,局部变量使用 var。如果会降低可读性,则不要使用 var。
// CORRECT — type is obvious
var users = userRepository.findAll();
var mapper = new ObjectMapper();
// WRONG — type is not obvious, use explicit type
var result = process(data);
记录模式(Java 21)
在 instanceof 和 switch 表达式中直接解构记录类。
// CORRECT
if (obj instanceof Point(int x, int y)) {
System.out.println(x + ", " + y);
}
String desc = switch (shape) {
case Circle(double r) -> "Circle r=" + r;
case Rect(double w, double h) -> "Rect " + w + "x" + h;
};
// WRONG — old style: cast + field access
if (obj instanceof Point p) {
System.out.println(p.x() + ", " + p.y());
}
未命名变量(Java 22)
对于未使用的变量、catch 参数和 lambda 参数,使用 _。
// CORRECT
try { ... } catch (Exception _) { ... }
try (var _ = ScopedValue.where(KEY, value)) { ... }
list.stream().map(_ -> "constant").toList();
// WRONG
try { ... } catch (Exception ignored) { ... }
try { ... } catch (Exception e) { ... } // when e is never used
不可变集合
使用工厂方法创建不可变集合。对于固定集合,绝不要使用 Arrays.asList() 或 new ArrayList<>()。
// CORRECT
var roles = List.of("ADMIN", "USER");
var config = Map.of("timeout", 30, "retries", 3);
var tags = Set.of("java", "backend");
// WRONG
var roles = Arrays.asList("ADMIN", "USER");
var roles = new ArrayList<>(List.of("ADMIN", "USER"));
var roles = Collections.unmodifiableList(Arrays.asList("ADMIN", "USER"));
有序集合(Java 21)
使用 getFirst()、getLast()、reversed(),不要进行索引运算。
// CORRECT
var first = list.getFirst();
var last = list.getLast();
var rev = list.reversed();
// WRONG
var first = list.get(0);
var last = list.get(list.size() - 1);
var rev = new ArrayList<>(list); Collections.reverse(rev);
字符串工具(Java 11+)
使用现代 String 方法。不要重新实现标准库已经提供的功能。
// CORRECT
str.isBlank() // replaces str.trim().isEmpty()
str.strip() // Unicode-aware trim
"ha".repeat(3) // "hahaha"
"Hello %s".formatted(name) // replaces String.format(...)
str.lines() // Stream<String> of lines
// WRONG
str.trim().isEmpty()
String.format("Hello %s", name)
空值安全
**系统内部不允许出现 null。**默认情况下,所有内容都非 null。不要让 null 超出系统边界(外部 API、数据库结果、用户输入)。
- 不要给所有内容都加上
@NonNull注解——这会污染代码。应将所有内容默认视为非 null。 - 仅在确实可能为 null 时使用
@Nullable(这种情况很少见,通常出现在系统边界)。 - 如果从外部来源接收到 null,应在边界处将其转换为
Optional、默认值,或尽早抛出异常。 - **绝不要将
null作为方法参数传递。**方法绝不要返回null。对于可能没有结果的方法,使用Optional。
// CORRECT — Optional for methods that may return nothing
public Optional<User> findByEmail(String email) {
return Optional.ofNullable(repository.get(email));
}
// WRONG — returning null
public User findByEmail(String email) {
return repository.get(email); // might return null
}
可选类型
- 对于可能没有结果的公共方法,将
Optional用作返回类型。它能表达这样的意图:“此方法可能什么都不返回,你必须对此进行处理。” - 绝不要将
Optional用作字段类型、方法参数类型或集合中的元素类型。 - 绝不要未经检查就调用
Optional.get()。请使用orElseThrow()、orElse()、map()、flatMap()、ifPresent()。
// CORRECT
userService.findByEmail(email)
.map(User::username)
.orElseThrow(() -> new UserNotFoundException(email));
// WRONG
User user = userService.findByEmail(email).get();
错误处理
使用 Java 的异常机制。 不要试图把 Java 变成 Go 或 C,用错误返回码或 Result 包装类型来处理错误。Java 有受检异常和非受检异常,请正确使用。
- 对于编程错误和意外故障,使用非受检异常(继承
RuntimeException)。 - 谨慎使用受检异常,仅在调用方确实必须处理失败且有合理恢复途径时使用。
- 绝不要吞掉异常。 始终记录日志或重新抛出。
- 除非位于顶层错误边界(例如,控制器通知或全局处理器),否则绝不要笼统地捕获
Exception或Throwable。 - 使用具体的异常类型。标准异常不适用时,创建自定义领域异常。
- 使用多重捕获,避免重复的 catch 代码块。
// CORRECT
catch (IOException | SQLException e) {
log.error("Data access failed", e);
}
// WRONG — duplicated handlers
catch (IOException e) { log.error("...", e); }
catch (SQLException e) { log.error("...", e); }
// CORRECT
public User getUser(UserId id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
// WRONG — returning null on error
public User getUser(UserId id) {
try {
return userRepository.findById(id).orElse(null);
} catch (Exception e) {
return null;
}
}
命名约定
- 类:使用
PascalCase,采用名词(UserService、PaymentProcessor)。 - 方法:使用
camelCase,采用动词(findByEmail、processPayment、calculateTotal)。 - 变量:使用
camelCase,采用名词(userId、orderItems)。 - 常量:使用
UPPER_SNAKE_CASE(MAX_RETRY_COUNT、DEFAULT_TIMEOUT)。 - 包:使用小写字母,以点分隔(
com.company.project.domain.user)。 - 布尔值:使用
is、has、should、can作为前缀(isValid()、hasPermission()、shouldRetry())。 - 集合:使用复数名称(
users、orders、products)。 - 公共 API 中不要使用缩写。使用
userRepository,而不是userRepo;使用transactionManager,而不是txMgr。 - 方法不应有令人意外的副作用。方法名称必须准确描述其行为。
代码风格与格式
- 在整个项目中使用统一的风格:可以选择标准 Java 编码规范,也可以选择 IntelliJ 默认风格。具体选择并不重要,重要的是保持一致。
- 默认将类设为包私有。仅将必要内容设为
public。 - 需要保持不可变时,对变量使用
final。 - 尽可能使用方法引用,而不是 lambda:使用
User::userId,而不是u -> u.userId()。 - 整理导入:标准库、第三方库、内部包。不要使用通配符导入。
- 如果项目包含 Spotless,每次更改代码后都运行格式化程序(仅针对已编辑的文件)。Gradle 使用:
./gradlew spotlessApply。Maven 使用:mvn spotless:apply。
流与集合
使用 Java Streams 转换集合。优先使用 filter()、map()、flatMap()、toList()。
// CORRECT
var activeEmails = users.stream()
.filter(User::isActive)
.map(User::email)
.toList();
// WRONG — manual loop for simple transformation
var activeEmails = new ArrayList<String>();
for (User user : users) {
if (user.isActive()) {
activeEmails.add(user.getEmail());
}
}
不要在中间操作(如 map、filter、flatMap)中执行副作用——这些操作应为纯函数。终端操作 forEach 才是执行副作用的合适位置。如果不需要中间操作,优先使用 Iterable.forEach(),而不是 stream().forEach()。
// WRONG — side effect in intermediate operation
users.stream()
.map(user -> { emailService.send(user); return user.email(); })
.toList();
// WRONG — unnecessary stream() when there is no pipeline
users.stream().forEach(user -> emailService.send(user));
// CORRECT — Iterable.forEach for simple side effects
users.forEach(user -> emailService.send(user));
// CORRECT — pure filter, side effect only in terminal forEach
users.stream()
.filter(User::isActive)
.forEach(user -> emailService.send(user));
// CORRECT — plain loop when you need break/continue or checked exceptions
for (var user : users) {
emailService.send(user);
}
测试
统一的测试命名
为整个项目选择一种命名约定并严格遵守。推荐格式:
methodName_shouldExpectedBehavior_whenCondition
示例:
@Test
void findByEmail_shouldReturnUser_whenUserExists() { ... }
@Test
void findByEmail_shouldThrowNotFoundException_whenUserDoesNotExist() { ... }
@Test
void processPayment_shouldReturnSuccess_whenBalanceSufficient() { ... }
不要在同一项目中混用 camelCase、snake_case、given/when/then 和 should 风格。一致性比具体采用哪种约定更重要。
测试结构
- 默认使用 JUnit 5 作为测试框架。
- 如果团队偏好 BDD 风格的规范测试,Spock Framework(基于 Groovy)也是可接受的替代方案。它功能强大,但需要熟悉 Groovy。
- 遵循 Arrange-Act-Assert(在 Spock 中使用 Given-When-Then)。
- 每项测试应只测试一种行为。不要编写覆盖无关行为的多断言测试。
- 谨慎使用
@DisplayName——命名良好的测试方法本身就能清楚说明用途。
TDD
测试驱动开发能打造更好的 API。先编写测试,会迫使你在实现之前设计好公共接口。即使不严格遵循 TDD,也应在写完方法签名后、编写实现代码之前立即编写测试。
测试独立性
测试必须相互独立且可重复。测试之间不得共享可变状态,也不得依赖执行顺序。
并发
虚拟线程(Project Loom)
对 I/O 密集型并发工作使用虚拟线程。对于大多数使用场景,虚拟线程无需借助响应式框架。
// CORRECT — Virtual Threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var future = executor.submit(() -> fetchData(url));
return future.get();
}
// WRONG — reactive chains for simple I/O
Mono.fromCallable(() -> fetchData(url))
.subscribeOn(Schedulers.boundedElastic())
.flatMap(data -> process(data))
.subscribe();
不可变性有助于安全并发
最佳并发策略是采用规范的 OOP:使用小型、不可变的对象,将逻辑拆分到作用域有限的小方法中。小对象创建和垃圾回收都很快,能减少争用。
结构化并发(Java 25)
使用 StructuredTaskScope 将派生子任务的生命周期作为一个整体进行管理。这样可以确保作用域关闭时,子任务能够可靠地取消并加入。
// CORRECT
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var user = scope.fork(() -> fetchUser(id));
var order = scope.fork(() -> fetchOrder(id));
scope.join().throwIfFailed();
return new Response(user.get(), order.get());
}
// WRONG — manual executor management, no automatic lifecycle
var executor = Executors.newFixedThreadPool(2);
var f1 = executor.submit(() -> fetchUser(id));
var f2 = executor.submit(() -> fetchOrder(id));
executor.shutdown();
return new Response(f1.get(), f2.get());
不要使用什么
- 不要使用 RxJava。 它过于复杂,难以调试,而且有了虚拟线程后也没有必要。如果项目正在使用 RxJava,不要再引入更多 RxJava。
- 如果虚拟线程能够按顺序表达相同逻辑,就避免使用
CompletableFuture链。 - 不要过早引入响应式模式。只有在确认存在背压需求时才使用。
现代 I/O
使用 Files 便捷方法和 Path.of() 工厂方法。对于简单操作,避免编写 BufferedReader/Writer 样板代码。
// CORRECT
String content = Files.readString(Path.of("config.json"));
Files.writeString(Path.of("output.txt"), data);
var path = Path.of("src", "main", "java");
// WRONG
var br = new BufferedReader(new FileReader("config.json"));
// ... manual read loop ...
var path = Paths.get("src", "main", "java");
使用内置的 HttpClient 发起 HTTP 调用。不要添加 HttpURLConnection 样板代码。
// CORRECT
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create(url)).GET().build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
// WRONG — HttpURLConnection with manual stream handling
var conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod("GET");
var reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
// ...
日期和时间
始终使用 java.time(Java 8+)。绝不要使用 Date、Calendar 或 SimpleDateFormat。
// CORRECT
LocalDate today = LocalDate.now();
LocalDate date = LocalDate.of(2025, Month.JANUARY, 15);
Instant now = Instant.now();
long days = ChronoUnit.DAYS.between(start, end);
Duration duration = Duration.ofMinutes(30);
// WRONG
Date date = new Date();
Calendar cal = Calendar.getInstance();
cal.set(2025, 0, 15); // zero-indexed months
日志记录
- 使用 SLF4J(
org.slf4j.Logger)。绝不要使用System.out.println()或System.err.println()。 - 使用参数化日志记录,绝不要进行字符串拼接。
// CORRECT
log.info("Processing order {} for user {}", orderId, userId);
// WRONG
log.info("Processing order " + orderId + " for user " + userId);
- 日志级别:
error— 需要关注的异常和故障。warn— 可恢复的问题、降级行为。info— 重要的业务事件(下单、处理付款)。debug— 用于故障排查的详细技术信息。trace— 非常细粒度的信息,通常在生产环境中禁用。
项目结构
- 在包结构中将领域代码与基础设施代码分离。
- 将配置类放在专门的
config包中。 - 遵循一致的包命名规则:
com.company.project.<domain>.<layer>(例如,com.acme.shop.order.repository)。 - 每个文件只包含一个公共类。除非内部类紧密耦合,否则不要在一个文件中定义多个类。
JVM 和 GC
- 除非测量确认存在问题,否则不要调整 GC 参数。 默认 GC(现代 JVM 中为 G1GC)足以满足绝大多数应用的需求。
- 编写规范的 OOP,使用小型、短生命周期的对象,胜过任何 GC 调优。
- 优化前先进行性能分析。使用 JFR(Java Flight Recorder)和 JMC(Java Mission Control)进行性能分析。
已弃用模式 — 请勿使用
这些模式已过时,不得用于新代码:
| 已弃用模式 | 现代替代方案 |
|---|---|
用于数据类的 Lombok @Value、@Data、@Getter、@Setter | record |
Arrays.asList() | List.of() |
new ArrayList<>(Arrays.asList(...)) | List.of() 或 new ArrayList<>(List.of(...)) |
Collections.unmodifiableList() | List.copyOf() 或 List.of() |
| 用于简单 I/O 的 RxJava / Project Reactor | 虚拟线程 |
带有 break 的旧式 switch 语句 | Switch 表达式 |
手动使用 instanceof 并进行强制转换 | instanceof 模式匹配 |
使用 + 拼接多行字符串 | 文本块 |
| 用于函数式接口的匿名内部类 | Lambda / 方法引用 |
用于简单场景的 synchronized 代码块 | java.util.concurrent 锁或虚拟线程 |
方法返回 null | Optional 或抛出异常 |
System.out.println() | SLF4J 日志记录 |
原始类型(如使用 List 而非 List<String>) | 始终使用泛型 |
Date、Calendar | java.time API(LocalDate、Instant 等) |
StringBuffer | StringBuilder(或文本块 / formatted()) |
str.trim().isEmpty() | str.isBlank() |
String.format(...) | "...".formatted(...) |
list.get(list.size() - 1) | list.getLast() |
Collections.reverse(new ArrayList<>(list)) | list.reversed() |
catch (Exception ignored) | catch (Exception _) |
重复的 catch 代码块 | 多重捕获 catch (A | B e) |
使用 BufferedReader 和循环读取文件 | Files.readString(Path.of(...)) |
Paths.get(...) | Path.of(...) |
HttpURLConnection | HttpClient(Java 11) |
| 为结构化任务手动管理执行器生命周期 | StructuredTaskScope(Java 25) |