1. 质量规范
质量规范则更侧重于代码的内在质量和逻辑正确性,旨在提高代码的健壮性、可靠性和性能。
1.1 命名风格
- 所有编程相关的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
- 所有编程相关的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式
- 代码和注释中都要避免使用任何人类语言中的种族歧视性或侮辱性词语
- 类名使用UpperCamelCase风格,以下情形例外:DO / PO / DTO / BO / VO / UID等
- 方法名、参数名、成员变量、局部变量都统一使用lowerCamelCase风格
- 常量命名应该全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长
- 抽象类命名使用Abstract或Base开头;异常类命名使用Exception结尾,测试类命名以它要测试的类的名称开始,以Test结尾
- 类型与中括号紧挨相连来定义数组
- POJO类中的任何布尔类型的变量,都不要加is前缀,否则部分框架解析会引起序列化错
- 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式
- 避免在子父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可理解性降低
- 杜绝完全不规范的英文缩写,避免望文不知义
【反例】
public class ConfusingName {
protected int stock;
protected String xx;
// 非setter/getter的参数名称,不允许与本类成员变量同名
public void access(String anta) {
if (condition) {
final int age= 16;
// ...
}
for (int i = 0; i < 10; i++) {
// 在同一方法体中,不允许与其它代码块中的age命名相同
final int age= 54;
// ...
}
}
}
class Son extends ConfusingName {
// 不允许与父类的成员变量名称相同
private int stock;
}
【正例】
public class ConfusingName {
protected int stock;
protected String xx;
public void access(String fila) {
if (condition) {
final int time = 666;
// ...
}
for (int i = 0; i < 10; i++) {
final int age = 15978;
// ...
}
}
}
class Son extends ConfusingName {
private int id;
}
1.2 常量定义
- 不允许任何魔法值(即未经预先定义的常量)直接出现在代码中
- long或Long赋值时,数值后使用大写L,不能是小写l,小写容易跟数字混淆,造成误解
- 浮点数类型的数值后缀统一为大写的D或F
【反例】
public String getInfo(String userName) {
String key = "key"+userName;
long num = 0l;
double height = 175.5;
return StringUtils.EMPTY;
}
【正例】
public String getInfo(String userName) {
String key = Contents.PREFIX+userName;
long num = 0L;
double height = 175.5D;
return StringUtils.EMPTY;
}
1.3 代码格式
- 如果大括号内为空,简洁地写成{}即可,大括号中间无需换行和空格
- 左小括号和右边相邻字符之间不需要空格;右小括号和左边相邻字符之间也不需要空格;而左大括号前需要加空格
- if / for / while / switch / do等保留字与左右括号之间都必须加空格
- 任何二目、三目运算符的左右两边都需要加一个空格
- 采用4个空格缩进,禁止使用Tab字符
- 注释的双斜线与注释内容之间有且仅有一个空格
- 在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开
- 单行字符数限制不超过120个,超出需要换行
- 方法参数在定义和传入时,多个参数逗号后面必须加空格
- IDE的text file encoding设置为UTF-8;IDE中文件的换行符使用Unix格式,不要使用Windows格式
【反例】
public static void main(String[] args) {
// 缩进4个空格
String say = "hello";
// 运算符的左右必须有一个空格
int flag=0;
// 关键词if
if (flag == 0)
System.out.println(say);
// 左大括号前加空格且不换行;左大括号后换行
if (flag == 1) { System.out.println("world");}
// 右大括号前换行,右大括号后有else,不用换行
else {System.out.println("ok"); }
// 在右大括号后直接结束,则必须换行
}
【正例】
public static void main(String[] args) {
// 缩进4个空格
String say = "hello";
// 运算符的左右必须有一个空格
int flag = 0;
// 关键词if
if (flag == 0) {
System.out.println(say);
}
// 左大括号前加空格且不换行;左大括号后换行
if (flag == 1) {
System.out.println("world");
// 右大括号前换行,右大括号后有else,不用换行
} else {
// 在右大括号后直接结束,则必须换行
System.out.println("ok");
}
}
1.4 OOP规约
- 避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可
- 所有的覆写方法,必须加 @Override注解
- 相同参数类型,相同业务含义,才可以使用的可变参数,参数类型避免定义为Objec
- 外部正在调用的接口或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响
- 不能使用过时的类或方法
- Object的equals方法容易抛空指针异常,应使用常量或确定有值的对象来调用equals
- 所有整型包装类对象之间值的比较,全部使用equals方法比较
- 任何货币金额,均以最小货币单位且为整型类型进行存储
- 浮点数之间的等值判断,基本数据类型不能使用 == 进行比较,包装数据类型不能使用equals进行判断
- BigDecimal的等值比较应使用compareTo() 方法,而不是equals() 方法
- 定义数据对象DO类时,属性类型要与数据库字段类型相匹配
- 禁止使用构造方法BigDecimal(double) 的方式把double值转化为BigDecimal对象
- 定义DO / PO / DTO / VO等POJO类时,不要设定任何属性默认值
- 序列化类新增属性时,请不要修改serialVersionUID字段,避免反序列失败
- 构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在init方法中
- POJO类必须写toString方法
- 禁止在POJO类中,同时存在对应属性xxx的isXxx() 和getXxx() 方法
【反例】
请参照文字说明
【正例】
请参照文字说明
1.5 日期时间
- 日期格式化时,传入pattern中表示年份统一使用小写的y
- 在日期格式中分清楚大写的M和小写的m,大写的H和小写的h分别指代的意义
- 获取当前毫秒数:System.currentTimeMillis();而不是new Date().getTime()
- 不允许在程序任何地方中使用:
- java.sql.Date
- java.sql.Time
- java.sql.Timestamp
- 禁止在程序中写死一年为365天,避免在公历闰年时出现日期转换错误或程序逻辑错误
【反例】
//时间格式化错误,YYYY
static final SimpleDateFormat FORMATTER = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss")
//获取时间戳,底层还是使用System.currentTimeMillis()
Long time = new Date().getTime()
【正例】
static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
Long time = System.currentTimeMillis()
1.6 集合处理
- 只要覆写equals,就必须覆写hashCode
- 判断所有集合内部的元素是否为空,使用isEmpty() 方法,而不是size() == 0的方式
- 在使用java.util.stream.Collectors类的toMap() 方法转为Map集合时,一定要使用参数类型为BinaryOperator,参数名为mergeFunction的方法,否则当出现相同key时会抛出IllegalStateException异常
- 在使用java.util.stream.Collectors类的toMap() 方法转为Map集合时,一定要注意当value为null时会抛NPE异常
- ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException异常:java.util.RandomAccessSubList cannot be cast to java.util.ArrayList
- 使用Map的方法keySet() / values() / entrySet() 返回集合对象时,不可以对其进行添加元素操作,否则会抛出UnsupportedOperationException异常
- Collections类返回的对象,如:emptyList() / singletonList() 等都是immutable list,不可对其进行添加或者删除元素的操作
- 在subList场景中,高度注意对父集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生ConcurrentModificationException异常
- 使用集合转数组的方法,必须使用集合的toArray(T[] array),传入的是类型完全一致、长度为0的空数组
- 使用Collection接口任何实现类的addAll() 方法时,要对输入的集合参数进行NPE判断
- 使用工具类Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法,它的add / remove / clear方法会抛出UnsupportedOperationException异常
- 泛型通配符<? extends T>来接收返回的数据,此写法的泛型集合不能使用add方法, 而<? super T>不能使用get方法,两者在接口调用赋值的场景中容易出错
- 在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行instanceof判断,避免抛出ClassCastException异常
- 不要在foreach循环里进行元素的remove / add操作。remove元素请使用iterator方式, 如果并发操作,需要对iterator对象加锁
【反例】
for (String item : list) {
if ("a".equals(item)) {
list.remove(item);
}
}
【正例】
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (Constants.A.equals(item)) {
iterator.remove();
}
}
1.7 并发处理
- 获取单例对象需要保证线程安全,其中的方法也要保证线程安全
- 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯
- 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
- 线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
- SimpleDateFormat是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用DateUtils工具类
- 必须回收自定义的ThreadLocal变量记录的当前线程的值,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题
- 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁
- 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁
- 在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁
- 在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同
- 并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用version作为更新依据
- 多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用ScheduledExecutorService则没有这个问题
【反例】
Lock lock = new XxxLock();
// ...
try {
// 如果此处抛出异常,则直接执行finally代码块
doSomething();
// 无论加锁是否成功,finally代码块都会执行
lock.lock();
doOthers();
} finally {
lock.unlock();
}
【正例】
Lock lock = new XxxLock();
// ...
lock.lock();
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}
1.8 控制语句
- 在一个switch块内,每个case要么通过continue / break / return等来终止,要么注释说明程序将继续执行到哪一个case为止;在一个switch块内,都必须包含一个default语句并且放在最后,即使它什么代码也没有
- 当switch括号内的变量类型为String并且此变量为外部参数时,必须先进行null判断
- 在if / else / for / while / do语句中必须使用大括号
- 三目运算符condition ? 表达式1:表达式2中,高度注意表达式1和2在类型对齐时,可能抛出因自动拆箱导致的NPE异常
- 在高并发场景中,避免使用“等于”判断作为中断或退出的条件
【反例】
public String getInfo(String userName,Integer code) {
//无default分支
switch (userName){
case "superAdmin":
return "-1";
case "admin":
return "0";
}
Integer a = 1;
Integer b = 2;
Integer c = null;
Boolean flag = false;
// a*b的结果是int类型,那么c会强制拆箱成int类型,抛出NPE异常
Integer result = (flag ? a * b : c);
return StringUtils.EMPTY;
}
【正例】
public String getInfo(String userName,Integer code) {
switch (userName){
case "superAdmin":
return "-1";
case "admin":
return "0";
default:
return "1";
}
int a = 1;
int b = 2;
int c = 3;
Boolean flag = false;
// a*b的结果是int类型,那么c会强制拆箱成int类型,抛出NPE异常
Integer result = (flag ? a * b : c);
}
1.9 注释规约
- 类、类属性、类方法的注释必须使用Javadoc规范,使用 /** 内容 */ 格式,不得使用 // xxx方式
- 所有的抽象方法(包括接口中的方法)必须要用Javadoc注释、除了返回值、参数异常说明外,还必须指出该方法做什么事情,实现什么功能
- 所有的类都必须添加创建者和创建日期
- 方法内部单行注释,在被注释语句上方另起一行,使用 // 注释。方法内部多行注释使用 /* */ 注释,注意与代码对齐
- 所有的枚举类型字段必须要有注释,说明每个数据项的用途
【反例】
//用户用户信息
private String getInfo(String userName,String userId){
return StringUtils.EMPTY;
}
【正例】
/**
* 获取用户信息
* @param userName 用户名
* @param userId 用户ID
* @return 返回信息字符串
*/
private String getInfo(String userName,String userId){
return StringUtils.EMPTY;
}
1.10 前后端规约
- 前后端交互的API,需要明确协议、域名、路径、请求方法、请求内容、状态码、响应体
- 前后端数据列表相关的接口返回,如果为空,则返回空数组[]或空集合{}
- 服务端发生错误时,返回给前端的响应信息必须包含HTTP状态码,errorCode、errorMessage、用户提示信息四个部分
- 在前后端交互的JSON格式数据中,所有的key必须为小写字母开始的lowerCamelCase风格,符合英文表达习惯,且表意完整
- errorMessage是前后端错误追踪机制的体现,可以在前端输出到type="hidden" 文字类控件中,或者用户端的日志中,帮助我们快速地定位出问题
- 对于需要使用超大整数的场景,服务端一律使用String字符串类型返回,禁止使用Long类型
- HTTP请求通过URL传递参数时,不能超过2048字节
- HTTP请求通过body传递内容时,必须控制长度,超出最大长度后,后端解析会出错
- 在翻页场景中,用户输入参数的小于1,则前端返回第一页参数给后端;后端发现用户输入的参数大于总页数,直接返回最后一页
- 服务器内部重定向必须使用forward;外部重定向地址必须使用URL统一代理模块生成,否则会因线上采用HTTPS协议而导致浏览器提示“不安全”,并且还会带来URL维护不一致的问题
【反例】
//请参照文字说明
【正例】
//请参照文字说明
1.11 其他
- 在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度
- 避免用ApacheBeanutils进行属性的copy,可采用spring工具类或者cglib进行浅拷贝
- velocity调用POJO类的属性时,直接使用属性名取值即可,模板引擎会自动按规范调用POJO 的getXxx(),如果是boolean基本数据类型变量(boolean命名不需要加is前缀),会自动调isXxx() 方法
- 后台输送给页面的变量必须加 $!{var} ——中间的感叹号
- 注意Math.random() 这个方法返回是double类型,注意取值的范围0 ≤ x < 1(能够取到零值,注意除零异常),如果想获取整数类型的随机数,不要将x放大10的若干倍然后取整,直接使用Random对象的nextInt或者nextLong方法
- 枚举enum(括号内)的属性字段必须是私有且不可变
【反例】
private String getInfo(String userName,String userId){
//方法体中定义的规则
Pattern pattern = Pattern.compile("规则");
}
【正例】
//方法体中定义的规则
static Pattern pattern = Pattern.compile("规则");
private String getInfo(String userName,String userId){
}