文章目录

1. 编程规约

(一)命名风格

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

说明:在本文MySQL规约中的建表约定第一条,表达是与否的变量采用is_xxx的命名方式,所以,需要在<resultMap>设置从is_xxx到xxx的映射关系。 反例:定义为基本数据类型Boolean isDeleted的属性,它的方法也是isDeleted(),框架在反向解析的时候,“误以为”对应的属性名称是deleted,导致属性获取不到,进而抛出异常。

2.【强制】避免在子父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可理解性降低。

【参考】各层命名规约: 

        A) Service/DAO层方法命名规约 

        1) 获取单个对象的方法用get做前缀。 

        2) 获取多个对象的方法用list做前缀,复数结尾,如:listObjects。 

        3) 获取统计值的方法用count做前缀。 

        4) 插入的方法用save/insert做前缀。 

        5) 删除的方法用remove/delete做前缀。 

        6) 修改的方法用update做前缀。 

        B) 领域模型命名规约

        1) 数据对象:xxxDO,xxx即为数据表名。 

        2) 数据传输对象:xxxDTO,xxx为业务领域相关的名称。 

        3) 展示对象:xxxVO,xxx一般为网页名称。 

        4) POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO。

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

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

(二)常量定义

        @TODO

(三)代码格式

         @TODO

(四)OOP规约

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

5.【强制】浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用equals来判断。

说明:浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。二进制无法精确表示大部分的十进制小数,具体原理参考《码出高效》。

/反例:
float a = 1.0F - 0.9F;
float b = 0.9F - 0.8F;
if (a == b) {
// 预期进入此代码块,执行其它业务逻辑
// 但事实上a==b的结果为false
}
Float x = Float.valueOf(a);
Float y = Float.valueOf(b);
if (x.equals(y)) {
// 预期进入此代码块,执行其它业务逻辑 但事实上equals的结果为false
}
// 正例: 
// (1)指定一个误差范围,两个浮点数的差值在此范围之内,则认为是相等的。 float a=1.0F-0.9F;
float b = 0.9F - 0.8F;
float diff = 1e-6F;
if (Math.abs(a - b) < diff) {
    System.out.println("true");
}
//(2)使用BigDecimal来定义值,再进行浮点数的运算操作。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
if (x.compareTo(y) == 0) {
    System.out.println("true");
}

6.【强制】如上所示BigDecimal的等值比较应使用compareTo()方法,而不是equals()方法。

说明:equals()方法会比较值和精度(1.0与1.00返回结果为false),而compareTo()则会忽略精度。

7.【强制】定义数据对象DO类时,属性类型要与数据库字段类型相匹配。

正例:数据库字段的bigint必须与类属性的Long类型相对应。 

反例:某个案例的数据库表id字段定义类型bigint unsigned,实际类对象属性为Integer,随着id越来越大,超过Integer的表示范围而溢出成为负数。

8.【强制】禁止使用构造方法BigDecimal(double)的方式把double值转化为BigDecimal对象。

说明:BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。如:BigDecimal g = new BigDecimal(0.1F); 实际的存储值为:0.10000000149 

正例:优先推荐入参为String的构造方法,或使用BigDecimal的valueOf方法,此方法内部其实执行了Double的toString,而Double的toString按double的实际能表达的精度对尾数进行了截断。 

BigDecimal recommend1 = new BigDecimal("0.1"); 

BigDecimal recommend2 = BigDecimal.valueOf(0.1);

9.关于基本数据类型与包装数据类型的使用标准如下:

        1) 【强制】所有的POJO类属性必须使用包装数据类型。 

        2) 【强制】RPC方法的返回值和参数必须使用包装数据类型。 

        3) 【推荐】所有的局部变量使用基本数据类型。 说明:POJO类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何NPE问题,或者入库检查,都由使用者来保证。 正例:数据库的查询结果可能是null,因为自动拆箱,用基本数据类型接收有NPE风险。

10.【强制】定义DO/DTO/VO等POJO类时,不要设定任何属性默认值。

11.【强制】POJO类必须写toString方法。使用IDE中的工具:source> generate toString时,如果继承了另一个POJO类,注意在前面加一下super.toString。

说明:在方法执行抛出异常时,可以直接调用POJO的toString()方法打印其属性值,便于排查问题。

(五)时间日期

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

