Java开发手册

一、编程规约

(一)命名风格

1.【强制】代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。

1
反例:_name / __name / $name / name_ / name$ / name__

2.【强制】所有编程相关的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。

1
2
3
说明:正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,纯拼音命名方式更要避免采用。
正例:ali / alibaba / taobao / cainiao/ aliyun/ youku / hangzhou 等国际通用的名称,可视同英文。
反例:DaZhePromotion [打折] / getPingfenByName() [评分] / int 某变量 = 3

3.【强制】类名使用 UpperCamelCase 风格,但以下情形例外:DO / BO / DTO / VO / AO / PO / UID 等。

1
2
正例:ForceCode / UserDO / HtmlDTO / XmlService / TcpUdpDeal / TaPromotion
反例:forcecode / UserDo / HTMLDto / XMLService / TCPUDPDeal / TAPromotion

4.【强制】方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格。

1
正例: localValue / getHttpMessage() / inputUserId

5.【强制】常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。

1
2
正例:MAX_STOCK_COUNT / CACHE_EXPIRED_TIME
反例:MAX_COUNT / EXPIRED_TIME

6.【强制】抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾;测试类命名以它要测试的类的名称开始,以 Test 结尾。

7.【强制】类型与中括号紧挨相连来表示数组。

1
2
正例:定义整形数组 int[] arrayDemo;
反例:在 main 参数中,使用 String args[]来定义。

8.【强制】POJO 类中的任何布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误。

1
2
反例:定义为基本数据类型 Boolean isDeleted 的属性,它的方法也是 isDeleted(),框架在反向解析的时候,“误以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。
正例:是否删除:deleted / 是否可用:enable

9.【强制】杜绝完全不规范的缩写,避免望文不知义。

1
反例:AbstractClass“缩写”命名成 AbsClass;condition“缩写”命名成 condi,此类随意缩写严重降低了代码的可阅读性。

10.【推荐】接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性。尽量不要在接口里定义变量,如果一定要定义变量,确定与接口方法相关,并且是整个应用的基础常量。

1
2
3
4
正例:接口方法签名 void commit();
接口基础常量 String COMPANY = "alibaba";
反例:接口方法定义 public abstract void f();
说明:JDK8 中接口允许有默认实现,那么这个 default 方法,是对所有实现类都有价值的默认实现。

11.【参考】各层命名规约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
A) Controller 层:
1)请求方式,获取信息用GET,提交数据用POST,修改数据用PUT,删除数据用DELETE。
2)接口地址,用使用驼峰式命名。
3)请求参数,使用@RequestParam接收独立参数,注意是否必填,不用使用实体接收,更不能使用Map接收;使用@API说明参数含义及取值范围。
B) Service/DAO 层方法命名规约:
1) 如果返回结果是唯一的,使用getBy…命名;如果返回结果是列表,使用queryBy…命名。
2) 查询列表用queryList开头,查询分页用query开头。
3) 获取统计值的方法用 count 做前缀。
4) 插入的方法用 save/insert 做前缀。
5) 物理删除的方法用 remove/delete 做前缀,逻辑删除使用 del 做前缀。
6) 修改的方法用 update 做前缀。
B) 领域模型命名规约:
1) 数据对象:xxxDO,xxx 即为数据表名。
2) 数据传输对象:xxxDTO,xxx 为业务领域相关的名称。
3) 展示对象:xxxVO,xxx 一般为网页名称。
D)POJO 是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO。

(二)常量定义

1.【强制】不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。

1
2
3
4
反例:
//本例中同学A定义了缓存的key,然后缓存提取的同学B使用了Id#taobao来提取,少了下划线,导致故障。
String key = "Id#taobao_" + tradeId;
cache.put(key, value);

2.【强制】在 long 或者 Long 赋值时,数值后使用大写的 L,不能是小写的 l,小写容易跟数字
混淆,造成误解。

1
说明:Long a = 2l; 写的是数字的 21,还是 Long 型的 2

3.【推荐】不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护。

1
2
说明:大而全的常量类,杂乱无章,使用查找功能才能定位到修改的常量,不利于理解,也不利于维护。
正例:缓存相关常量放在类 CacheConsts 下;系统配置相关常量放在类 ConfigConsts 下。

(三)代码格式

1.【强制】提交代码前,一定要对代码进行格式化。

1
2
3
说明:在开发工具IntelliJ IDEA中
使用:Ctrl + Alt + L 进行代码格式化
使用:Ctrl + Alt + O 自动导包及去除多余不用的包

