JAVA代码编程规范
一、编程规范
(一)命名规约
1【强制】 POJO 类中布尔类型的变量,都不要加is
,否则部分框架解析会引起序列化错误。
- 反例:定义为基本数据类型
boolean isSuccess;
的属性,它的方法也是isSuccess()
,RPC 框架在反向解析的时候,“以为”对应的属性名称是success
,导致属性获取不到,进而抛出异常。
2【推荐】 接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的 Javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是与接口方法有关,并且是整个应用的基础常量。
- 正例:接口方法签名
void f();
接口基础常量表示为String COMPANY = "alibaba";
- 反例:接口方法定义为
public abstract void f();
- 说明:JDK8 中接口允许有默认实现,那么这个
default
方法,是对所有实现类都有价值的默认实现。
(二)常量定义
1【强制】 不允许出现任何魔法值(即未经定义的常量)直接出现在代码中。
- 反例:
String key = "Id#taobao_" tradeId;
cache.put(key, value);
2【强制】 long
或者Long
初始赋值时,必须使用大写的L
,不能是小写的l
,小写容易跟数字1
混淆,造成误解。
- 说明:
Long a = 2l;
写的是21
,还是Long
型的2
?
(三)格式规约
1【强制】 任何运算符左右必须加一个空格。
- 说明:运算符包括赋值运算符
=
、逻辑运算符&&
、加减乘除符号、三目运算符等。
2【强制】 方法参数在定义和传入时,多个参数逗号后边必须加空格。
- 正例:
method("a", "b", "c");
无论是参数a
还是参数b
后面皆有空格。
(四)OOP规约
1【强制】 避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。
2【强制】 Object
的equals
方法容易抛空指针异常,应使用常量或确定有值的对象来调用equals
。
- 正例:
"test".equals(object);
- 反例:
object.equals("test");
- 说明:推荐使用
java.util.Objects#equals
(JDK7 引入的工具类)
3【强制】 所有的相同类型的包装类对象之间值得比较,全部使用equals
方法比较。
- 说明:对于
Integer var = ?
在-128
至127
之间的赋值,Integer
对象是在IntegerCache.cache
产生,会复用已有对象,这个区间内的Integer
值可以直接使用==
进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用equals
方法进行判断。
4【强制】 关于基本数据类型与包装数据类型的使用标准为:所有的 POJO 类属性必须使用包装数据类型;RPC 方法的返回值和参数必须使用包装数据类型;所有的局部变量推荐使用基本数据类型。
- 说明:POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显示地进行赋值,任何 NPE 问题,或者入库检查,都是使用者来保证。
- 正例:数据库的查询结果可能是
null
,因为自动拆箱,用基本数据类型接收有 NPE 风险。 - 反例:比如显示成交总额涨跌情况,即正负
x%
,x
为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示0%
,这是不合理的,应该显示成中划线-
,所以包装数据类型的null
值,能够表示额外的信息,如远程调用失败,异常退出。
5【强制】 序列化类新增属性时,请不要修改serialVersionUID
字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改serialVersionUID
值。
- 说明:注意
serialVersionUID
不一致会抛出序列化运行时异常。
6【推荐】 循环体内,字符串的联接方式,使用StringBuilder
的append
方法进行扩展。
- 反例:
String str = "start";for(int i = 0; i < 100; i ){str = str "hello";}
- 说明:反编译出的字节码文件显示每次循环都会
new
出一个StringBuilder
。对象,然后进行sppend
操作,最后通过toString
方法返回String
对象,造成内存资源浪费。
7【推荐】 慎用Object
的clone
方法来拷贝对象。
- 说明:对象的
clone
方法默认是浅拷贝,若想实现深拷贝需要重写clone
方法实现属性对象的拷贝。
(五)集合处理
1【强制】 关于hashCode
和equals
的处理,遵循规则:只要重写equals
,就必须重写hashCode
;因为Set
存储的是不重复的对象,依据hashCode
和equals
进行判断,所以Set
存储的对象必须重写这两个方法;如果自定义对象作为Map
的键,那么必须重写hashCode
和equals
。
- 正例:
String
重写了hashCode
和equals
方法,所以我们可以非常愉快地使用String
对象作为key
来使用。
2【强制】 ArrayList
的subList
结果不可强转成 ArrayList
,否则会抛出ClassCastException
异常:java.util.RandomAccessSubList cannot be cast to java.util.ArrayList;
- 说明:
subList
返回的是ArrayList
的内部类SubList
,并不是ArrayList
,而是ArrayList
的一个视图,对于SubList
子列表的所有操作最终会反映到原列表上。
3【强制】 使用工具类Arrays.asList()
把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear
方法会抛出UnsupportedOperationException
异常。
- 说明:
asList
的返回对象是一个Arrays
内部类,并没有实现集合的修改方法。Arrays.asList
体现的是适配器模式,只是转换接口,后台的数据仍然是数组。
4【推荐】 使用entrySet
遍历Map
类集合KV
,而不是keySet
方式进行遍历。
- 说明:
keySet
其实是遍历了2
次,一次是转为Iterator
对象,另一次是从hashMap
中取出key
对应的value
,而entrySet
只是遍历了一次就把key
和value
都放到了entry
中,效率更高。如果是 JDK8,使用Map.foreach
方法。 - 正例:
values()
返回的是V
值集合,是一个list
集合对象;keySet()
返回的是K
值集合,是一个Set
集合对象;entrySet()
返回的是K-V
值组合集合。
5【推荐】 高度注意Map
类集合K/V
能不能存储null
值的情况,如下表格:
集合类 | Key | Value | Supper | 说明 |
---|---|---|---|---|
Hashtable | 不允许为null |
不允许为null |
Dictionary | 线程安全 |
ConcurrentHashMap | 不允许为null |
不允许为null |
AbstractMap | 分段锁技术 |
TreeMap | 不允许为null |
允许为null |
AbstractMap | 线程不安全 |
HashMap | 允许为null |
允许为null |
AbstractMap | 线程不安全 |
- 反例:由于
HashMap
的干扰,很多人认为ConcurrentHashMap
是可以置入null
值,注意存储null
值时会抛出 NPE 异常。
(六)并发处理
1【强制】 线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。
- 正例:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
2【强制】 线程池不允许使用Executors
去创建,而是通过ThreadPoolExecutor
的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
- 说明:
Executors
返回的线程池对象的弊端如下, FixedThreadPool
和SingleThreadPool
,允许的请求队列长度为Integer.MAX_VALUS
,可能会堆积大量的请求,从而导致 OOM。CachedThreadPool
和ScheduledThreadPool
,允许的创建线程数量为Integer.MAX_VALUS
,可能会创建大量的线程,从而导致 OOM。
3【强制】 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
4【强制】 并发修改同一记录时,避免更新丢失,要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用version
作为更新依据。
- 说明:如果每次访问冲突概率小于
20%
,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3
次。
5【强制】 多线程并行处理定时任务时,Timer
运行多个TimerTask
时,只要其中之一没有捕获抛出异常,其他任务便会自动终止运行,使用ScheduledExecutorService
则没有这个问题。
(七)控制语句
1【推荐】 尽量少用else
,if-eles
的方式可以改写成:
if(condition){...return obj;}// 接着写 else 的业务逻辑代码
- 说明:如果非得使用
if()...else if()...else...
方式表达逻辑,请勿超过 3 层,超过请使用状态设计模式。 - 正例:逻辑上超过 3 层的
if-eles
代码可以使用卫语句,或者状态模式来实现。
2【推荐】 除常用方法(如getXxx/isXxx
)等外,不用在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
- 说明:很多
if
语句内的逻辑相当复杂,阅读者需要分析条件表达式的最终结果,才能确定什么样的条件执行什么样的语句,那么,如果阅读者分析逻辑表达式错误呢? - 正例:
// 伪代码如下boolean existed = (file.open(fileName, "w") != null) && (...) || (...);if(existed){...}
- 反例:
if((file.open(fileName, "w") != null) && (...) || (...)){...}
(八)注释规约
1【参考】 注释掉的代码尽快要配合说明,而不是简单的注释掉。
- 说明:代码被注释掉有两种可能性,
1)
后续会恢复此段代码逻辑;2)
永久不用。前者如果没有备注信息,难以知悉注释动机;后者建议直接删掉(代码仓库保持了历史代码)。
2【参考】 特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描,经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。
- 说明 1:待办事宜
(TODO):(标记人,标记时间,[预处理时间])
,表示需要实现,但目前还未实现的功能。这实际上是一个 Javadoc 的标签,目前的 Javadoc 还没有实现,但已经被广泛使用。只能应用于类,接口和方法,原因就在于它是一个 Javadoc 标签。 - 说明 2:错误,不能工作
(FIXME):(标记人,标记时间,[预处理时间])
,在注释中用FIXME
标记某代码是错误的,而且不能工作,需要及时纠正的情况。
(九)其他
1【强制】 后台输送给页面的变量必须加$!{var}
——中间的感叹号。
- 说明:如果
var = null
或者不存在,那么${var}
会直接显示在页面上。
2【强制】 注意Math.random()
这个方法返回时double
类型, 注意取值的范围0 ≤ x < 1
(能够取到零值,注意除零异常),如果想获取整数类型的随机数,不要将x
放大10
的若干倍然后取整,直接使用Random
对象的nextInt
或者nextLong
方法。
3【强制】 获取当前毫秒数System.currentTimeMillis();
而不是new Date().getTime();
- 说明:如果想获取更加精确的纳秒时间值,用
System.nanoTime()
。在 JDK8 中,针对统计时间等场景,推荐使用Instant
类。
二、异常日志
(一)异常处理
1【强制】 不能在finally
块中使用return
,finally
块中的return
返回后方法结束执行,不会再执行try
块中的 return
语句。
2【推荐】 防止 NPE 是程序员的基本修养,注意 NPE 产生的场景:
- 返回类型为包装数据类型,有可能是
null
,返回int
值时注意判空; - 数据库的查询语句可能为
null
; - 集合里的元素即使
isNotEmpty
,取出的数据元素可能为null
; - 远程调用返回对象,一律要求进行 NPE 判断;
- 对于 Session 中获取的数据,建议 NPE 检查,避免空指针。
3【参考】 避免出现重复的代码(Dont’t Repeat Yourself),即 DRY 原则。
- 说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是共用模块。
- 正例:一个类中有多个
public
方法,都需要进行数行相同的参数校验操作,这个时候请抽取, private boolean checkParam(DTO dto){...}
(二)日志规约
1【强制】 应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架 SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);
2【强制】 对trace/debug/info
级别的日志输出,必须使用条件输出形式或者使用占位符的方式。
- 说明:
logger.debug("Processing trade with id: " id " symbol: " symbol);
如果日志级别是warn
,上述日志不会打印,但是会执行字符串拼接操作,如果symbol
是对象,会执行toString()
方法,浪费了系统资源,执行了上述操作,最终日志却没有打印。 - 正例:(条件)
if(logger.isDebugEnabled()){logger.debug("Processing trade with id: " id " symbol: " symbol);}
- 正例:(占位符)
logger.debug("Processing trade with id: {} symbol: {}", id, symbol);
- 1
3【推荐】 谨慎地记录日志。生产环节禁止输出debug
日志;有选择地输出info
日志;如果使用warn
来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。
- 说明:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
三、MySQL 规约
(一)建表规约
1【强制】 表名、字段名必须使用小写字母或数字;禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
- 正例:
getter_admin, task_config, level3_name
- 反例:
GetterAdmin, taskConfig, level_3_name
2【强制】 唯一索引名为uk_字段名
;普通索引名则为idx_字段名
。
- 说明:
uk_
即unique key
;idx_
即index
的简称。
3【强制】 小数类型为decimal
,禁止使用float
和double
。
- 说明:
float
和double
在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不正确的结果。如果存储的数据范围超过decimal
的范围,建议将数据拆成整数和小数分开存储。
4【强制】 varchar
是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为text
,独立出来一张表,用主键来对应,避免影响其他字段索引效率。
5【强制】 表必备三字段:id
,gmt_create
,gmt_modified
。
- 说明:其中
id
必为主键,类型为unsigned bigint
、单表时自增、步长为1
;gmt_create
和gmt_modified
的类型均为date_time
类型。
(二)索引规约
1【强制】 业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。
- 说明:不用以为唯一索引影响了
insert
速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验和控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。
2【强制】 超过三个表禁止join
。需要join
的字段,数据类型保持绝对一致;多表关联查询时,保证被关联的字段需要有索引。
- 说明:即使双表
join
也要注意表索引、SQL 性能。
3【强制】 在varchar
字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。
- 说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90% 以上,可以使用
count(distinct left(列名,索引长度))/count(*)
的区分度来确定。
(三)SQL规约
1【强制】 不要使用count(列名)
或count(常量)
来替代count(*)
,count(*)
就是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。
- 说明:
count(*)
会统计值为 NULL 的行,而count(列名)
不会统计此列为 NULL 值的行。
2【强制】 count(distinct col)
计算该列除 NULL 之外的不会重复数量。注意count(distinct col1, col2)
如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为0
。
(四)ORM规约
1【强制】 在表查询中,一律不要使用*
作为查询的字段列表,需要哪些字段必须明确写明。
- 说明:1)增加查询分析器解析成本;2)增减字段容易与
resultMap
配置不一致。
2【强制】 不要用resultClass
当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义;反过来,每一个表也必然有一个与之对应。
- 说明:配置映射关系,使字段与 DO 类解耦,方便维护。
3【强制】 xml
配置中参数注意使用:#{}
,#param#
不要使用${}
,此种方式容易出现 SQL 注入。
四、其他
1【强制】 依赖于一个二方库群时,必须定义一个统一版本变量,避免版本号不一致。
- 说明:依赖
springframework-core, -context, -beans
,它们都是同一个版本,可以定义一个变量来保持版本:${spring.version}
,定义依赖的时候,引用该版本。
2【强制】 禁止在子项目的POM
依赖中出现相同的GroupId
,相同的ArtifiactId
,但是不同的Version
。
- 说明:在本地调试时会使用各子项目指定的版本号,但是合并成一个
war
,只能有一个版本号出现在最后的lib
目录中。曾经出现过线下调试是正确的,发布到线上出故障的先例。
3【强制】 用户输入的 SQL 参数严格使用参数绑定或者 METADATA 字段值限定,防止 SQL 注入。