人的出身没法改变,长相没法改变。但是一个人的代码,就是你的第二张脸。
代码符合规范,并且长期以往坚持的人,一定不会差。
想让别人信任你,想写出更好的代码,想变得优秀,先从眼下的代码规范开始做起吧。
常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
正例:MAX_STOCK_COUNT / CACHE_EXPIRED_TIME
反例:MAX_COUNT / EXPIRED_TIME
POJO 类中的任何布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误。
说明:在本文 MySQL 规约中的建表约定第一条,表达是与否的变量采用 is_xxx 的命名方式,所以,需要 在设置从 is_xxx 到 xxx 的映射关系。
反例:定义为基本数据类型 Boolean isDeleted 的属性,它的方法也是 isDeleted(),框架在反向解析的时 候,“误以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。
if/for/while/switch/do 等保留字与括号之间都必须加空格。
注释的双斜线与注释内容之间有且仅有一个空格。
正例: // 这是示例注释,请注意在双斜线之后有一个空格
单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则:
1) 第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进,参考示例。
2) 运算符与下文一起换行。
3) 方法调用的点符号与下文一起换行。
4) 方法调用中的多个参数需要换行时,在逗号后进行。
5) 在括号前不要换行,见反例。
正例: StringBuilder sb = new StringBuilder(); // 超过 120 个字符的情况下,换行缩进 4 个空格,并且方法前的点号一起换行 sb.append(“yang”).append(“hao”)… .append(“chen”)… .append(“chen”)… .append(“chen”);
反例: StringBuilder sb = new StringBuilder(); // 超过 120 个字符的情况下,不要在括号前换行 sb.append(“you”).append(“are”)…append (“lucky”); Java 编码规范 - 9 - // 参数很多的方法调用可能超过 120 个字符,逗号后才是换行处 method(args1, args2, args3, … , argsX);
避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。
所有的覆写方法,必须加@Override 注解。
相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用 Object。
说明:可变参数必须放置在参数列表的最后。(建议开发者尽量不用可变参数编程)
正例:public List listUsers(String type, Long… ids) {…}
Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。
正例:“test”.equals(object);
反例:object.equals(“test”);
所有整型包装类对象之间值的比较,全部使用 equals 方法比较
@Test
public void test01() {
Integer i = 129;
Integer j = 129;
System.out.println(i == j);
// 结果为false
System.out.println(i.equals(j));
// 结果为true
}
说明:对于 Integer var = ? 在-128 至 127 之间的赋值,Integer 对象是在 IntegerCache.cache 产生, 会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断
浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断。
说明:浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。二进制无法精确表示大部分的十进制小数。
反例:
@Test
public void test02() {
float a = 1.0F - 0.9F;
float b = 0.9F - 0.8F;
System.out.println(a == b);
Float x = Float.valueOf(a);
Float y = Float.valueOf(b);
System.out.println(x.equals(y));
}
正例: 使用 BigDecimal 来定义值,再进行浮点数的运算操作。
@Test
public void test03() {
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);
System.out.println(x.compareTo(y));
}
BigDecimal 的等值比较应使用 compareTo()方法,而不是 equals()方法。
说明:equals()方法会比较值和精度(1.0 与 1.00 返回结果为 false),而 compareTo()则会忽略精度
@Test
public void test04() {
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
System.out.println(a.compareTo(b));
// 0 代表相等
System.out.println(a.equals(b));
// false
}
禁止使用构造方法 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);
定义数据对象 DO 类时,属性类型要与数据库字段类型相匹配。
正例:数据库字段的 bigint 必须与类属性的 Long 类型相对应。 反例:某个案例的数据库表 id 字段定义类型bigint unsigned,实际类对象属性为 Integer,随着 id 越来 越大,超过 Integer 的表示范围而溢出成为负数。关于基本数据类型与包装数据类型的使用标准如下:
1) 所有的 POJO 类属性必须使用包装数据类型。
2) RPC 方法的返回值和参数必须使用包装数据类型。
3) 推荐所有的局部变量使用基本数据类型。
说明:POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证。
正例:数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。
反例:某业务的交易报表上显示成交总额涨跌情况,即正负 x%,x 为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线-。所以包装数据类型 的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。
定义 DO/DTO/VO 等 POJO 类时,不要设定任何属性默认值。
反例:POJO 类的 createTime 默认值为 new Date(),但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。
序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。 说明:注意 serialVersionUID 不一致会抛出序列化运行时异常。
构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中
POJO 类必须写 toString 方法。(@Data注解已重写)使用 IDE 中的工具:source> generate toString 时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString。 说明:在方法执行抛出异常时,可以直接调用 POJO 的 toString()方法打印其属性值,便于排查问题。
禁止在 POJO 类中,同时存在对应属性 xxx 的 isXxx()和 getXxx()方法。 说明:框架在调用属性 xxx 的提取方法时,并不能确定哪个方法一定是被优先调用到的。
不允许在程序任何地方中使用:
1)java.sql.Date
2)java.sql.Time
3)java.sql.Timestamp
说明:第 1 个不记录时间,getHours()抛出异常;第 2 个不记录日期,getYear()抛出异常;第 3 个在构造 方法 super((time/1000)*1000),在 Timestamp 属性 fastTime 和 nanos 分别存储秒和纳秒信息。
反例: java.util.Date.after(Date)进行时间比较时,当入参是 java.sql.Timestamp 时,会触发 JDK BUG(JDK9 已修复),可能导致比较时的意外结果。
关于 hashCode 和 equals 的处理,遵循如下规则:
1) 只要覆写 equals,就必须覆写 hashCode。
2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写这两种方法。
3) 如果自定义对象作为 Map 的键,那么必须覆写hashCode 和 equals。
说明:String 因为覆写了 hashCode 和 equals 方法,所以可以愉快地将 String 对象作为 key 来使
补充:如果不重写equals,那么equals对于基本类型,比较的是值,而对于包装类,比较的是地址,和==一样。
判断所有集合内部的元素是否为空,使用 isEmpty()方法,而不是 size()==0 的方式。
说明:在某些集合中,前者的时间复杂度为 O(1),而且可读性更好。
正例:
Map map = new HashMap<>(16);
if(map.isEmpty())
{
System.out.println(“no element in this map.”);
}
Collections 类返回的对象,如:emptyList()/singletonList()等都是 immutable list, 不可对其进行添加或者删除元素的操作。
反例:如果查询无结果,返回 Collections.emptyList()空集合对象,调用方一旦进行了添加元素的操作,就会触发 UnsupportedOperationException 异常
使用工具类 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”; 也会随之修改,反之亦然。
@Test
public void test05(){
List<Integer> integers = Arrays.asList(1, 2, 3);
integers.add(4);
// 异常:java.lang.UnsupportedOperationException
}
泛型通配符来接收返回的数据,此写法的泛型集合不能使用 add 方法, 而不能使用 get 方法,两者在接口调用赋值的场景中容易出错。
说明:扩展说一下 PECS(Producer Extends Consumer Super)原则:
第一、频繁往外读取内容的,适合用 。
第二、经常往里插入的,适合用
不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
正例:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("1".equals(item)) {
iterator.remove();
}
}
for (String s :list){
System.out.println(s);
}
反例:
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用, 如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。 尽量在代理中使用 try-finally 块进行回收。
正例:
public void test09() {
ThreadLocal objectThreadLocal = new ThreadLocal();
objectThreadLocal.set(1);
try {
// 业务逻辑
} finally {
objectThreadLocal.remove();
}
}
在使用阻塞等待获取锁的方式中,必须在 try 代码块之外,并且在加锁方法与 try 代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在 finally 中无法解锁。
说明一:如果在 lock 方法与 try 代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功 获取锁。
说明二:如果 lock 方法在try 代码块之内,可能由于其它方法抛出异常,导致在 finally 代码块中,unlock 对未加锁的对象解锁,它会调用 AQS 的tryRelease 方法(取决于具体实现类),抛出 IllegalMonitorStateException 异常。
说明三:在 Lock 对象的 lock 方法实现中可能抛出 unchecked 异常,产生的后果与说明二相同。
正例:
@Test
public void test10() {
Lock lock = new ReentrantLock();
// 此处是任意锁
lock.lock();
try {
// doSomething();
// doOthers();
} finally {
lock.unlock();
}
}
反例:
@Test
public void test10() {
Lock lock = new ReentrantLock();
// 此处是任意锁
try {
// 如果此处抛出异常,则直接执行 finally 代码块
// doSomething();
// 无论加锁是否成功,finally 代码块都会执行
lock.lock();
// doOthers();
} finally {
lock.unlock();
}
}
捕获异常后要么记录日志并处理,要么重新抛出,禁止捕获异常后不做任何的记录和处理。
流量入口层禁止对外抛异常,必须捕获并处理所有异常,然后封装成该层的响应结果返回。
异常返回要区分需要用户感知和不需要用户感知的情况(前者给用户返回明确异常描述,后者给用户返回系统异常)。
类、类属性、类方法的注释必须使用 Javadoc 规范,使用/** */格式,不得使用 // xxx 方式。
说明:在 IDE 编辑窗口中,Javadoc 方式会提示相关注释,生成 Javadoc 可以正确输出相应注释;在 IDE 中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。
所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。
方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使 用/* */注释,注意与代码对齐。
所有的枚举类型字段必须要有注释,说明每个数据项的用途。
发送MQ消息需手动设置超时时间,弱依赖场景必须手动catch异常,防止因MQ服务端问题发送消息超时阻塞代码流程
消费MQ消息,数据校验不通过等异常情况,打印错误日志然后返回成功,千万不要抛异常,会导致不停重试不停失败
一个应用只连一个redis集群
对redis能弱依赖则弱依赖,手动catch异常,避免阻塞业务流程
尽可能的设置key的过期时间(条件允许可以打散过期时间,防止集中过期),不过期的数据重点关注idletime
表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint (1 表示是,0 表示否)。 说明:任何字段如果为非负数,必须是 unsigned。
注意:POJO 类中的任何布尔类型的变量,都不要加 is 前缀,所以,需要在设置从 is_xxx 到 Xxx 的映射关系。数据库表示是与否的值,使用 tinyint 类型,坚持 is_xxx 的命名方式是为了明确其取值含义与取值范围。
正例:表达逻辑删除的字段名is_deleted,1 表示删除,0 表示未删
表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只 出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
说明:MySQL 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写。因此,数据库名、表名、字 段名,都不允许出现任何大写字母,避免节外生枝。
正例:cargo_admin,rdc_config,level3_name
反例:CargoAdmin,rdcConfig,level_3_name
主键索引名为 pk_ 字段名;唯一索引名为 uk_ 字段名;普通索引名则为 idx_ 字段名。
说明:pk_ 即 primary key;uk_ 即 unique key;idx_ 即 index 的简称
varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引效率
说明:其中 id 必为主键,类型为bigint unsigned、单表时自增、步长为 1。create_time, update_time 的类型均为 datetime 类型,前者现在时表示主动式创建,后者过去分词表示被动式更新。
不要使用 count(列名)或 count(常量)来替代 count(* ),count(* )是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。
说明:count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行
SQL默认读从库,少量不能接受主从延迟的业务场景单独提供主库查询方法
数据订正(特别是删除或修改记录操作)时,要先 select,避免出现误删除,确认无误才能执行更新语句。
POJO 类的布尔属性不能加 is,而数据库字段必须加 is_,要求在 resultMap 中进行字段与属性之间的映射。
更新数据表记录时,必须同时更新记录对应的 update_time 字段值为当前时间。