(四)OOP 规约

1.【强制】避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。
2.【强制】所有的覆写方法,必须加@ Override 注解。

1
说明:getObject()与 get0bject()的问题。一个是字母的 O,一个是数字的 0,加 @Override 可以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,其实现类会马上编译报错。

3.【强制】不能使用过时的类或方法。

1
说明:java.net.URLDecoder 中的方法 decode(String encodeStr) 这个方法已经过时,应该使用双参数decode(String source, String encode)。接口提供方既然明确是过时接口,那么有义务同时提供新的接口;作为调用方来说,有义务去考证过时方法的新实现是什么。

4.【强制】 Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调 equals 。

1
2
3
正例:"test".equals(object);
反例:object.equals("test");
说明:推荐使用 java.util.Objects#equals(JDK7 引入的工具类)。

5.【强制】所有整型包装类对象之间值的比较,全部使用 equals 方法比较。

1
说明:对于 Integer var = ? 在-128 至 127 之间的赋值,Integer 对象是在 IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。

6.【强制】任何货币金额,均以最小货币单位且整型类型来进行存储。

1
说明:金额使用 Long 类型来存储,单位是分

(五)日期时间

1.【强制】日期格式化时,传入 pattern 中表示年份统一使用小写的 y。

1
2
3
说明:日期格式化时,yyyy 表示当天所在的年,而大写的 YYYY 代表是 week in which year(JDK7之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的 YYYY就是下一年。
正例:表示日期和时间的格式如下所示:
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")

2.【强制】在日期格式中分清楚大写的 M 和小写的 m,大写的 H 和小写的 h 分别指代的意义。

1
2
3
4
5
说明:日期格式中的这两对字母表意如下:
1) 表示月份是大写的 M;
2) 表示分钟则是小写的 m;
3) 24 小时制的是大写的 H;
4) 12 小时制的则是小写的 h。

3.【强制】获取当前毫秒数:System.currentTimeMillis(); 而不是 new Date().getTime()。

1
说明:如果想获取更加精确的纳秒级时间值,使用 System.nanoTime 的方式。在 JDK8 中,针对统计时间等场景,推荐使用 Instant 类。

4.【强制】不允许在程序任何地方中使用:1)java.sql.Date 2)java.sql.Time 3)java.sql.Timestamp。

1
2
说明:第 1 个不记录时间,getHours()抛出异常;第 2 个不记录日期,getYear()抛出异常;第 3 个在构造方法 super((time/1000)*1000),fastTime 和 nanos 分开存储秒和纳秒信息。
反例:java.util.Date.after(Date)进行时间比较时,当入参是 java.sql.Timestamp 时,会触发 JDK BUG(JDK9 已修复),可能导致比较时的意外结果。

5.【强制】不要在程序中写死一年为 365 天,避免在公历闰年时出现日期转换错误或程序逻辑错误。

1
2
3
4
5
6
7
8
9
10
11
12
正例:
// 获取今年的天数
int daysOfThisYear = LocalDate.now().lengthOfYear();
// 获取指定某年的天数
LocalDate.of(2011, 1, 1).lengthOfYear();
反例:
// 第一种情况:在闰年 366 天时,出现数组越界异常
int[] dayArray = new int[365];
// 第二种情况:一年有效期的会员制,今年1月26日注册,硬编码365返回的却是1月25日
Calendar calendar = Calendar.getInstance();
calendar.set(2020, 1, 26);
calendar.add(Calendar.DATE, 365);

6.【推荐】使用枚举值来指代月份。如果使用数字,注意 Date,Calendar 等日期相关类的月份 month 取值在 0-11 之间。

1
2
说明:参考 JDK 原生注释,Month value is 0-based. e.g., 0 for January.
正例:Calendar.JANUARY,Calendar.FEBRUARY,Calendar.MARCH 等来指代相应月份来进行传参或比较。

(六)集合处理

1.【强制】判断所有集合内部的元素是否为空,使用 isEmpty()方法,而不是 size()==0 的方式。

1
2
3
4
5
6
说明:前者的时间复杂度为 O(1),而且可读性更好。
正例:
Map<String, Object> map = new HashMap<>();
if(map.isEmpty()) {
System.out.println("no element in this map.");
}

2.【强制】在使用 java.util.stream.Collectors 类的 toMap()方法转为 Map 集合时,一定要使用含有参数类型为 BinaryOperator,参数名为 mergeFunction 的方法,否则当出现相同 key 值时会抛出 IllegalStateException 异常。

