mcpField
J
java-best-practices

java-best-practices

github.com/virtuslab开发777 次安装34 周前更新
代码仓库

关于 java-best-practices

面向 Java 开发者和编码 Agent 的实践技能,聚焦 Java 21+,并明确排除过时写法。内容涵盖设计、实现、错误处理与测试。

主要功能

  1. 01Java 21+ 实践指导
  2. 02组合方法与组合优先
  3. 03Record 与密封类型
  4. 04空值与异常处理
  5. 05流、集合与测试

适用场景

  1. 01按现代实践审查 Java 代码
  2. 02实现 Java 21+ 特性
  3. 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、@Setterrecord
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 锁或虚拟线程
方法返回 nullOptional 或抛出异常
System.out.println()SLF4J 日志记录
原始类型(如使用 List 而非 List<String>)始终使用泛型
Date、Calendarjava.time API(LocalDate、Instant 等)
StringBufferStringBuilder(或文本块 / 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(...)
HttpURLConnectionHttpClient(Java 11)
为结构化任务手动管理执行器生命周期StructuredTaskScope(Java 25)

评论

支持 Markdown