说明:日期格式化时,yyyy表示当天所在的年,而大写的YYYY代表是week in which year(JDK7之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的YYYY就是下一年。 

正例:表示日期和时间的格式如下所示: new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")

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

说明日期格式中的这两对字母表意如下: 

        1) 表示月份是大写的M; 

        2) 表示分钟则是小写的m; 

        3) 24小时制的是大写的H; 

        4) 12小时制的则是小写的h。

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

说明:第1个不记录时间,getHours()抛出异常;第2个不记录日期,getYear()抛出异常;第3个在构造方法super((time/1000)*1000),在Timestamp 属性fastTime和nanos分别存储秒和纳秒信息。 

反例:某个案例的数据库表id字段定义类型bigint unsigned,实际类对象属性为Integer,随着id越来越大,超过Integer的表示范围而溢出成为负数。

(六)集合处理

15.【强制】关于hashCode和equals的处理,遵循如下规则:

        1) 只要覆写equals,就必须覆写hashCode。 

        2) 因为Set存储的是不重复的对象,依据hashCode和equals进行判断,所以Set存储的对象必须覆写这两种方法。 

        3) 如果自定义对象作为Map的键,那么必须覆写hashCode和equals。 

        说明:String因为覆写了hashCode和equals方法,所以可以愉快地将String对象作为key来使用。

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

说明:在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", 8.3));
pairArrayList.add(new Pair<>("version2", null));
Map<String, Double> map = pairArrayList.stream().collect(
// 抛出NullPointerException异常
Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));

正例:表示日期和时间的格式如下所示: new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")

17.【强制】ArrayList的subList结果不可强转成ArrayList,否则会抛出 ClassCastException异常:java.util.RandomAccessSubList cannot be cast to java.util.ArrayList。

说明:subList()返回的是ArrayList的内部类SubList,并不是 ArrayList本身,而是ArrayList 的一个视图,对于SubList的所有操作最终会反映到原列表上。

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

反例:直接使用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隐患。

19.【强制】使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UnsupportedOperationException异常。

说明:asList的返回对象是一个Arrays内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组。

String[] str = new String[] { "chen", "yang", "hao" }; 

List list = Arrays.asList(str);

第一种情况:list.add("yangguanbao"); 运行时异常。

第二种情况:str[0] = "change"; 也会随之修改,反之亦然。

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

正例

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);
    }
}

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

https://cdn.xwder.com/image/blog/xwder/1-20201228140653602.png

反例:由于HashMap的干扰,很多人认为ConcurrentHashMap是可以置入null值,而事实上,存储null值时会抛出NPE异常。

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

(七)并发处理

23.【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全。

说明:资源驱动类、工具类、单例工厂类都需要注意。

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

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

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

说明:Executors返回的线程池对象的弊端如下:

        1) FixedThreadPool和SingleThreadPool: 

        允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。 

        2) CachedThreadPool: 

        允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

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

正例注意线程安全,使用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。


@TODO 此模块需要继续完善

 

(八)控制语句

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

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

说明:asList的返回对象是一个Arrays内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组。

(九)注释规约

(十)前后端规约

28.【强制】前后端数据列表相关的接口返回,如果为空,则返回空数组[]或空集合{}。

正例:此条约定有利于数据层面上的协作更加高效,减少前端很多琐碎的null判断。

29.【强制】服务端发生错误时,返回给前端的响应信息必须包含HTTP状态码,errorCode、errorMessage、用户提示信息四个部分。

30.【强制】在前后端交互的JSON格式数据中,所有的key必须为小写字母开始的lowerCamelCase风格,符合英文表达习惯,且表意完整。

31.【强制】服务器内部重定向必须使用forward;外部重定向地址必须使用URL统一代理模块生成,否则会因线上采用HTTPS协议而导致浏览器提示“不安全”,并且还会带来URL维护不一致的问题。

(十一)其他

32.【强制】避免用Apache Beanutils进行属性的copy。

说明:Apache BeanUtils性能较差,可以使用其他方案比如Spring BeanUtils, Cglib BeanCopier,注意均是浅拷贝。

33.【强制】注意 Math.random() 这个方法返回是double类型,注意取值的范围 0≤x<1(能够取到零值,注意除零异常),如果想获取整数类型的随机数,不要将x放大10的若干倍然后取整,直接使用Random对象的nextInt或者nextLong方法。