简介:这是一份华为技术团队编写的《Java语言编码规范》PDF文档,面向企业Java开发人员、代码评审人员以及需要建立团队编码标准的技术管理者。文档共29页,内容按范围、规范性引用文件、术语与定义展开,重点覆盖排版规范、注释规范、命名规范、编码规范与JTEST规范五大模块,每一模块均区分「规则」与「建议」两级要求,其中JTEST规范部分将静态检查工具的约束项与人工编码习惯对应起来,便于在持续集成环节落地。压缩包仅1个PDF文件,体积约1.12MB,轻量便于下载后快速查阅或打印。文档提供修订记录、章节编号与目录结构,可当团队内部规范模板直接引用。目前已有193人学习下载,适合希望统一代码风格、降低维护成本、提升代码可读性与评审效率的Java开发者参考。
1. 从一份 29 页 PDF 说起:华为 Java 编码规范约束的到底是什么
第一次拿到这份文档,翻目录会觉得它像一张格式检查表:排版、注释、命名、编码、JTEST,五块内容,正文只有 29 页。真拿去套一个几十万行的 Java 代码库才会发现,它管的是三件更硬的事:代码长什么样、注释写到什么颗粒度、异常和资源怎么收尾。文档把每条约束分成「规则」和「建议」两档,规则是强制必须遵守,建议是必须加以考虑,这个分档是整套规范的设计核心——把「必须过线」和「尽力而为」拆开,评审时就不会为一条格式小事吵半天。
适用范围写得很直白:用 Java 做产品与项目开发,尤其是多人协作、代码要长期维护、还要过 JTEST 静态扫描的团队。前四块管手写代码,最后一块管自动化校验,两者是一套组合拳。团队正在讨论要不要上 Checkstyle、注释率卡多少、异常代码怎么统一时,这份文档可以直接当基准。
2. 排版与命名落地:4 空格缩进、80 字符折行和包名倒置怎么配
2.1 缩进、大括号与空行的硬性约束
排版这一章几乎全是规则,没有商量余地。4.1.1 规定程序块采用缩进风格,缩进空格数为 4 个,工具自动生成的代码可以不一致。4.1.2 要求分界符{和}各独占一行、位于同一列,并与引用它们的语句左对齐,函数体、类与接口定义、if/for/do/while/switch/case全都照此办理。
// 不符合:大括号跟在语句尾部,缩进也不是 4 个空格 for (int i = 0; i < len; i++) { doSomething(i); } // 符合:大括号独占一行,并与 for 左对齐 for (int i = 0; i < len; i++) { doSomething(i); }后一种写法看着占行,但它让代码块的边界永远不会被误读,diff 里也更容易看出逻辑改动。4.1.5 补充要求if、for、do、while、case、switch、default自占一行,执行语句无论几条都要加括号;哪怕只有一句write();,也得包进{}。4.1.6 要求相对独立的程序块之间、变量说明之后加空行,4.1.7 则明确只允许空格、不允许 TAB——TAB 在不同编辑器里展开宽度不同,换台机器整个文件就散了,JBuilder、UltraEdit 里有「行首 TAB 转空格」的选项,直接打开。
2.2 80 字符折行与操作符换行位置
注意:80 字符是硬线,不是建议。长表达式要在低优先级操作符处断开,操作符放到新行行首。
// 在 && 处断行,&& 提到新行行首 if (maxValue != null && new String(config.getPath()).length() < LogConfig.getMaxLength()) { // program code } // 参数列表折行,后续行缩进到与第一个参数对齐 public static LogIterator read(String logType, Date startTime, Date endTime, int logLevel, String userName, int bufferNum) { // program code }&&的优先级低于!=和<,在它前面断开不会改变求值顺序,读起来也更顺;参数折行把后续行缩进到第一个参数的位置,是为了让「哪些是参数」一眼能数清。这两条分别由 Checkstyle 的LineLength和Indentation负责,配置见 2.4。
2.3 空格规则:双目加、单目不加
4.1.8 用一张表把空格讲清楚了,核心是「对等操作符前后加,关系密切的立即操作符后不加」。另外括号内侧不加空格,多重括号之间不加空格,连续空格不超过两个。
| 场景 | 规则 | 示例 | | 逗号、分号 | 只在后面加 | int a, b, c; | | 赋值/比较/算术/逻辑/位运算 | 双目操作符前后加 | a = b + c; | | 取反、自增自减、取地址 | 单目操作符前后不加 | flag = !isEmpty; i++; | | 点号 | 前后都不加 | p.id = pid; | | if/for/while/switch 与括号 | 关键字后加一个空格 | if (a >= b) |
2.4 命名规范的四套大小写规则
| 元素 | 规则 | 示例 | | 包名 | 域后缀倒置 + 全小写 | com.huawei.msg.relay | | 类/接口 | 每词首字母大写 | LogManager、CustomerList | | 方法 | 首词小写,其余词首字母大写 | calculateRate、addNewOrder | | 属性 | 同方法,且不能与方法同名 | customerName、orderNumber | | 常量 | 全大写 + 下划线,final static | MAX_VALUE、DEFAULT_START_DATE |
存取方法另有约定:get+ 非布尔属性名、is+ 布尔属性名、set+ 属性名,动作方法用动词或动宾结构。属性名可以与公有方法参数同名,但不能与局部变量同名,引用非静态成员用this,引用静态成员用类名。
public class Person { private String name; private static List properties; public void setName(String name) { this.name = name; // this 区分成员变量与参数 } public void setProperties(List properties) { Person.properties = properties; // 静态成员用类名引用 } }命名违规可以直接交给 Checkstyle 的PackageName、TypeName、MethodName、MemberName、ConstantName五个模块,默认规则和这套基本一致,只有包名需要额外配正则。
<module name="Checker"> <module name="LineLength"> <property name="max" value="80"/> </module> <module name="TreeWalker"> <module name="Indentation"> <property name="basicOffset" value="4"/> <property name="braceAdjustment" value="0"/> </module> <module name="LeftCurly"> <property name="option" value="nl"/> </module> <module name="RightCurly"> <property name="option" value="alone"/> </module> <module name="NeedBraces"/> <module name="WhitespaceAround"/> <module name="PackageName"> <property name="format" value="^com\.huawei\.[a-z]+(\.[a-z]+)*$"/> </module> <module name="TypeName"/> <module name="MethodName"/> <module name="ConstantName"/> </module> </module>LineLength.max=80对应折行规则;Indentation.basicOffset=4对应 4 空格缩进;LeftCurly.option=nl(new line)强制{换行,RightCurly.option=alone强制}独占一行;NeedBraces对应「执行语句必须加括号」;WhitespaceAround管双目操作符两侧空格;PackageName.format用正则把包名钉死在com.huawei.*命名空间下,防止有人随手起个utils包。
3. 注释规范:30% 注释量与 Javadoc 标签体系怎么落笔
3.1 package.html 与文件头注释
包注释单独写进一个package.html放在该包目录下,方便 Javadoc 收集,内容包括一句话简述、详细描述、产品模块名称与版本、版权信息。
<html> <body> <p>为 Relay 提供通信类,上层业务使用本包的通信类与 SP 进行通信。</p> <p>详细描述本包内容,以及它在整个项目中的位置。</p> <p>MMSC V100R002 Relay<br> (C) 版权所有 2002-2007</p> </body> </html>文件头注释放在package语句之前,5.1.4 特意要求用/*而不是/**起头,避免被 Javadoc 收集成正式接口文档。
/* * 文件名:LogManager.java * 版权:Copyright 2002-2007 Huawei Tech. Co. Ltd. All Rights Reserved. * 描述:MMSC V100R002 Relay 通用日志系统 * 修改人:张三 * 修改时间:2001-02-16 * 修改内容:新增 * 修改人:李四 * 修改时间:2001-02-26 * 修改单号:WSS368 * 修改内容:…… */ package com.huawei.msg.relay.comm;每次修改都要在文件头补一条修改记录,CheckIn 时可以把这段信息直接粘到版本控制系统的注释栏;代码还没纳入受控之前可以省掉。文件名是可选项,修改单号在没上单号系统时也可以空着。
3.2 类与方法的 Javadoc 标签
类注释放在package之后、class或interface之前,用/** */包起来,方便 Javadoc 收集。
/** * LogManager 类集中控制对日志读写的操作。 * <p>全部为静态变量与静态方法,对外提供统一接口。分配对应日志类型的 * 读写器,读取或写入符合条件的日志纪录。</p> * * @author 张三, 李四, 王五 * @version 1.2, 2001-03-25 * @see LogIterator * @see BasicLog * @since CommonLog1.0 */ public class LogManager { // ... }方法注释在类的标签体系上多出参数和异常部分,第一句话必须是简洁的功能概括并以句号结束,因为 Javadoc 生成简介时只取第一句。
/** * 根据日志类型与时间读取日志。 * <p>查询条件为 null 或 0 表示无限制,反复器缓冲数为 0 读不到日志。 * 查询时间为左包含原则,即 [startTime, endTime)。</p> * * @param logTypeName 日志类型名(在配置文件中定义的) * @param startTime 查询日志的开始时间 * @param endTime 查询日志的结束时间 * @param bufferNum 日志反复器缓冲记录数 * @return 结果集,日志反复器 * @since CommonLog1.0 */ public static LogIterator read(String logType, Date startTime, Date endTime, int logLevel, String userName, int bufferNum) { // ... }5.1.11 有一条容易被忽略:方法内部用throw抛出的异常必须在注释里标明,throws子句声明的非运行期异常也必须标明。标签用法上,@exception标注运行期异常,@throws标注非运行期异常,Javadoc 里两者等价,但规范要求分开用。常用标签整理如下。
| 标签 | 用途 | 适用位置 | | @author | 作者,可多个 | 类、接口 | | @version | 版本号与日期 | 类、接口 | | @param | 参数逐个说明 | 方法 | | @return | 返回值说明 | 方法 | | @exception | Runtime 异常说明 | 方法 | | @throws | 非 Runtime 异常说明 | 方法 | | @see | 相关类或方法 | 类、方法 | | @since | 从哪个版本开始有 | 类、方法 | | @deprecated | 不建议使用 | 类、方法 |
3.3 注释位置、缩排与空行
注释要挨着被描述的代码,放上方或右方,不能放在代码下面;放上方时与上面的代码空一行隔开;注释本身要和被描述内容同一个缩进层级。
public void example() { // 注释写在被描述代码上方,且与代码同缩进 codeBlockOne(); // 与上面的代码之间空一行 codeBlockTwo(); }switch里如果故意穿透case,必须在下个case之前写明原因,这条在 5.1.16 里是硬规则。原因很实际:审核的人看到没有break第一反应是漏了,注释能省掉一次来回沟通。
switch (level) { case DEBUG: log.debug(msg); break; // 有意穿透:INFO 级别同样需要写入审计日志 case INFO: audit(msg); break; default: break; }另外几条容易被当耳旁风的:函数、变量、结构命名好的话,代码本身就是注释,不必再写重复信息;注释要写「为什么」而不是「做什么」,// 如果 receiveFlag 为真这种注释没有信息量,改成// 如果从连接收到消息才有用。
3.4 用脚本卡住 30% 注释率
5.1.1 要求有效注释量在 30% 以上,用注释统计工具来量。没有现成工具时,一段 awk 可以先粗算,思路是「//行计入注释,块注释内的行计入注释,空行跳过,其余算代码」。
# 粗略统计单个 Java 文件的注释行占比 awk ' /^[[:space:]]*\/\// { comment++; next } /^[[:space:]]*\/\*/ { inblock=1 } inblock { comment++; if (/\*\//) inblock=0; next } /^[[:space:]]*$/ { next } { code++ } END { total = comment + code; if (total > 0) printf "注释行: %d, 代码行: %d, 注释率: %.1f%%\n", comment, code, comment * 100 / total; }' src/main/java/com/huawei/msg/relay/comm/LogManager.javacomment累计注释行,inblock标记是否在块注释内部,code累计代码行,空行不参与分母。注释率的定义是注释行 /(注释行 + 代码行)。这个脚本是粗算,块注释的首尾行也被算进注释,想要更精确就用 Checkstyle 的JavadocMethod、JavadocType、JavadocVariable分别校验方法、类、字段是否有 Javadoc,两者一起用基本能覆盖 30% 这条线。
4. 编码规范:单一职责、异常处理和资源关闭的边界
4.1 一个方法一件事,数据类必须重载 toString
7.1.1 要求一个函数仅完成一件功能,即使功能只有一两行也应该抽成方法,理由是功能明确、可读性高、方便维护和测试。7.1.3 把同样的要求抬到类级别:一个类仅实现一组相近功能,逻辑处理、数据、显示三者分离,数据类不能包含数据处理逻辑,通信类不能包含显示逻辑。
7.1.4 要求所有数据类重载toString(),返回有意义的内容;父类已经实现了合理toString()的子类可以继承不重写。
public class TopoNode { private String nodeName; @Override public String toString() { return "NodeName : " + nodeName; } }@Override是 Java 5+ 的写法,规范原文没提,但加上能让编译器帮你在签名写错时报错。这条的实际价值在排查问题:日志里log.info("node={}", node)直接打出可读内容,而不是TopoNode@1a2b3c,半夜看日志的人会感谢这条规则。
4.2 try-catch-finally 里的 close
7.1.5 是资源关闭的强制条款:数据库操作、IO 操作等需要close()的对象,必须在 try-catch-finally 的 finally 中关闭。
FileOutputStream out = null; try { out = new FileOutputStream(file); out.write(data); } catch (IOException ioe) { logger.error("写文件失败: " + file, ioe); } finally { if (out != null) { try { out.close(); } catch (IOException ioe) { logger.error("关闭文件流失败: " + file, ioe); } } }坑在 finally 里:close()自身也会抛IOException,所以得再套一层 catch,否则异常会盖掉主流程的异常。文档写于 JDK 7 之前,那时只能用这种写法。JDK 7 以后可以写成 try-with-resources:
try (FileOutputStream out = new FileOutputStream(file)) { out.write(data); } catch (IOException ioe) { logger.error("写文件失败: " + file, ioe); }try-with-resources 自动调用 close,并把 close 抛出的异常作为 suppressed 异常挂在主异常上,比手写 finally 少一层嵌套。但老版本 JTEST 规则可能仍然提示「资源需在 finally 关闭」,项目基线是旧规则的话按团队要求保留 finally 写法;能在 JDK 8+ 上跑且门禁允许,try-with-resources 是等价且更安全的替代。
4.3 两类异常的边界与描述信息
7.1.8 把异常分成两类,处理方式完全不同。
| 类型 | 继承来源 | 方法声明是否加 throws | 注释标签 | | 运行期异常 | RuntimeException | 不加 | @exception | | 非运行期异常 | Exception | 必须加 | @throws |
7.1.6 规定 catch 之后如果不对异常做处理,就必须记日志或者调printStackTrace(),有特殊原因不处理要用注释说明。7.1.7 要求自己抛出的异常必须填写详细描述信息,便于问题定位。
throw new IOException("Writing data error! Data: " + data.toString());4.4 参数合法性检查归谁做
7.1.2 给出一个默认约定:接口方法参数的合法性检查由方法调用者负责,接口本身要明确写清这个归属。文档点了两个极端——调用者和被调用者都不查,合法性检查被漏掉,问题留到线上;两边都查,代码冗余、效率下降。常见做法是在接口文档和 Javadoc 里写「本方法不校验 xxx,调用方需保证……」;实现里只对关键的空值和取值范围做兜底,避免重复校验。
4.5 存取控制符能收就收
6.2.3 是建议项:不是必须 public 的属性用 protected,不是必须 protected 的用 private。配合 6.2.4「含有集合意义的属性尽量用复数命名」(customers、orderItems)一起做,能明显减少后面重构时改签名的摩擦,也方便 IDE 的引用查找收敛到最小范围。
5. JTEST 规范落地:把 29 页纸面规则接进 CI 门禁
JTEST 在文档里被单列成第八章,规则 8.1、建议 8.2,说明这套规范从一开始就没打算靠人工评审兜底。Checkstyle、PMD、SpotBugs 三件套能把大部分条款自动化,映射关系大致如下。
| 规范条目 | 工具 | 检查项 | | 4 空格缩进 | Checkstyle | Indentation(basicOffset=4) | | 大括号独占一行 | Checkstyle | LeftCurly(nl) | | 80 字符折行 | Checkstyle | LineLength(max=80) | | 执行语句必须加括号 | Checkstyle | NeedBraces | | 类名 PascalCase | Checkstyle | TypeName | | 常量全大写 | Checkstyle | ConstantName | | 方法必须有 Javadoc | Checkstyle | JavadocMethod | | 空 catch 块 | PMD | EmptyCatchBlock | | 资源未关闭 | SpotBugs | OS_OPEN_STREAM |
Maven 里挂 Checkstyle 最省事:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-checkstyle-plugin</artifactId> <version>3.3.1</version> <configuration> <configLocation>config/huawei-checkstyle.xml</configLocation> <consoleOutput>true</consoleOutput> <failOnViolation>true</failOnViolation> <violationSeverity>error</violationSeverity> </configuration> <executions> <execution> <id>checkstyle-validate</id> <phase>validate</phase> <goals> <goal>check</goal> </goals> </execution> </executions> </plugin>configLocation指向第 2 章那份 XML;phase=validate让检查在编译之前跑,问题还没进字节码就被拦下;violationSeverity=error意味着只有 error 级别才卡构建。这一步是整套规范能不能落地的关键——规范本身把条款分成「规则」和「建议」两档,工具里就对应severity的 error 和 warning:规则用 error 卡门禁,建议用 warning 只做提示,红线守住,也不至于因为一条命名建议把构建搞红。
落地时的小技巧是把 warning 级别的项单独输出成报告,不阻塞流水线,但在合并请求里用行内标注展示,评审时直接对着标注讨论。等团队对某条建议的接受度上来了,再把它从 warning 升到 error。这个渐进过程比一次性把所有条款设成 error 更容易推得动,老代码库基数大的时候尤其明显。跑通之后配合mvn checkstyle:check -Dcheckstyle.config.location=config/huawei-checkstyle.xml,本地先自查一遍再提交,省得流水线上来回改。
本文还有配套的精品资源,点击获取