java-contract

为项目建立并执行 Java 编码契约:把需求没说明的边界决策(无结果 / null 参数 / 集合里的 null / 重复 key / 结果顺序与可变性 / 非法参数 / 静态还是实例 / 重复到达)落成 .claude/rules/java-contract.md, 动手前先读、写完编译验证;审存量代码时按失效模式七组追问。 在编写、修改、审查 Java 代码时使用;也用于决定某个方法该返回 n

SKILL.md

GitHub

Java 编码契约

模型对 JDK 写法的判断已经够准(实测语法陷阱 3/3、7/7 全部避开),API 字典类内容不必再写。真正的分歧在 需求文档不写、几个实现会给几种答案的地方。本技能只做三件事:把这些决策定下来、动手前读一遍、写完验证。

两条原则

  1. 契约先落盘,用前先读。 第一次在项目里写 Java 时,把八项边界决策写进 .claude/rules/java-contract.md; 之后每次动 .java 前先读它。它不会自动进上下文——实测里一个被要求不浏览其他文件的 agent 虽然有这份契约,仍按自己的判断把该保留的记录丢了。不落盘的决策等于没定,落了盘没人读同样等于没定。 读不到它时(文件不存在、只能看片段、子代理拿不到仓库),按本技能默认值写,并在回复里点明 「按 java-contract 默认,未读到项目契约」。分歧摆在明面上,比静默按自己的判断做要好。
  2. 写完即编译。 目标版本从 pom.xml 的 maven.compiler.release / <source> / sourceCompatibility / java.toolchain 读,四者都读不到才问一次。一个类写完就 mvn -q compile(Gradle:./gradlew compileJava)。 版本不匹配在这一步暴露,比背着规则表写 200 行再让 CI 拦下来便宜。

第一步:读项目

三件事都在仓库里,做完再动笔:

读什么 得到什么
pom.xml / build.gradle 目标 JDK;<modules> 说明是否多模块
.claude/rules/java-contract.md 已有契约。读到才生效,读到就遵守、不重新提议
同包下 2~3 个同类方法 既有惯例:返回 Optional 还是裸值、抛异常还是错误码

项目惯例与下表默认冲突时,惯例优先。 契约的作用是消除分歧,不是推行新风格。

契约八项

需求没说明时按下表取;项目已有不同做法就记进契约文件,不在代码里另搞一套。

分歧点 默认
无结果 单条 → Optional;列表 → 空集合;聚合值看 0 是否有业务含义(求和 → 0,平均 / 最大 → Optional)。不返回 null
null 参数 必填标量 → Objects.requireNonNull,不编默认值;集合参数一律按空处理,不用 requireNonNull,业务要求非空用 isEmpty() 校验
集合中的 null 元素 null 元素跳过;字段无值看用途——累加 / 平均跳过整条记录,拼接跳过该值,排序 / 分页保留并排末尾;先剔除再切页
重复 key putIfAbsent 保留首条;要覆盖用 put 并在注释写明原因
返回集合的形态 顺序:对外返回的 Map 保插入顺序(groupingBy 传 LinkedHashMap::new),字典序用 TreeMap。可变性:默认不可变;Map.copyOf 不保序,保序 Map 用 unmodifiableMap(new LinkedHashMap<>(x));8 用 unmodifiableList(new ArrayList<>(x))
非法参数 展示层容错(pageNo < 1 纠正为 1),领域层 fail fast(状态码不认识就抛)。抛哪种异常见 references/01-contract.md 第五节
静态还是实例 纯函数 static;依赖注入用实例;同一个类不混用
重复到达 重复请求 → 返回上次结果、不重复执行、不抛异常(抛了会重投);已终态(CLOSED / REFUNDED)又收到成功通知 → 默认不复活、不加权益,但落一条可查询的待处理记录(只打日志不算),返回成功

