关于 java-coding-guideline
java-coding-guideline 是面向 Java 开发者的编码规范技能,覆盖代码风格、空值处理、资源管理及异常与断言的选择。它还强调在外部边界和内部不变量场景中采用不同的失败处理方式。
主要功能
- 01明确导入与垂直间距规范
- 02空值注解与基本类型选择
- 03构造器、不可变对象与组合规范
- 04资源管理与异常捕获规则
- 05按信任边界选择异常或断言
适用场景
- 01统一 Java 项目的代码评审标准
- 02编写空值与资源处理逻辑
- 03判断输入校验应使用异常还是断言
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ gabia/ agent-skills --skill java-coding-guideline安装后在 Claude Code 中输入/java-coding-guideline确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/async.mdjavadoc.mdlombok.mdsecurity-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 时:
- 让
close()具有幂等性(可安全地多次调用) - 绝不从
close()抛出异常(会产生抑制异常问题) - 记录错误,而不是抛出错误
✅ 应该这样做
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) | Instant | API 时间戳、数据库存储 |
| 用户显示 | ZonedDateTime | UI 中显示的时间 |
| 仅日期 | 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 注释应针对需要立即处理的任务
- 避免添加重复说明代码内容的不必要注释
另请参阅
参考指南
如需深入了解特定主题,请参阅:
- references/async.md - CompletableFuture 和异步编程模式
- references/javadoc.md - Javadoc 约定和异常文档说明
- references/lombok.md - 允许和禁止使用的 Lombok 注解
- references/security-resources.md - 安全最佳实践、密码处理、SQL 注入防范、PII 脱敏