制定代码规约的意义
统一的代码风格可以让开发工程师们没有代码心理壁垒,每个人可以轻松阅读并快速理解代码逻辑,便于高效协作,逐步形成团队代码的风格。
- 高效
标准统一,提升沟通效率和协作效率,好的编码规范可以最大限度的提高团队开发的合作效率。 - 质量
长期的规范性编码还可以让开发人员养成好的编码习惯,甚至锻炼出更加严谨的思维,防患未然,提升质量意识,降低故障率和维护成本,快速定位问题。 - 情怀
程序员应该追求代码的美、系统的美、设计的美,追求卓越的工匠精神,打磨精品代码。
命名
【规范】 类名使用UpperCamelCase 风格,必须遵从驼峰形式,但以下情形例外: ( 领域模型的相关命名 )DO / BO / DTO / VO 等。
-
[正例]: MarcoPolo / UserDO / XmlService / TcpUdpDeal / TaPromotion
-
[反例]: macroPolo / UserDo / XMLService / TCPUDPDeal / TAPromotion
【规范】方法名、参数名、成员变量、局部变量都统一使用lowerCamelCase 风格,必须遵从驼峰形式。
- [正例]:localValue / getHttpMessage() / inputUserId
【规范】常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
public static final String GET_APPLICATION_SETTINGS_BY_MESSAGE_NAME_AND_APP_NAME = "/v1/capp/common/getApplicationSettingsWithMessageNameAndAppName";
【规范】抽象类命名使用 Abstract 或 Base 开头 ; 异常类命名使用 Exception 结尾 ; 测试类命名以它要测试的类的名称开始,以 Test 结尾。枚举类名建议带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。
//抽象类
public abstract class AbstractPingController {
//....
}
//异常类
public class CommunicationServiceException extends RuntimeException {
//....
}
//测试类
public class ApplicationSettingControllerTest {
//....
}
//枚举类
public enum SexEnum {
M("m", "先生"),
Z("z", "女士");
private String code;
private String value;
SexEnum(String code, String value) {
this.code = code;
this.value = value;
}
}
【规范】POJO 类中布尔类型的变量,都不要加 is ,否则部分框架解析会引起序列化错误。
- [正例]:
private Boolean showDetails;
- [反例]:
private Boolean isShowDetails;
【规范】各层命名规约:
-
Service / DAO 层方法命名规约
- 获取单个对象的方法用 get 做前缀。
- 获取多个对象的方法用 list 做前缀(习惯:getXXXList)。
- 获取统计值的方法用 count 做前缀。
- 插入的方法用 save( 推荐 ) 或 insert 做前缀。
- 删除的方法用 remove( 推荐 ) 或 delete 做前缀。
- 修改的方法用 update 做前缀(或modify)。
-
领域模型命名规约
- 数据对象: xxxDO , xxx 即为数据表名。
- 数据传输对象: xxxDTO , xxx 为业务领域相关的名称。
- 展示对象: xxxVO , xxx 一般为网页名称。
- POJO 是 DO / DTO / BO / VO 的统称,禁止命名成 xxxPOJO 。
常量
【规范】不允许任何魔法值( 即未经定义的常量 ) 直接出现在代码中。
String key =" Id # taobao _"+ tradeId;
cache . put(key , value);
格式规约
【风格】单行太长需换行
【风格】方法体内的执行语句组、变量的定义语句组、不同的业务逻辑之间或者不同的语义之间插入一个空行。相同业务逻辑和语义之间不需要插入空行。
OOP规约
【效率】避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。
【规范】所有的覆写方法,必须加@ Override 注解。
【规范】对外暴露的接口签名,原则上不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加@Deprecated 注解,并清晰地说明采用的新接口或者新服务是什么。
【规范】Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用equals。
[正例]:"test".equals(object);
[反例]:object.equals("test");
【规范】所有的相同类型的包装类对象之间值的比较,全部使用equals方法比较。(注意空指针)
说明:对于 Integer var = ?在-128至127之间的赋值,Integer对象是在IntegerCache.cache产生,会复用已有对象,这个区间内的Integer值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,推荐使用equals方法进行判断。
【规范】关于基本数据类型与包装数据类型的使用标准如下:
- 所有的 POJO 类属性必须使用包装数据类型。
注意:当使用BeanUtils.copyProperties(EnityA,EnityB)进行拷贝时如果属性是Boolean包装类型,拷贝的值不对,会总是false
如果需要拷贝的话,Boolean需要拆箱,用boolean。
//源码 java的rt.jar包中的Instrospector类
if (argCount == 0) {
if (name.startsWith(GET_PREFIX)) {
// Simple getter
pd = new PropertyDescriptor(this.beanClass, name.substring(3), method, null);
} else if (resultType == boolean.class && name.startsWith(IS_PREFIX)) {
// Boolean getter
pd = new PropertyDescriptor(this.beanClass, name.substring(2), method, null);
}
}
//所以,Boolean类型的 isXXX()是不支持复制的,只支持boolean类型的isXXX()复制。Boolean类型的getXXX()是支持的。
-
RPC 方法的返回值和参数必须使用包装数据类型。
-
所有的局部变量【推荐】使用基本数据类型。
【强制】序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败 ; 如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。
【规范】构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在init方法中。
【规范】使用索引访问用String的split方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛IndexOutOfBoundsException的风险。
String str = "a,b,c,,";
String[] ary = str.split(",");
//预期大于 3,结果是 3
System.out.println(ary.length);
【规范】当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起,便于阅读。
【风格】类内方法定义顺序依次是:公有方法或保护方法>私有方法>getter/setter方法。
【效率】final可提高程序响应效率,声明成final的情况:
- 不需要重新赋值的变量,包括类属性、局部变量。
- 对象参数前加 final ,表示不允许修改引用的指向。
- 类方法确定不允许被重写。
final boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
集合处理
【强制】关于hashCode和equals的处理,遵循如下规则:
- 只要重写equals,就必须重写hashCode。
- 因为Set存储的是不重复的对象,依据hashCode和equals进行判断,所以Set存储的对象必须重写这两个方法。
- 如果自定义对象做为Map的键,那么必须重写hashCode和equals。
【强制】不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。
[反例]:
List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
for (String temp : a) {
if("1".equals(temp)){
a.remove(temp);
}
}
说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?(java.util.ConcurrentModificationException)
[正例]:
Iterator<String> it = a.iterator();
while(it.hasNext()){
String temp = it.next();
if(删除元素的条件){
it.remove();
}
}
【规范】集合初始化时,尽量指定集合初始值大小。
说明:ArrayList尽量使用ArrayList(int initialCapacity)初始化。
【规范】使用entrySet遍历Map类集合KV,而不是keySet方式进行遍历。
说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的 value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是JDK8,使用 Map.foreach 方法。
Map<String, String> map = new HashMap<String, String>();
map.put("1", "@@");
map.put("2", "##");
/**
* JDK8推荐使用
*/
map.forEach((K, V) -> {
System.out.println("Key : " + K);
System.out.println("Value : " + V);
});
/**
* foreach推荐使用
*/
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("Key : " + entry.getKey());
System.out.println("Value : " + entry.getValue());
}
/**
* 不推荐使用
*/
for (String key : map.keySet()) {
System.out.println("Key : " + key);
System.out.println("Value : " + map.get(key));
}
【强制】高度注意 Map 类集合 K/V 能不能存储 null 值的情况,如下表格:
| 集合类 | Key | Value | Super | 说明 |
|---|---|---|---|---|
| Hashtable | 不允许为 null | 不允许为 null | Dictionary | 线程安全 |
| ConcurrentHashMap | 不允许为 null | 不允许为 null | AbstractMap | 分段锁技术 |
| TreeMap | 不允许为 null | 允许为 null | AbstractMap | 线程不安全 |
| HashMap | 允许为 null | 允许为 null | AbstractMap | 线程不安全 |
并发处理
【规范】获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
说明:资源驱动类、工具类、单例工厂类都需要注意。
【规范】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
public class TimerTaskThread extends Thread { public TimerTaskThread(){
super.setName("TimerTaskThread"); ... }
【规范】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资 源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者 “过度切换”的问题。
【规范】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明:Executors 返回的线程池对象的弊端如下:
- FixedThreadPool 和 SingleThreadPool:
- 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
- CachedThreadPool 和 ScheduledThreadPool:
- 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
【效率】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造 成死锁。
说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序 也必须是 A、B、C,否则可能出现死锁。
【规范】并发修改同一记录时,避免更新丢失,要么在应用层加锁,要么在缓存加锁,要么在 数据库层使用乐观锁,使用 version 作为更新依据。
说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次 数不得小于 3 次。
【规范】多线程并行处理定时任务时,Timer 运行多个 TimeTask 时,只要其中之一没有捕获 抛出的异常,其它任务便会自动终止运行,使用ScheduledExecutorService 则没有这个问题。
【规范】HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升,在 开发过程中注意规避此风险。
控制语句
【规范】在一个 switch 块内,每个 case 要么通过 break/return 等来终止,要么注释说明程 序将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default 语句并且 放在最后,即使它什么代码也没有。
【规范】在 if/else/for/while/do 语句中必须使用大括号,即使只有一行代码,避免使用 下面的形式:if (condition) statements;
【规范】推荐尽量少用 else, if-else 的方式可以改写成:
if(condition){
...
return obj; }
// 接着写 else 的业务逻辑代码;
说明:如果非得使用if()...else if()...else...方式表达逻辑,【强制】请勿超过3层,
超过请使用状态设计模式 或者 卫语句。
卫语句示例:
public void today() { if (isBusy()) {
System.out.println(“change time.”); return;
}
if (isFree()) {
System.out.println(“go to travel.”);
return;
}
System.out.println(“stay at home to learn Alibaba Java Coding Guidelines.”);
return;
}
【规范】除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复 杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
说明:很多 if 语句内的逻辑相当复杂,阅读者需要分析条件表达式的最终结果,才能明确什么 样的条件执行什么样的语句,那么,如果阅读者分析逻辑表达式错误呢?
[正例]:
//伪代码如下
boolean existed = (file.open(fileName, "w") != null) && (...) || (...); if (existed) {
... }
[反例]:
if ((file.open(fileName, "w") != null) && (...) || (...)) { ...
}
【规范】方法中需要进行参数校验的场景:
- 调用频次低的方法。
- 执行时间开销很大的方法,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退,或者错误,那得不偿失。
- 需要极高稳定性和可用性的方法。
- 对外提供的开放接口,不管是RPC/API/HTTP接口。
- 敏感权限入口。
【规范】方法中不需要参数校验的场景:
- 极有可能被循环调用的方法,不建议对参数进行校验。但在方法说明里必须注明外部参数检查。
- 底层的方法调用频度都比较高,一般不校验。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。一般 DAO 层与 Service 层都在同一个应用中,部署在同一 台服务器中,所以 DAO 的参数校验,可以省略。
- 被声明成private只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参 数已经做过检查或者肯定不会有问题,此时可以不校验参数。
注释规约
【规范】类、类属性、类方法的注释必须使用 Javadoc 规范,使用/**内容*/格式,不得使用 //xxx 方式。
【规范】所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、 异常说明外,还必须指出该方法做什么事情,实现什么功能。
说明:对子类的实现要求,或者调用注意事项,请一并说明。
【风格】方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使用/* */注释,注意与代码对齐。
【规范】所有的枚举类型字段必须要有注释,说明每个数据项的用途。
【规范】代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑 等的修改。
【规范】注释掉的代码尽量要配合说明,而不是简单的注释掉。\
说明:代码被注释掉有两种可能性:
- 后续会恢复此段代码逻辑。
- 永久不用。前者如果没 有备注信息,难以知晓注释动机。
后者建议直接删掉(代码仓库保存了历史代码)。
【风格】特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描, 经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。
- 待办事宜(TODO)