java-contract
为项目建立并执行 Java 编码契约:把需求没说明的边界决策(无结果 / null 参数 / 集合里的 null / 重复 key / 结果顺序与可变性 / 非法参数 / 静态还是实例 / 重复到达)落成 .claude/rules/java-contract.md, 动手前先读、写完编译验证;审存量代码时按失效模式七组追问。 在编写、修改、审查 Java 代码时使用;也用于决定某个方法该返回 n
SKILL.mdskills/java-contract/SKILL.md
GitHubJava 编码契约
模型对 JDK 写法的判断已经够准(实测语法陷阱 3/3、7/7 全部避开),API 字典类内容不必再写。真正的分歧在 需求文档不写、几个实现会给几种答案的地方。本技能只做三件事:把这些决策定下来、动手前读一遍、写完验证。
两条原则
- 契约先落盘,用前先读。 第一次在项目里写 Java 时,把八项边界决策写进
.claude/rules/java-contract.md; 之后每次动.java前先读它。它不会自动进上下文——实测里一个被要求不浏览其他文件的 agent 虽然有这份契约,仍按自己的判断把该保留的记录丢了。不落盘的决策等于没定,落了盘没人读同样等于没定。 读不到它时(文件不存在、只能看片段、子代理拿不到仓库),按本技能默认值写,并在回复里点明 「按 java-contract 默认,未读到项目契约」。分歧摆在明面上,比静默按自己的判断做要好。 - 写完即编译。 目标版本从
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 方法问六句,答「是」的才受契约约束——
- 入参为空时,结果是「没有」还是「就是 0」?→ 第一节(无结果)
- 哪个参数为
null时方法无意义?→ 第二节(null 参数) - 入参或中间结果里有集合吗?→ 第三节(集合里的 null 元素)
- 有没有从外部数据构造
Map、或按 key 分组汇总?→ 第四节(重复 key)、第七节(顺序与可变性) - 有分页、条数、状态码这类外部数值或枚举参数吗?→ 第五节(非法参数)
- 这是写操作吗?同一个请求可能到达两次(外部重试、用户连点、回调重投)?→ 第八节(重复到达)
六句都不成立(纯计算、无集合入参、不写数据)就不必套模板。静态还是实例这一项不看单个方法,建类时定一次、
类内统一。每项的识别判据、两项冲突时听谁,见 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 里会喧宾夺主,
而它恰恰是必须读到契约才做得对的一条。
写完即验证
- 编译:
mvn -q compile。版本问题在这一步暴露,别留到 CI。 - 契约相关分支至少跑一次:空入参、无结果、重复 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 原生,不追问第二次。
使用流程
- 读项目:pom 确认目标 JDK;
.claude/rules/java-contract.md有就先读(它决定契约);同包 2~3 个同类方法看惯例。 - 无契约文件时按「契约八项」提议落盘,等用户确认。
- 用六问定位本次方法落在哪几项,定下返回契约,再动手。
- 写代码;编译报错先按「编译报错时」判版本,再改语法。
- 写完编译,契约分支跑一次。
- 要审存量代码时按
references/02-review.md独立执行。