1
2
3
4
5
6
7
8
9
10
11
12
说明:参数 mergeFunction 的作用是当出现 key 重复时,自定义对 value 的处理策略。
正例:
List<Pair<String, Double>> pairArrayList = new ArrayList<>(3);
pairArrayList.add(new Pair<>("version", 6.19));
pairArrayList.add(new Pair<>("version", 10.24));
pairArrayList.add(new Pair<>("version", 13.14));
// 生成的 map 集合中只有一个键值对:{version=13.14}
Map<String, Double> map = pairArrayList.stream().collect(Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));
反例:
String[] departments = new String[] {"iERP", "iERP", "EIBU"};
// 抛出 IllegalStateException 异常
Map<Integer, String> map = Arrays.stream(departments).collect(Collectors.toMap(String::hashCode, str -> str));

3.【强制】在使用 java.util.stream.Collectors 类的 toMap()方法转为 Map 集合时,一定要注意当 value 为 null 时会抛 NPE 异常。

1
2
3
4
5
6
7
8
9
10
11
12
说明:在 java.util.HashMap 的 merge 方法里会进行如下的判断:
if (value == null || remappingFunction == null) {
// ...
throw new NullPointerException();
}
反例:
List<Pair<String, Double>> pairArrayList = new ArrayList<>(2);
pairArrayList.add(new Pair<>("version1", 4.22));
pairArrayList.add(new Pair<>("version2", null));
Map<String, Double> map = pairArrayList.stream().collect(
// 抛出 NullPointerException 异常
Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));

4.【强制】使用 Map 的方法 keySet() / values() / entrySet() 返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常。

5.【强制】使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组。

1
2
3
4
5
6
7
8
9
10
11
反例:直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[] 类,若强转其它类型数组将出现 ClassCastException 错误。
正例:
List<String> list = new ArrayList<>(2);
list.add("guan");
list.add("bao");
String[] array = list.toArray(new String[0]);
说明:使用 toArray 带参方法,数组空间大小的 length,
1) 等于 0,动态创建与 size 相同的数组,性能最好。
2) 大于 0 但小于 size,重新创建大小等于 size 的数组,增加 GC 负担。
3) 等于 size,在高并发情况下,数组创建完成之后,size 正在变大的情况下,负面影响与 2 相同。
4) 大于 size,空间浪费,且在 size 处插入 null 值,存在 NPE 隐患。

6.【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
正例:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (删除元素的条件) {
iterator.remove();
}
}
反例:
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?

7.【推荐】集合泛型定义时,在 JDK7 及以上,使用 diamond 语法或全省略。

1
2
3
4
5
6
说明:菱形泛型,即 diamond,直接使用<>来指代前边已经指定的类型。
正例:
// diamond 方式,即<>
HashMap<String, String> userCache = new HashMap<>(16);
// 全省略方式
ArrayList<User> users = new ArrayList(10);

8.【推荐】集合初始化时,指定集合初始值大小。

1
2
3
说明:HashMap 使用 HashMap(int initialCapacity) 初始化,如果暂时无法确定集合大小,那么指定默认值(16)即可。
正例:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。
反例:HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容量 7 次被迫扩大,resize 需要重建 hash 表。当放置的集合元素个数达千万级别时,不断扩容会严重影响性能。

9.【推荐】使用 entrySet 遍历 Map 类集合 KV ,而不是 keySet 方式进行遍历。

1
2
说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8,使用Map.forEach 方法。
正例:values()返回的是 V 值集合,是一个 list 集合对象;keySet()返回的是 K 值集合,是一个 Set 集合对象;entrySet()返回的是 K-V 值组合集合。

10.【推荐】高度注意 Map 类集合 K/V 能不能存储 null 值的情况,如下表格:

集合类 Key Value Super 说明
Hashtable 不允许为 null 不允许为 null Dictionary 线程安全
ConcurrentHashMap 不允许为 null 不允许为 null AbstractMap 锁分段技术(JDK8:CAS)
TreeMap 不允许为 null 允许为 null AbstractMap 线程不安全
HashMap 允许为 null 允许为 null AbstractMap 线程不安全
1
反例:由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,而事实上,存储 null 值时会抛出 NPE 异常。

11.【参考】利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的
contains()进行遍历去重或者判断包含操作。

(七)并发处理

1.【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

1
2
说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。
如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

2.【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

1
2
3
4
5
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

3.【强制】 SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static ,必须加锁,或者使用 DateUtils 工具类。

