💡 建议直接阅读《阿里巴巴 Java 技术开发手册》
规整的代码格式会给程序的开发与后续的维护提供很大方便,简单总结以下编码规范:
- 每条语句要单独占一行,一条命令以分号结束
- 声明变量时,尽量使每个变量的声明独占一行,即使是相同的数据类型也要放置在单独一行上,这样做有助于添加注释
- 声明局部变量的同时对其初始化
- 关键字与关键字之间如果有多个空格,这些空格均被视作为一个,且多行空格没有意义,为了便于理解和阅读,应该严格控制好空格的数量
- 为了方便后续的维护,不要使用技术性很高、难懂、易混淆判断的语句
- 对于关键方法要添加注释,来帮助阅读者理解代码结构
1. 命名风格
《阿里巴巴 Java 技术开发手册》中关于命名风格的代码规范共计 16 条,具体如下表所示:
- 【强制】代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束
【❌反例】name / __name / $Object / Object$ / name
- 【强制】代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。
【说明】正确的英文拼写和语法可以让阅读者更易于理解,避免歧义。注意,纯拼音的命名方式也要避免采用。
【✅正例】alibaba / taobao / youku / hangzhou
【❌反例】DaZhePromotion / getPingfenByName() / int something = 3
- 【强制】类名使用
UpperCamelCase
风格,必须遵循驼峰形式,但这些情况除外:DO / BO / DTO / VO / AO
【✅正例】MarcoPolo / UserDO / XmlService / TcpUdpDeal
【❌反例】marcoPolo / UserDo / XMLService / TCPUDPDeal
- 【强制】方法名、参数名、成员变量、局部变量都统一使用
lowCamelCase
风格,必须遵从驼峰形式
【✅正例】localValue / getHttpMessage() / inputUserId
- 【强制】常量命名全部大小,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字过长
【✅正例】MAX_STOCK_COUNT
【❌反例】MAX_COUNT,没有说明关于什么内容的
- 【强制】抽象类命名使用
Abstract
或者Base
开头,异常类命名使用Exception
结尾,测试类命名以它要测试的名称开始,以Test
结尾 - 【强制】中括号是数组类型的一部分
【✅正例】String[] agrs;
【❌反例】采用String args[];
来定义数组,尽管 Java 是允许这样定义数组的
- 【强制】
POJO
类中的布尔类型的变量,都不要加is
,否则部分框架解析会引起序列化错误。
【❌反例】定义了一个isDeleted
布尔型属性,其方法也是isDeleted()
,RPC 框架在反向解析时,以为对应的属性名称是deleted
,导致属性获取不到,进而跑出异常。
- 【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语音的英语单词。包名统一使用单数形式,但是类名有复数含义,类名可以使用复数形式。
【✅正例】包名 com.alibaba.open.util / 类名 MessageUtils
- 【强制】杜绝完全不规范的缩写,避免望文不知道含义
【反例】AbstractClass 缩写为 AbsClass,condition 缩写为 condi,这种随意缩写的命名方式严重降低了代码的可阅读性
- 【推荐】为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词来表达含义
【✅正例】从远程仓库拉取代码的类命名为PullCodeFormRemoteRepository
【❌反例】变量int a;
这种随意的命名方式
- 【推荐】如果模块、接口、类、方法使用了设计模式,在命名时体现出具体模式
【说明】将设计模式体现在名字中,有利于阅读者快速理解架构设计理念
public class OrderFactory;
public class LoginProxy;
public class ResourceObserver;
- 【推荐】接口类中的方法和属性不要加任何修饰符(
**public**
也不要加),保持代码的简洁性,并加上有效的 Javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,必须要与接口方法相关,并且是整个应用的基础常量。
【说明】JDK8 中接口允许有默认实现,那么这个default
方法,是对所有实现类都有价值的默认实现
【✅正例】
- 接口方法签名
void f();
- 接口基础常量
String COMPANY = "alibaba";
【❌反例】接口方法定义 public abstract void f();
- 接口和实现类的命名有 2 种规则
【强制】对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用Impl
的后缀与接口区别
【✅正例】CacheServiceImpl
实现CacheService
接口
【推荐】如果是形容能力的接口名称,取对应的形容词做接口名(通常是-able
的形式)
【✅正例】AbstractTranslator
实现Translatable
- 【参考】枚举类名建议带上
Enum
后缀,枚举成员名称需要全部大写,单词间用下划线隔开。
【说明】枚举其实就是特殊的常量类,且构造方法被默认强制是私有
【✅正例】枚举名字为ProcessStatusEnum
的成员名称:SUCCESS / UNKNOWN_REASON
- 【参考】Service / DAO 层方法命名规约
- 获取单个对象的方法用
get
做前缀 - 获取多个对象的方法用
list
做前缀 - 获取统计值的方法用
count
做前缀 - 插入的方法用
save
/insert
做前缀 - 删除的方法用
remove
/delete
做前缀 - 修改的方法用
update
做前缀
- 获取单个对象的方法用
【参考】领域模型命名规约
- 数据对象:xxxDO,xxx 即为数据表名
- 数据传输对象:xxxDTO,xxx 为业务领域相关的名称
- 展示对象:xxxVO,xxx 一般为网页名称
- POJO 是 DO / DTO / BO / VO 的统称,禁止命名成 xxxPOJO
2. 常量定义
【强制】不允许任何魔法值(即未经过定义的常量)直接出现在代码中
String key = "Id#taobao_" + tradeId;
cache.put(key, value);
【强制】
long
或者Long
初识赋值时,使用大写的L
,不能是小写的l
,小写容易跟数字 1 混淆,造成误解。
【❌反例】Long a = 2l;
写的是数字 21,还是 Long 型的 2 呢?
- 【推荐】不要使用一个常量类维护所有常量,应该按照功能进行归类,分开维护。
【说明】大而全的常量类,得使用查找功能才能定为到修改的常量,不利于理解和维护
【✅正例】缓存相关常量放在类CacheConsts
下,系统配置相关常量放在类ConfigConsts
下
- 【推荐】常量的复用层次有 5 层:跨应用层共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量
- 跨应用共享常量:放置在二方库中,通常是 client.jar 中的 constant 目录下
- 应用内共享常量:放置在一方库中,通常是 modules 中的 constant 目录下
【❌反例】易懂变量也要统一定义成应用内共享常量,如果有两个程序员定义了YES
变量,预期返回结果为true
,但实际返回为false
// Class A
public static final String YES = "yes";
// Class B
public static final String YES = "y";
System.out.println(A.YES.equals(B.YES)); // false
- 子工程内部共享常量:即在当前子工程的 constant 目录下
- 包内共享常量:即在当前包下单独的 constant 目录下
- 类内共享常量:直接在类内部使用
private static final
定义
:::info
📋 补充知识
——————————————————
- 一方库:别称一方包,本工程中的各模块相互依赖
- 二方库:别称二方包,公司内部的依赖库,一般指内部其他项目的 jar 包
- 三方库:别称三方包,公司之外其他组织的开源库,比如 Apache、Google 发布的 jar 包 :::
【推荐】如果变量值仅在一个范围内变化,且带有名称之外的延伸属性,定义为枚举类。下面正例中的数字就是延伸信息,表示星期几。
public Enum {
MONDAY(1),
TUESDAY(2),
WEDNESDAY(3),
THURSDAY(4),
FRIDAY(5),
SATURDAY(6),
SUNDAY(7);
}
3. 代码格式
【强制】大括号的使用约定:如果大括号内为空,则简洁地写成
{}
即可,不需要换行;如果大括号内是非空代码块,则- 左大括号前不换行
- 左大括号后换行
- 右大括号前换行
- 右大括号后还有
else
等代码则不换行,表示终止的右大括号后必须换行
【强制】左小括号和字符之间不出现空格,同样,右小括号和字符之间也不出现空格。
if ( a == b )
【强制】
if
/for
/while
/switch
/do
等保留字与括号之间都必须加上空格【强制】任何二目、三目运算符的左右两边都需要加一个空格
【说明】运算符包括赋值运算符=
、逻辑运算符&&
、加减乘除符号等
- 【强制】采用 4 个空格缩进,禁止使用 tab 字符
【说明】如果使用 tab 缩进,必须设置 1 个 tab 为 4 个空格。IDEA 设置 tab 为 4 个空格时,请勿勾选 Use tab character。
public static void main(String[] args) {
// 缩进 4 个空格
String say = "Hello";
// 运算符的左右必须有一个空格
int flag = 0;
// 关键词 if 与括号之间必须有一个空格,括号内的 f 与左括号,0 与右括号不需要空格
if (flag == 0) {
System.out.println(say);
}
// 左大括号前空格不换行,左大括号后换行
if (flag == 1) {
System.out.println("world");
} else {
System.out.println("ok");
}
}
【强制】注释的双斜线与注释内容之间有且仅有一个空格
【强制】单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则:
- 第 2 行相对第一行缩进 4 个空格,从第 3 行开始,不再继续缩进
- 运算符与下文一起换行
- 方法调用的点符号与下文一起换行
- 方法调用时,多个参数,需要换行时,在逗号后进行
- 在括号前不要换行 | | ```java StringBuffer sb = new StringBuffer(); // 超过 120 个字符的情况下, // 换行缩进 4 个空格,点号和方法名称一起换行 sb.append(“zi”).append(“xin”)… .append(“huang”)….;
| ```java
StringBuffer sb = new StringBuffer();
// 超过 120 个字符的情况下,换行缩进 4 个空格,点号和方法名称一起换行
sb.append("zi").append("xin")...append
("huang");
// 参数很多的方法调用可能超过 120 个字符,不要在逗号前换行
method(arg1, arg2, arg3, ...
, argN);
| | —- | —- | —- |
- 【强制】方法参数在定义和传入时,多个参数逗号后边必须加空格
【✅正例】method('a', 'b', 'c');
【强制】IDE 的 text file encoding 设置为 UTF-8,文件的换行符使用 Unix 格式,不要使用 Windows 格式
【推荐】没有必要增加若干空格来使某一行的字符与上一行对应位置的字符对齐
【说明】在变量多的情况下,字符对齐是一件非常麻烦的事情
int a = 3;
long b = 4L;
float c = 5F;
StringBuffer sb = new StringBuffer();
- 【推荐】方法体内的执行语句组、变量的定义语句组、不同的业务逻辑之间或者不同的语义之间插入一个空行,相同业务员逻辑和语义之间不需要插入空行。
4. 控制语句
【强制】在一个
switch
代码块内,每个case
要么通过break
/return
等来终止,要么注释说明程序将继续执行到哪一个case
为止。在一个switch
代码块内,都必须包含一个default
语句并且放在最后,即使它什么代码也没有。【强制】在
if
/else
/for
/while
/do
语句中必须使用大括号,即使只有一行代码,避免采用单行的编码方式if (condition) statements;
【推荐】表达异常的分支,少用
if-else
方式
【说明】如果非得使用if()...else if()...else...
方式表达逻辑,避免后续代码维护困难,请勿超过 3 层
【✅正例】超过 3 层的if-else
的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现
| | ```java if (condition) { … return obj; }
// 接着写 else 的业务逻辑
| ```java
public void today() {
if (isBusy()) {
System.out.println("change time.");
return;
}
if (isFree()) {
System.out.println("go to travel.");
return;
}
System.out.println("Learn Alibaba Java Coding");
return;
}
| | —- | —- | —- |
- 【推荐】除常用方法(如getX / isXxx)等外,不要在条件判断中执行其他复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的变量名,以提高可读性
【说明】很多if
语句内的逻辑相当复杂,阅读者需要分析条件表达式的最终结果,才能明确什么样的条件执行什么样的语句。
| | ```java final boolean existed = (file.open(fileName, “w”) != null) && (…);
if (existed) { … }
| ```java
if ((file.open(fileName, "w") != null) && (...)) {
...
}
| | —- | —- | —- |
- 【推荐】循环体重的语句要考量性能,以下操作尽量移动至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的
try-catch
操作。
【推荐】接口入参保护,这种场景常见的是用于做批量操作的接口
【推荐】以下情况中需要对参数进行校验
- 调用频次低的方法
- 执行时间开销很大的方法,这种情况下,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退,或者错误,那得不偿失
- 需要极高稳定性和可用性的方法
- 对外提供的开放接口,不管是 RPC / API / HTTP 接口
- 敏感权限入口
【参考】以下情况中不需要进行参数校验
【强制】类、类属性、类方法的注释必须使用 Javadoc 规范,使用
/**内部*/
格式,不得使用// xxx
方式
【说明】在 IDE 编辑窗口中,Javadoc 方式会提示相关注释,生成 Javadoc 可以正确输出相应注释。在 IDE 中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的含义,提高阅读效率。
- 【强制】所有抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。
【说明】对子类的实现要求,或者调用注意事项,请一并说明。
【强制】所有的类都必须添加创建者和创建日期
【强制】方法内部单行注释,在被注释语句上方另起一行,使用
//
注释,方法内部多行注释使用/* */
,注意和代码保持对齐【强制】所有的枚举类型字段必须要有注释,说明每个数据项的用途
【推荐】与其用“半吊子”英文来注释,不如用中文注释把问题说清楚,专有名词与关键字保持英文原文即可
【❌反例】“TCP 连接超时”解释成“传输控制协议连接超时”,反而让阅读者理解更费劲
- 【推荐】代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改
【说明】代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义
- 【参考】谨慎注释掉代码,在上方详细说明,而不是简单地注释掉。如果无用,则删除。
【说明】代码被注释掉有 2 种可能性,前者如果没有备注信息,难以知晓动机;后者建议直接删掉(前提是 Git 仓库保存了历史代码)
- 后续会恢复此段代码逻辑
- 永久不用
【参考】对于注释的要求如下,完全没有注释的大段代码对于阅读者形同天数,注释是给自己看得,即使间隔很久,也能清晰地理解当时的思路。同时,注释也是给继任者看得,使得他人能够快速解题自己的工作。
- 能够准确地反应设计思想和代码逻辑
- 能够描述业务含义,使别的程序员能够迅速了解到代码背后的信息
【参考】好的命名、代码结构是自解释的,注释力求精简准确、表达到位。避免出现注释的一个极端,过多琐碎的注释,代码的逻辑一旦修改,修改注释也是相当大的负担。
【❌反例】方法名put
,加上 2 个有意义的变量名elephant
和fridge
,已经说明了这是在干什么,语义清晰地代码不需要额外的注释。
// put elephant into fridge
put(elephant, fridge);
- 【参考】特殊注释标记,请注明标记人与标记时间,注意及时处理这些标记,通过标记扫描,经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。
- 待办事项(TODO):(标记人,标记时间,[预处理时间])
表示需要实现,但目前还未实现的功能。这实际上是一个 Javadoc 标签,目前的 Javadoc 还没有实现,但已经被广泛使用。只能应用于类、接口和方法(因为它是一个 Javadoc 标签)。
- 错误,不能工作(FIXME):(标记人,标记时间,[预处理时间])
在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正。
6. 面向对象编程(OOP)规约
【强制】避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓地增加编译器解析成本,直接用类名来访问即可。
【强制】所有的覆写方法,必须加上
@Override
注解
【说明】getObject()
与get0bject()
的问题,一个是字母 O,一个是数字 0,加上@Override
后可以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,其实现类会马上编译报错。
- 【强制】相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用
Object
。
【说明】可变参数必须放置在参数列表最后,建议尽量不用可变参数编程,这一点 Scala 编程也是这么要求的
public User getUsers(String type, Integer... ids) {...}
【强制】外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加上
@Deprecated
注解,并清晰的说明采用的新辛苦或者新服务是什么。【强制】不能使用过时的类或方法
【说明】java.net.URLDecoder
中的方法decode(String encodeStr)
这个方法已经过时,应该使用双参数decode(String source, String encode)
。 接口提供方既然明确是过时接口,那么有义务提供新的接口。作为调用方来说,有义务去考证过时方法的新实现是什么。
- 【强制】
Object
的equals
方法容易抛空指针异常,应该使用常量或确定有值的对象来调用equals
。
【说明】推荐使用java.util.Objects#equals
【✅正例】"test".equals(object);
【❌反例】object.equals("test");
- 【强制】所有相同类型的包装类对象之间值的比较,全部使用
equals
方法比较
【说明】对于Integer var = ?
在 -128 至 127 范围内的赋值,Integer
对象是在IntegerCache.cache
产生,会复用已有对象,这个区间内的Integer
值可以直接使用==
进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用equals
方法进行判断。
- 【强制】关于基本数据类型与包装数据类型的使用标准如下:
- 所有 POJO 类属性必须使用包装数据类型
- RPC 方法的返回值和参数必须使用包装数据类型
- 【推荐】所有局部变量使用基本数据类型
【说明】POJO 类属性没有初值是提醒使用者在需要使用时,必须显式地进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证
【✅正例】数据库的查询结果可能是null
,因为自动拆箱,用基本数据类型接收有 NPE 风险
【❌反例】比如显示成交总额涨跌情况,即正负 x%,x 为基本数据类型,调用 RPC 服务,调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线。所以包装数据类型的null
值。能够表示额外的信息,例如远程调用失败、异常退出。
:::info
📋 补充知识
——————————————————
POJO (Plain Oridinary Java Object) 简单的 Java 对象,实际上就是普通的 JavaBeans。
:::
- 【强制】定义 DO / DTO / VO 等 POJO 类时,不要设定任何属性默认值
【❌反例】POJO 类的gmtCreate
默认值为new Date();
,但是这个属性自数据提取时,并没有置入具体值,在更新其他字段时又附带更新了此字段,导致创建时间被修改成当前时间。
- 【强制】序列化类新增属性时,请不要修改
serialVersionUID
字段,避免反序列失败。如果完全不兼容升级,避免反序列化混乱,那么请修改serialVersionUID
值
【说明】注意serialVersionUID
不一定会抛出序列化运行时异常
【强制】构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在
init
方法中【强制】POJO 类必须写
toString
方法,使用 IDE 中的工具:source -> generate toString 时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString。【强制】使用索引访问 String 的
split
方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛IndexOutOfBoundsException
的风险。String str = "a,b,c,,";
String[] arr = str.split(",");
// 预期大于 3,结果是 3
System.out.println(arr.length);
14,【推荐】当一个类有多个构造方法,或者多个同名方法,这些方法应该按照顺序防止在一起,便于阅读,此条规则优先于第 15 条规则
- 【推荐】类内方法定义顺序依次是:
public
或者protected
方法 >private
方法 >getter
/setter
方法
【说明】public
方法是类的调用这和维护者最关心的方法,首屏展示最好;protected
方法虽然只是子类关心,也可能是“模板设计模式”下的核心方法;而private
方法外部一般不需要特别关系,是一个黑盒实现。因为承载的信息价值较低,所有 Service 和 DAO 的 getter
/setter
方法放在类体最后。
【推荐】
setter
方法中,参数名称与类成员变量名称一致,this.成员名 = 参数名
。在getter
/setter
方法中,不要增加业务逻辑,增加排查问题的难度public Integer getData() {
if (true) {
return this.data + 100;
} else {
return this.data - 100;
}
}
【推荐】循环体内,字符串的连接方式,使用
StringBuilder
的append
方法进行扩展。
【说明】反编译出的字节码文件显示每次循环都会new
出一个StringBuilder
对象,然后进行append
操作,最后通过toString
方法返回String
对象,造成内存资源浪费。
String str = "start";
for (int i = 0; i < 100; i++) {
str += "hello";
}
【推荐】
final
可以声明类、成员变量、方法以及本地变量,下列情况使用final
关键字:- 不允许被继承的类,如
String
类 - 不允许修改引用的域对象,如
POJO
类的域对象 - 不允许被重写的方法,如POJO类的
setter
方法 - 不允许运行过程中重新赋值的局部变量
- 避免上下文重复使用一个变量,使用
final
描述可以强制重新定义一个变量,方便更好地进行重构
- 不允许被继承的类,如
【推荐】慎用
Object
的clone
方法来拷贝对象
【说明】对象的clone
方法默认是浅拷贝,若想实现深拷贝需要重写clone
方法实现属性对象的拷贝
- 【推荐】类成员与方法访问控制从严:
- 如果不允许外部直接通过
new
来创建对象,那么构造方法必须是private
- 工具类不允许有
public
或者default
构造方法 - 类非
static
成员变量,并且与子类共享,必须是protected
- 类非
static
成员变量,并且仅在本类使用,必须是private
- 类
static
成员变量如果仅在本类使用,必须是private
- 若是
static
成员变量,必须考虑是否为final
- 类成员方法只供类内部调用,必须是
private
- 类成员方法只对继承类公开,那么限制为
protected
- 如果不允许外部直接通过
【说明】任何类、方法、参数、变量,严格控制访问范围,过于宽泛的访问范围,不利于模块解耦
【思考】如果是一个private
方法,想删除就删除,可是一个public
的service
方法,或者一个public
的成员变量,删除一下,不得手心冒汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大,无限制的到处跑,那么你会担心的。
7. 集合处理
- 【强制】关于
hashCode
和equals
的处理,遵循如下规则:- 只要重写
equals
,就必须重写hashCode
- 因为
Set
存储的是不重复的对象,依据hashCode
和equals
进行判断,所以Set
存储的对象必须重写这 2 个方法 - 如果自定义对象作为
Map
键,那么必须重写hashCode
和equals
- 只要重写
【说明】String
重写了hashCode
和equals
方法,所以我们可以非常愉快地使用String
对象作为key
来使用
- 【强制】
ArrayList
的subList
结果不可强转成ArrayList
,否则会抛出ClassCastException
异常,即java.util.RandomAccessSubList cannot be cast to java.util.ArrayList
。
【说明】subList
返回的时ArrayList
的内部类SubList
,并不是ArrayList
,而是ArrayList
的一个视图,对于SubList
子列表的所有操作最终会反映到原列表上。
【强制】在
subList
场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、删除均会产生ConcurrentModificationException
异常。【强制】使用集合转数组的方法,必须使用集合的
toArray(T[] array)
,传入的是类型完全一样的数组,大小就是list.size()
。