怎么定位到哪一项:对每个新增的 public 方法问六句,答「是」的才受契约约束——

  1. 入参为空时,结果是「没有」还是「就是 0」?→ 第一节(无结果)
  2. 哪个参数为 null 时方法无意义?→ 第二节(null 参数)
  3. 入参或中间结果里有集合吗?→ 第三节(集合里的 null 元素)
  4. 有没有从外部数据构造 Map、或按 key 分组汇总?→ 第四节(重复 key)、第七节(顺序与可变性)
  5. 有分页、条数、状态码这类外部数值或枚举参数吗?→ 第五节(非法参数)
  6. 这是写操作吗?同一个请求可能到达两次(外部重试、用户连点、回调重投)?→ 第八节(重复到达)

六句都不成立(纯计算、无集合入参、不写数据)就不必套模板。静态还是实例这一项不看单个方法,建类时定一次、 类内统一。每项的识别判据、两项冲突时听谁,见 references/01-contract.md。

落盘:模板 assets/contract-template.md → .claude/rules/java-contract.md,paths 固定 "**/*.java",首行标记 <!-- seeded: java-contract@⟨版本⟩ -->,⟨版本⟩ 填本文件 frontmatter 的 version。 文件已存在且无该标记,说明是用户自己写的——只补缺失事实,不改原文。

加一道保险:把最通用的四五条同步写进类的 javadoc(集合参数 null 与空等价、集合里 null 元素跳过、 无列表结果返回空集合、返回集合不可变)。实测这四项写在 javadoc 里就能让没读到契约文件的实现者做对; 细粒度规则(字段无值按用途三分、先剔除再切页)留契约文件——那部分写在 javadoc 里会喧宾夺主, 而它恰恰是必须读到契约才做得对的一条。

写完即验证

  1. 编译:mvn -q compile。版本问题在这一步暴露,别留到 CI。
  2. 契约相关分支至少跑一次:空入参、无结果、重复 key 三类各一个调用或用例。没有测试的,说明原因。

编译报错时:cannot find symbol 且 symbol 是 class var、或方法名属于 JDK 9+ API(List.of、 isBlank、Stream.toList)时,先怀疑版本,不要顺着报错去补 import 或改名——那会把版本问题改成一堆 无用代码。record / 文本块 / switch 表达式会直接提示 are not supported in -source N,按提示换写法。 一律改代码,不改 maven.compiler.release,那只会让产物在目标运行时 UnsupportedClassVersionError。

审查(存量代码,独立动作)

靶子是已有的、别人写的代码。刚生成的新代码不必逐条对照——这些坑生成时基本不犯,对照只是消耗上下文。 审存量时读 references/02-review.md,按它的七组顺序逐组用一句问题去追代码路径:数据是否在两处各算一遍、 状态是否还有第二份副本、信任边界是否被绕过、什么会随运行时间累积。 不要按 API 分类扫(数值一组、字符串一组)——那会把范围收窄到分组枚举过的写法,最贵的问题往往落在组外。 审查与生成分开做:写完立刻自审会顺着自己的思路看,最容易漏的是「返回契约与调用方期望不一致」。 可交给子代理执行,rubric 用项目里的契约文件。

库边界

读 pom.xml 一次,只在下表能力缺失且任务确实需要时推荐一次:

能力 缺失时推荐
判空 / 集合 / 字符串 / 日期 国内栈 cn.hutool:hutool-bom;Spring 国际栈 commons-lang3
Bean 映射 org.mapstruct:mapstruct(编译期生成)
加密 / 哈希 cn.hutool:hutool-crypto
JSON / HTTP 跟随项目既有栈;都没有则 Jackson / OkHttp

只给坐标加一句理由,不给 API 示例。用户拒绝就用 JDK 原生,不追问第二次。

使用流程

  1. 读项目:pom 确认目标 JDK;.claude/rules/java-contract.md 有就先读(它决定契约);同包 2~3 个同类方法看惯例。
  2. 无契约文件时按「契约八项」提议落盘,等用户确认。
  3. 用六问定位本次方法落在哪几项,定下返回契约,再动手。
  4. 写代码;编译报错先按「编译报错时」判版本,再改语法。
  5. 写完编译,契约分支跑一次。
  6. 要审存量代码时按 references/02-review.md 独立执行。