1
2
3
4
5
6
7
8
9
正例:注意线程安全,使用 DateUtils。亦推荐如下处理:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,
DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。

4.【强制】必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 try-finally 块进行回收。

1
2
3
4
5
6
7
正例:
objectThreadLocal.set(userInfo);
try {
// ...
} finally {
objectThreadLocal.remove();
}

5.【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。

1
说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。

6.【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。

1
说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。

7.【强制】在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
说明一:如果在 lock 方法与 try 代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。
说明二:如果 lock 方法在 try 代码块之内,可能由于其它方法抛出异常,导致在 finally 代码块中,unlock 对未加锁的对象解锁,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),抛出 IllegalMonitorStateException 异常。
说明三:在 Lock 对象的 lock 方法实现中可能抛出 unchecked 异常,产生的后果与说明二相同。
正例:
Lock lock = new XxxLock();
// ...
lock.lock();
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}
反例:
Lock lock = new XxxLock();
// ...
try {
// 如果此处抛出异常,则直接执行 finally 代码块
doSomething();
// 无论加锁是否成功,finally 代码块都会执行
lock.lock();
doOthers();
} finally {
lock.unlock();
}

8.【强制】在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
说明:Lock 对象的 unlock 方法在执行时,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),如果当前线程不持有锁,则抛出 IllegalMonitorStateException 异常。
正例:
Lock lock = new XxxLock();
// ...
boolean isLocked = lock.tryLock();
if (isLocked) {
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}
}

9.【强制】并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。

1
说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3 次。

(八)控制语句

1.【强制】在一个 switch 块内,每个 case 要么通过 continue/break/return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default语句并且放在最后,即使它什么代码也没有。

1
说明:注意 break 是退出 switch 语句块,而 return 是退出方法体。

2.【强制】当 switch 括号内的变量类型为 String 并且此变量为外部参数时,必须先进行null判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
反例:如下的代码输出是什么?(报:java.lang.NullPointerException)
public class SwitchString {

public static void main(String[] args) {
method(null);
}

public static void method(String param) {
switch (param) {
// 肯定不是进入这里
case "sth":
System.out.println("it's sth");
break;
// 也不是进入这里
case "null":
System.out.println("it's null");
break;
// 也不是进入这里
default:
System.out.println("default");
}
}
}

3.【强制】在 if / else / for / while / do 语句中必须使用大括号。

1
说明:即使只有一行代码,禁止不采用大括号的编码方式:if (condition) statements;

4.【强制】三目运算符 condition? 表达式 1 : 表达式 2 中,高度注意表达式 1 和 2 在类型对齐时,可能抛出因自动拆箱导致的 NPE 异常。

1
2
3
4
5
6
7
8
9
10
说明:以下两种场景会触发类型对齐的拆箱操作:
1) 表达式 1 或表达式 2 的值只要有一个是原始类型。
2) 表达式 1 或表达式 2 的值的类型不一致,会强制拆箱升级成表示范围更大的那个类型。
反例:
Integer a = 1;
Integer b = 2;
Integer c = null;
Boolean flag = false;
// a*b 的结果是 int 类型,那么 c 会强制拆箱成 int 类型,抛出 NPE 异常
Integer result=(flag ? a*b : c);

5.【强制】在高并发场景中,避免使用”等于”判断作为中断或退出的条件。

1
2
说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替。
反例:判断剩余奖品数量等于 0 时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数,这样的话,活动无法终止。

6.【推荐】表达异常的分支时,少用 if-else 方式 ,这种方式可以改写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
	if (condition) {
...
return obj;
}
// 接着写 else 的业务逻辑代码;
说明:如果非使用 if()...else if()...else...方式表达逻辑,避免后续代码维护困难,请勿超过 3 层。
正例:超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现,其中卫语句。示例如下:
public void findBoyfriend(Man man) {
if (man.isUgly()) {
System.out.println("本姑娘是外貌协会的资深会员");
return;
}
if (man.isPoor()) {
System.out.println("贫贱夫妻百事哀");
return;
}
if (man.isBadTemper()) {
System.out.println("银河有多远,你就给我滚多远");
return;
}
System.out.println("可以先交往一段时间看看");
}

7.【推荐】除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。

1
2
3
4
5
6
7
8
9
10
11
12
说明:很多 if 语句内的逻辑表达式相当复杂,与、或、取反混合运算,甚至各种方法纵深调用,理解成本非常高。如果赋值一个非常好理解的布尔变量名字,则是件令人爽心悦目的事情。
正例:
// 伪代码如下
final boolean existed = (file.open(fileName, "w") != null) && (...) ||(...);
if (existed) {
//...
}
反例:
public final void acquire (long arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}

