mcpField
J
java-coding-guideline

java-coding-guideline

github.com/gabia开发2151 次安装14 周前更新
代码仓库

关于 java-coding-guideline

java-coding-guideline 是面向 Java 开发者的编码规范技能,覆盖代码风格、空值处理、资源管理及异常与断言的选择。它还强调在外部边界和内部不变量场景中采用不同的失败处理方式。

主要功能

  1. 01明确导入与垂直间距规范
  2. 02空值注解与基本类型选择
  3. 03构造器、不可变对象与组合规范
  4. 04资源管理与异常捕获规则
  5. 05按信任边界选择异常或断言

适用场景

  1. 01统一 Java 项目的代码评审标准
  2. 02编写空值与资源处理逻辑
  3. 03判断输入校验应使用异常还是断言

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/gabia/agent-skills --skill java-coding-guideline

安装后在 Claude Code 中输入/java-coding-guideline确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
async.md
javadoc.md
lombok.md
security-resources.md

详细介绍

Java 编码指南

重要:除非更具体的技能明确覆盖,否则这些规则适用于所有 Java 代码。

导入语句

始终使用显式导入,不使用通配符,以提高代码的清晰度和可维护性。

✅ 推荐
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
❌ 不推荐
import static org.assertj.core.api.Assertions.*;

垂直间距(可读性)

在类中保持一致的垂直间距,以缩小差异并让代码更易于浏览。