8.【推荐】循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的 try-catch 操作 ( 这个 try-catch 是否可以移至循环体外 )。

9.【推荐】避免采用取反逻辑运算符。

1
2
3
说明:取反逻辑不利于快速理解,并且取反逻辑写法必然存在对应的正向逻辑写法。
正例:使用 if (x < 628) 来表达 x 小于 628。
反例:使用 if (!(x >= 628)) 来表达 x 小于 628。

10.【推荐】接口入参保护,这种场景常见的是用作批量操作的接口。

1
反例:某业务系统,提供一个用户批量查询的接口,API 文档上有说最多查多少个,但接口实现上没做任何保护,导致调用方传了一个 1000 的用户 id 数组过来后,查询信息后,内存爆了。

(九)注释规约

1.【强制】类、类属性、类方法的注释必须使用 Javadoc 规范,使用/**内容*/格式,不得使用
// xxx 方式。

1
说明:在 IDE 编辑窗口中,Javadoc 方式会提示相关注释,生成 Javadoc 可以正确输出相应释;在 IDE 中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。

2.【强制】所有的抽象方法 ( 包括接口中的方法 ) 必须要用 Javadoc 注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。

1
说明:对子类的实现要求,或者调用注意事项,请一并说明。

3.【强制】所有的类都必须添加创建者和创建日期。

4.【强制】方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使
/* */注释,注意与代码对齐。

5.【推荐】与其“半吊子”英文来注释,不如用中文注释把问题说清楚。专有名词与关键字保持英文原文即可。

1
反例:“TCP 连接超时”解释成“传输控制协议连接超时”,理解反而费脑筋。

6.【推荐】代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。

1
说明:代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义。

7.【推荐】在类中删除未使用的任何字段和方法;在方法中删除未使用的任何参数声明与内部变量。

8.【参考】特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描,经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。

1
2
3
4
1) 待办事宜(TODO):(标记人,标记时间,[预计处理时间])
表示需要实现,但目前还未实现的功能。这实际上是一个 Javadoc 的标签,目前的 Javadoc 还没有实现,但已经被广泛使用。只能应用于类,接口和方法(因为它是一个 Javadoc 标签)。
2) 错误,不能工作(FIXME):(标记人,标记时间,[预计处理时间])
在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正的情况。

(十)其它

1.【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。

1
说明:不要在方法体内定义:Pattern pattern = Pattern.compile(“规则”);

2.【推荐】任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存。

3.【推荐】及时清理不再使用的代码段或配置信息。

1
2
3
4
5
6
7
8
说明:对于垃圾代码或过时配置,坚决清理干净,避免程序过度臃肿,代码冗余。
正例:对于暂时被注释掉,后续可能恢复使用的代码片断,在注释代码上方,统一规定使用三个斜杠(///)来说明注释掉代码的理由。如:
public static void hello() {
/// 业务方通知活动暂停
// Business business = new Business();
// business.active();
System.out.println("it's finished");
}

二、异常日志

1.【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。

2.【强制】事务场景中,抛出异常被 catch 后,如果需要回滚,一定要注意手动回滚事务。

3.【强制】finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。

1
说明:如果 JDK7 及以上,可以使用 try-with-resources 方式。

4.【强制】不要在 finally 块中使用 return。

1
2
3
4
5
6
7
8
9
10
11
12
说明:try 块中的 return 语句执行成功后,并不马上返回,而是继续执行 finally 块中的语句,如果此处存在 return 语句,则在此直接返回,无情丢弃掉 try 块中的返回点。
反例:
private int x = 0;
public int checkReturn() {
try {
// x 等于 1,此处不返回
return ++x;
} finally {
// 返回的结果是 2
return ++x;
}
}

5.【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。

1
说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。

6.【推荐】防止 NPE ,是程序员的基本修养,注意 NPE 产生的场景:

1
2
3
4
5
6
7
8
1) 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。
反例:public int f() { return Integer 对象}, 如果为 null,自动解箱抛 NPE。
2) 数据库的查询结果可能为 null
3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null
4) 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
5) 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
6) 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。
正例:使用 JDK8 的 Optional 类来防止 NPE 问题。

7.【参考】对于公司外的 http/api 开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间 RPC 调用优先考虑使用 Result 方式,封装 isSuccess()方法、“错误码”、“错误简短信息”;而应用内部推荐异常抛出。