规则
  • 以下内容之间插入恰好一个空行:
    • 字段块与下一个字段块之间(分组时)
    • 字段与第一个构造函数之间
    • 构造函数与方法之间
    • 方法与下一个方法之间
  • 此外,在类的左花括号 { 后立即插入一个空行(位于第一个字段或初始化器之前),除非该类有意保持为空。
✅ 推荐
public final class SomeClass {

    @NonNull
    private final String fieldA;

    @NonNull
    private final String fieldB;

    public void someFunc() {
        // some action
    }

    public String getFieldA() {
        return fieldA;
    }
}
❌ 不推荐
public final class SomeClass {
    @NonNull
    private final String fieldA;
    @NonNull
    private final String fieldB;
    public void someFunc() {
        // some action
    }
    public String getFieldA() {
        return fieldA;
    }
}

构造函数

使用单个主构造函数,并通过 this() 委托辅助构造函数。

✅ 推荐
public final class SomeClass {

    private final int field1;
    private final int field2;

    public SomeClass() {
        this(1, 2);
    }

    public SomeClass(int field1, int field2) {
         this.field1 = field1;
         this.field2 = field2;
    }
}
❌ 不推荐
public final class SomeClass {

    private final int field1;
    private final int field2;

    public SomeClass() {
        this.field1 = 1;
        this.field2 = 2;
    }

    public SomeClass(int field1, int field2) {
         this.field1 = field1;
         this.field2 = field2;
    }
}

Optional 的用法

避免将 Optional 用作返回值或参数。改用 @Nullable 注解。

✅ 推荐
public final class SomeClass {

    @Nullable
    public Integer someNullable() {
        if (something) return null;
        return 1;
    }

    public void func(@Nullable Integer param) {
        // do something
    }
}
❌ 不推荐
public final class SomeClass {

    @NonNull
    public Optional<Integer> someNullable() {
        if (something) return Optional.ofNullable(null);
        return Optional.ofNullable(1);
    }

    public void func(@NonNull Optional<Integer> param) {
        // do something
    }
}

包装类型与基本类型

  • 预期值可能为 null 时,使用包装类型
  • 不应出现 null 时,使用基本类型
✅ 推荐
public final class SomeClass {

    private final int field1;

    @Nullable
    private final Long field2;

    public SomeClass(int field1, @Nullable Long field2) {
        this.field1 = field1;
        this.field2 = field2;
    }
}
❌ 不推荐
public final class SomeClass {

    @NonNull
    private final Integer field1;

    @Nullable
    private final Long field2;

    public SomeClass(@NonNull Integer field1, @Nullable Long field2) {
        this.field1 = field1;
        this.field2 = field2;
    }
}

Null 比较

  • 一般情况下使用 A == null
  • 在 Stream API 或函数式编程场景中使用 Objects.isNull
✅ 推荐
final Integer someVariable = null;

if (someVariable == null) {
    // do something
}

List<Integer> lists = new ArrayList<>();
lists.stream()
  .filter(Objects::isNull)
  .collect(Collectors.toList());
❌ 不推荐
final Integer someVariable = null;

if (Objects.isNull(someVariable)) {
    // do something
}

List<Integer> lists = new ArrayList<>();
lists.stream()
  .filter(p -> p == null)
  .collect(Collectors.toList());

Nullability 注解

  • 使用 JSpecify 注解
  • 对所有方法(包括私有方法)应用 @Nullable 和 @NonNull
  • 避免使用类级别或包级别的注解(存在 Lombok 兼容性问题)
✅ 推荐
public final class SomeClass {

    @Nullable
    private final Integer field1;

    @NonNull
    private final String field2;

    @NonNull
    public String func() {
        return process();
    }

    @Nullable
    private String process() {
        if (field1 == null) return null;
        return "awesome";
    }
}
❌ 不推荐
public final class SomeClass {

    private final Integer field1;
    private final String field2;

    public String func() {
        return process();
    }

    private String process() {
        if (field1 == null) return null;
        return "awesome";
    }
}

包命名

仅使用小写字母和数字,并遵循 Google Java Style Guide 约定。

✅ 推荐
package com.example.project.initscript;
❌ 不推荐
package com.example.project.initScript;

静态工厂方法

仅使用 of 命名约定。对于多种类型,使用 of<Postfix> 格式。

✅ 推荐
public final class SomeClass {

    private final int field1;

    private SomeClass(int field1) {
        this.field1 = field1;
    }

    public static SomeClass ofDefault() {
        return new SomeClass(1);
    }

    public static SomeClass ofSpecial() {
        return new SomeClass(2);
    }
}
❌ 不推荐
public final class SomeClass {

    private final int field1;

    private SomeClass(int field1) {
        this.field1 = field1;
    }

    public static SomeClass from() {
        return new SomeClass(1);
    }

    public static SomeClass valueOf() {
        return new SomeClass(2);
    }
}

资源管理

对 AutoCloseable 资源始终使用 try-with-resources。切勿依赖终结器。

Try-With-Resources
✅ 推荐
public String readFile(@NonNull Path path) throws IOException {
    try (var reader = Files.newBufferedReader(path)) {
        return reader.lines().collect(Collectors.joining("\n"));
    }
}

// Multiple resources
public void copyStream(@NonNull InputStream in, @NonNull OutputStream out) throws IOException {
    try (in; out) {  // Java 9+: can use already-declared variables
        in.transferTo(out);
    }
}
❌ 不推荐
public String readFile(@NonNull Path path) throws IOException {
    BufferedReader reader = Files.newBufferedReader(path);
    try {
        return reader.lines().collect(Collectors.joining("\n"));
    } finally {
        reader.close();  // The original exception may be suppressed if one occurs here
    }
}

优先使用组合而非继承

优先使用组合而非继承。

✅ 推荐
public final class SomeFeature {
    public void func() { /* ... */ }
}

public final class SomeClass {

    private final SomeFeature feature;

    public void func2() {
        feature.func();
        // do something
    }
}
❌ 不推荐
public class SomeFeature {
    public void func() { /* ... */ }
}

public final class SomeClass extends SomeFeature {

    public void func2() {
        super.func();
        // do something
    }
}

不可变对象

除非扩展需求或框架要求另有规定,否则所有对象都应使用 final 关键字。

✅ 推荐
public final class SomeClass {
    // ...
}
❌ 不推荐
public class SomeClass {
    // ...
}

断言与异常

根据信任边界和调用方承担的恢复责任,选择使用断言还是异常。应针对每个条件应用此规则,而不是针对整个类或方法。

不要根据 Java 访问修饰符或组件名称推断边界。public 方法可能是内部 API,而 private 解析器也可能处理外部输入。

决策表
条件机制
无效的外部请求、命令、消息或公共 API 参数抛出异常
无效的运行时配置抛出异常
网络、数据库、文件系统或外部服务故障抛出异常
安全、授权或数据完整性检查抛出异常
调用方必须转换、重试、跟踪、记录日志或以其他方式处理故障抛出异常
内部 API 中由程序员控制的前置条件或后置条件使用 assert
由内部连接或分派保证的关系使用 assert
实现正确时不可能到达的状态使用 assert
在外部边界抛出异常

如果在有效的运行时操作期间可能发生故障,或者调用方必须能够对此作出响应,就应抛出异常。

外部边界包括:

  • HTTP、RPC、XML-RPC、CLI、消息和事件处理器
  • 由独立开发的使用方调用的公共库 API
  • 用户或租户提供的输入
  • 运行时配置
  • 正常操作期间可能出现的网络、数据库、文件系统或外部服务中的意外或无效数据

即使启用了断言,也要使用异常。外部可见的行为不得取决于 JVM 的 -ea 选项。

选择能清楚表达故障的异常类型:

  • 调用方提供的参数无效时,使用 IllegalArgumentException
  • 当前运行时状态无法满足操作要求时,使用 IllegalStateException
  • 对于调用方必须加以区分的操作故障,使用领域专用异常
  • 有意将 I/O 故障作为非检查异常公开时,使用 UncheckedIOException

使用 @throws 文档说明外部可见的异常。

断言内部不变量

对于内部 API 中由程序控制的前置条件、后置条件和不变量,请使用 assert。这包括使用可信应用数据的内部发布器、生成器、转换器和解析器。

常见的断言场景包括:

  • 内部解析器必须生成非空白键
  • 生成器或转换器必须生成非空结果
  • 相关领域对象必须相互引用
  • 持久化操作必须填充 ID
  • 插入后立即执行的查询必须返回已插入的实体
  • 拦截器必须已填充内部上下文值
  • 内部调度正确时,某个分支必须不可达
public void publish(@NonNull String objectKey, @NonNull String generatedScript) {
    assert !objectKey.isBlank();
    assert !generatedScript.isBlank();

    objectStore.put(objectKey, generatedScript);
}

断言在生产环境中可能被禁用。因此:

  • 绝不能使用断言验证外部输入
  • 绝不能使用断言执行授权或安全检查
  • 绝不能对需要运行时恢复的条件使用断言
  • 绝不能在断言表达式中加入副作用
  • 绝不能将 AssertionError 作为正常控制流捕获
  • 确保断言禁用时,代码仍然正确且安全

如果不变量不明显,请添加断言消息。

assert collector.getAgentId() == agent.getId() : "collector must own the agent";
按条件应用规则

同一个方法中可以同时包含断言和异常。

public void resize(@NonNull Pool pool, @NonNull Bucket bucket, long requestedSize, long minimumSize) {
    assert pool.getId() == bucket.getPoolId();
    assert pool.getType() == PoolType.OBJECT_STORAGE;

    if (requestedSize < minimumSize) {
        throw new IllegalArgumentException("requested size is smaller than the minimum size");
    }
}

池之间的关系是内部路由不变量。请求的大小是运行时参数,调用方可能会合理地错误提供该参数。

对于 S3 上传失败、数据库错误或网络超时等操作故障,内部 API 仍必须抛出异常。仅仅因为 API 是内部 API,并不意味着预期的运行时故障就应变成断言。

验证一次,然后在内部断言

在边界处使用异常验证外部数据。将其转换为可信的内部数据后,使用断言记录传递下来的不变量。

解析器遵循相同规则:

  • 解析外部或不可信内容时,抛出解析异常
  • 解析由同一应用生成并验证的内容时,断言相关假设
异常日志记录

抛出异常并不意味着每一层都应记录该异常。保留原始原因并传播异常,直到某一层能够处理、转换、重试或终止该操作。在该处理边界记录一次异常,而不是在每一层都记录后重新抛出。


捕获异常

只捕获必要的最少异常。

✅ 应该这样做
try {
    // do something
} catch (IllegalArgumentException e) {
    // do catch
}
❌ 不要这样做
try {
    // do something
} catch (RuntimeException e) {
    // do catch
}
实现 AutoCloseable

实现 AutoCloseable 时:

  1. 让 close() 具有幂等性(可安全地多次调用)
  2. 绝不从 close() 抛出异常(会产生抑制异常问题)
  3. 记录错误,而不是抛出错误
✅ 应该这样做
public final class DatabaseConnection implements AutoCloseable {

    @NonNull
    private final Connection connection;

    private volatile boolean closed = false;

    @Override
    public void close() {
        if (closed) return;  // Idempotent: safe to call multiple times

        closed = true;
        try {
            connection.close();
        } catch (SQLException e) {
            // Log only; do not throw (exceptions in close can suppress the original)
            logger.warn("Failed to close connection", e);
        }
    }
}
❌ 不要这样做
public final class DatabaseConnection implements AutoCloseable {

    @NonNull
    private final Connection connection;

    @Override
    public void close() throws SQLException {
        connection.close();  // Not idempotent; exception propagates
    }
}

日期/时间 API

仅使用 java.time(JSR-310)。新代码中禁止使用 java.util.Date 和 java.util.Calendar。

类型选择
使用场景类型示例
存储/传输(UTC)InstantAPI 时间戳、数据库存储
用户显示ZonedDateTimeUI 中显示的时间
仅日期LocalDate生日、到期日期
仅时间LocalTime营业时间、闹钟
时间间隔Duration / Period已用时间、日期差
✅ 应该
public final class Event {

    @NonNull
    private final Instant createdAt;  // UTC timestamp

    @NonNull
    private final LocalDate eventDate;  // Date only

    @NonNull
    private final ZoneId timeZone;  // Display time zone

    @NonNull
    public ZonedDateTime displayTime() {
        return createdAt.atZone(timeZone);
    }
}

格式化器重用(线程安全):

public final class DateFormats {

    // DateTimeFormatter is thread-safe, so reuse as constants
    public static final DateTimeFormatter ISO_DATE = DateTimeFormatter.ISO_LOCAL_DATE;

    public static final DateTimeFormatter ENGLISH_DATE = DateTimeFormatter.ofPattern("MMM dd, yyyy");

    private DateFormats() {}
}
❌ 不应该
public final class Event {

    @NonNull
    private final Date createdAt;  // Do not use java.util.Date

    @NonNull
    private final Calendar calendar;  // Do not use java.util.Calendar

    @NonNull
    public String format() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");  // Not thread-safe
        return sdf.format(createdAt);
    }
}
旧版互操作(仅限边界处)

与需要 Date 的旧版 API 对接时,在边界处进行转换:

// Instant → Date (boundary layer only)
Date legacyDate = Date.from(instant);

// Date → Instant (convert immediately on receipt)
Instant instant = legacyDate.toInstant();

补充指南

建造者模式

使用 Lombok 风格的建造者,以保持 API 一致性。

变量和方法命名
  • 使用 JVM 10+ 的 var 关键字
  • 使用有意义的变量名
    • 但避免变量、函数和字段名称过长
  • 方法名应使用动词
  • 除 getter 外,避免使用 get 前缀
驼峰命名约定

对大小写混合的单词和缩写使用驼峰命名(例如,将“MultiNIC”改为“MultiNic”)。

控制流语句

对于简单的 if 语句,使用不带大括号的单行语法。

✅ 应该
if (someCondition) doSomething();
❌ 不应该
if (someCondition) {
    doSomething();
}
注释
  • 仅在特殊情况下使用注释
  • TODO 注释应针对需要立即处理的任务
  • 避免添加重复说明代码内容的不必要注释

另请参阅

参考指南

如需深入了解特定主题,请参阅:

评论

支持 Markdown