1
2
3
说明:关于 RPC 方法返回方式使用 Result 方式的理由:
1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
2)如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。

三、MySQL 数据库

(一)建表规约

1.【强制】数据库字符集采用utf8mb4。表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字下划线形式。字段如果是单字节,需要在末尾加上下划线;如果是多字节,则不需要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- ----------------------------
-- Table structure for member
-- ----------------------------
DROP TABLE IF EXISTS `member`;
CREATE TABLE `member` (
`id_` bigint(20) NOT NULL COMMENT '编号',
`phone_` varchar(20) DEFAULT NULL COMMENT '手机号',
`name_` varchar(20) DEFAULT NULL COMMENT '姓名',
`flag_` tinyint(1) NOT NULL DEFAULT '0' COMMENT '标识字段:0,1',
`enable_` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否可用:0:不可用,1:可用',
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '创建人',
`creator_id` bigint DEFAULT NULL COMMENT '创建人ID',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '更新人',
`updater_id` bigint DEFAULT NULL COMMENT '更新人ID',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark_` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id_`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户表';

2.【强制】表达是与否概念的字段,必须使用 xxx_ 的方式命名,数据类型是tinyint(1 表示是,0 表示否)。

1
说明:表达逻辑删除的字段名 enable_,1 表示可用,0 表示不可用。

3.【强制】表名不使用复数名词

1
说明:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。

4.【强制】禁用保留字,如 desc、range、match、delayed 等,请参考 MySQL 官方保留字。

5.【强制】唯一索引名为uk_字段名;普通索引名则为idx_字段名。

1
说明:uk_ 即 unique key;idx_ 即 index 的简称。

6.【强制】小数类型为 decimal,禁止使用 float 和 double。金额转为为分进行存储,存储类型为bigint(20).

1
说明:在存储的时候,float 和 double 都存在精度损失的问题,很可能在比较值的时候,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储。

7.【强制】如果存储的字符串长度几乎相等,使用 char 定长字符串类型。

8.【强制】 varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度
大于此值,定义字段类型为 text ,独立出来一张表,用主键来对应,避免影响其它字段索引效率。

(二)SQL 语句

1.【强制】在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。

1
说明:1)增加查询分析器解析成本。2)增减字段容易与 resultMap 配置不一致。3)无用字段增加网络消耗,尤其是 text 类型的字段。

2.【强制】不要使用count(列名)count(常量)来替代count(*)count(*)是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。

1
说明:count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行。

3.【强制】count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col1,col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0。

4.【强制】当某一列的值全是 NULL 时,count(col)的返回结果为 0,但 sum(col)的返回结果为NULL,因此使用 sum()时需注意 NPE 问题。

1
正例:可以使用如下方式来避免 sum 的 NPE 问题:SELECT IFNULL(SUM(column), 0) FROM table;

5.【强制】使用 ISNULL() 来判断是否为 NULL 值。

1
2
3
4
5
6
7
说明:NULL 与任何值的直接比较都为 NULL。
1) NULL<>NULL 的返回结果是 NULL,而不是 false。
2) NULL=NULL 的返回结果是 NULL,而不是 true。
3) NULL<>1 的返回结果是 NULL,而不是 true。
反例:在 SQL 语句中,如果在 null 前换行,影响可读性。select * from table where column1 is null and
column3 is not null; 而`ISNULL(column)`是一个整体,简洁易懂。从性能数据上分析,`ISNULL(column)`
执行效率更快一些。

6.【强制】不得使用外键与级联,一切外键概念必须在应用层解决。

1
说明:(概念解释)学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。

7.【强制】禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。

8.【强制】数据订正(特别是删除或修改记录操作)时,要先 select ,避免出现误删除,确认无
误才能执行更新语句。

9.【强制】对于数据库中表记录的查询和变更,只要涉及多个表,都需要在列名前加表的别名(或
表名)进行限定。

1
2
3
说明:对多表进行查询记录、更新记录、删除记录时,如果对操作列没有限定表的别名(或表名),并且操作列在多个表中存在时,就会抛异常。
正例:select t1.name from table_first as t1 , table_second as t2 where t1.id=t2.id;
反例:在某业务中,由于多表关联查询语句没有加表的别名(或表名)的限制,正常运行两年后,最近在某个表中增加一个同名字段,在预发布环境做数据库变更后,线上查询语句出现出 1052 异常:Column 'name' in field list is ambiguous。

10.【推荐】 in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在 1000 个之内。