数据库
【强制】表达是与否概念的字段,必须使用is_xxx的方式命名,数据类型是unsigned tinyint(1表示是,0表示否),此规则同样适用于odps建表。
说明:任何字段如果为非负数,必须是unsigned。
注意:POJO类中的任何布尔类型的变量,都不要加is前缀,所以,需要在
正例:表达逻辑删除的字段名is_deleted,1表示删除,0表示未删除。
【强制】表名不使用复数名词。
说明:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于DO类名也是单数形式,符合表达习惯。
【推荐】字段允许适当冗余,以提高性能,但是必须考虑数据同步的情况。冗余字段应遵循:
1)不是频繁修改的字段。
2)不是唯一索引的字段。
3)不是varchar超长字段,更不能是text的字段。
正例:各业务线经常冗余存储商品名称,避免查询时需要调用IC服务获取。
【推荐】单表行数超过500万行或者单表容量超过2GB,才推荐进行分库分表。
说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。
反例:某业务三年总数据量才2万行,却分成1024张表,问:你为什么这么设计?答:分1024张表,不是标配吗?
【强制】varchar是可变长字符串,不预先分配存储空间,长度不要超过5000,如果存储长度大于此值,定义字段类型为TEXT,独立出来一张表,用主键来对应,避免影响其它字段索引效率。
【参考】合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。
正例:无符号值可以避免误存负数,且扩大了表示范围:
对象 | 年龄区间 | 类型 | 字节 | 表示范围 |
---|---|---|---|---|
人 | 150岁之内 | tinyint unsigned | 1 | 无符号值:0到255 |
龟 | 数百岁 | smallint unsigned | 2 | 无符号值:0到65535 |
恐龙化石 | 约数千万年 | int unsigned | 4 | 无符号值:0到约43亿 |
太阳 | 约50亿年 | bigint unsigned | 8 | 无符号值:0到约10的19次方 |
【强制】业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。
说明:不要以为唯一索引影响了insert速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验和控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。
【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
说明:索引文件具有B-Tree的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
【强制】在varchar字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为20的索引,区分度会高达90%以上,可以使用count(distinct left(列名, 索引长度))/count(*)的区分度来确定。
【强制】小数类型为decimal,禁止使用float和double。
说明:在存储的时候,float 和 double 都存在精度损失的问题,很可能在比较值的时候,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储。
【强制】主键索引应以pk_开头,唯一索引要以uk_开头,普通索引要以idx_开头。
说明:pk_即primary key;uk_ 即unique key;idx_即index的简称
【推荐】避免where 条件语句中的字段使用计算函数。
说明:OB优化器表达式规范化做得不是特别完善,有些场景无法走索引,where id*2>4 这样会使该字段上的索引失效。
【推荐】SQL性能优化的目标:至少要达到 range 级别,要求是ref级别,如果可以是const最好。
说明:
1)const 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。
2)ref 指的是使用普通的索引。(normal index)
3)range 对索引进行范围检索。
反例:explain表的结果,type=index,索引物理文件全扫描,速度非常慢,这个index级别比较range还低,与全表扫描是小巫见大巫
【强制】不要使用count(列名)或count(常量)来替代count(),count()就是SQL92定义的标准统计行数的语法,跟数据库无关,跟NULL和非NULL无关。
说明:count(*)会统计值为NULL的行,而count(列名)不会统计此列为NULL值的行。
【强制】count(distinct col) 计算该列除NULL之外的不重复数量。注意 count(distinct col1, col2) 如果其中一列全为NULL,那么即使另一列有不同的值,也返回为0
【强制】当某一列的值全是NULL时,count(col)的返回结果为0,但sum(col)的返回结果为NULL,因此使用sum()时需注意NPE问题。
正例:可以使用如下方式来避免sum的NPE问题:SELECT IFNULL(SUM(column), 0) FROM table;
【强制】使用ISNULL()来判断是否为NULL值。
说明: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)执行效率更快一些。
【强制】在代码中写分页查询逻辑时,若count为0应直接返回,避免执行后面的分页语句。
【强制】IDB数据订正(特别是删除或修改记录操作)时,要先select,避免出现误删除,确认无误才能提交执行。
【推荐】SQL语句中表的别名前加as,并且以t1、t2、t3、...的顺序依次命名。
说明:1)别名可以是表的简称,或者依据表在SQL语句中出现的顺序,以t1、t2、t3的方式命名。2)别名前加as使别名更容易识别。
正例:select t1.name from table_first as t1, table_second as t2 where t1.id=t2.id;
【推荐】in操作能避免则避免,若实在避免不了,需要仔细评估in后边的集合元素数量,控制在1000个之内
【参考】因需要,所有的字符存储与表示,均采用utf8字符集,那么字符计数方法注意:
说明:
SELECT LENGTH("阿里巴巴"); 返回为12
SELECT CHARACTER_LENGTH("阿里巴巴"); 返回为4
如果需要存储表情,那么选择utf8mb4来进行存储,注意它与utf8编码的区别。
【推荐】利用延迟关联或者子查询优化超多分页场景。
说明:MySQL并不是跳过offset行,而是取offset+N行,然后返回放弃前offset行,返回N行,那当offset特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行SQL改写。
正例:先快速定位需要获取的id段,然后再关联:
SELECT t1.* FROM 表1 as t1, (select id from 表1 where 条件 LIMIT 100000,20 ) as t2 where t1.id=t2.id
反例:“服务市场”某交易分页超过1000页,用户点击最后一页时,数据库基本处于半瘫痪状态。
【推荐】有时间精度要求的业务,可以使用datetime(6); 对精度没要求的,设置为datetime即可。
说明:约定采用datetime(6),精确到微秒
【推荐】为了避免plan cache命中率下降或者plan cache被污染,对于 c1 in (v1, v2, ...) 或者insert into t1 values(v1, v2,...) 的SQL语句,业务方需尽量明确in数量。
说明:对于c1 in (v1,v2,…)或者insert into t1 values(v1,v2,…)这种参数个数不确定的SQL语句,在plan cache中需要为不同参数数量的SQL语句产生不同的缓存项,浪费内存且容易污染plan cache。
plan cache该功能对执行计划进行缓存,当相同的SQL语句多次执行时,可跳过优化阶段,直接进入执行阶段,从而提高SQL语句的执行速度。
【强制】任何操作,都是先保存数据库成功后,再进行缓存的新增、更新、清除操作。
【强制】不允许直接拿HashMap与HashTable作为查询结果集的输出。
反例:某同学为避免写一个xxx,直接使用HashTable来接收数据库返回结果,结果出现日常是把bigint转成Long值,而线上由于数据库版本不一样,解析成BigInteger,导致线上问题。
【推荐】不要写一个大而全的数据更新接口,传入为POJO类,不管是不是自己的目标更新字段,都进行update table set c1=value1,c2=value2,c3=value3; 这是不对的。执行SQL时,不要更新无改动的字段,一是易出错;二是效率低;三是binlog增加存储。
【参考】
中间件
【强制】 [MsgBroker] 消息投递不保证不重复,所以消息接收端需要控制幂等。
【强制】 [SOFA RPC] 单元化架构机房里的系统,进行SOFA RPC调用时,调用方必须要配置VIP;非单元化架构机房里的系统,则一定不能配置VIP。
说明:单元化架构,跨机房调用,强依赖VIP配置。故必须配置VIP。而非单元化架构,因为大部分运维层面并不提供VIP域名,或者没有ANTVIP部署,则不能配置。如果配置了,启动期可能会调用失败。已发生过多次因为VIP配置不当,引起的线上问题。
VIP配置方式拿SOFA标签举例:
安全
【强制】用户敏感数据禁止直接展示,必须按蚂蚁敏感信息展示规范标准对展示数据脱敏。
说明:比如支付宝中查看个人手机号码会显示成:158****9119,隐藏中间4位
【强制】日志信息存储、打印要对敏感数据脱敏。
说明:比如日志不能打印用户名、密码以及信用卡有效期等敏感数据,日志存储时间的设置等
【强制】page size过大导致内存溢出
单测
【强制】单元测试必须使用assert来验证。
【强制】单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下。
说明:源码编译时会跳过此目录,而单元测试框架默认是扫描此目录。
工具类
【推荐】工具类二方库已经提供的,不要在本应用中编程实现。
json操作: fastjson
md5操作:commons-codec
工具集合:Guava包
数组操作:ArrayUtils(org.apache.commons.lang3.ArrayUtils)
集合操作:CollectionUtils(org.apache.commons.collections4.CollectionUtils)
除上面以外还有NumberUtils、DateFormatUtils、DateUtils等优先使用org.apache.commons.lang3这个包下的,不要使用org.apache.commons.lang包下面的。原因是commons.lang这个包是从JDK1.2开始支持的所以很多1.5/1.6的特性是不支持的,例如:泛型。
MAVEN
【推荐】所有pom文件中的依赖声明放在
1.子模块自己的坐标定义只需要写artifactID即可,groupID和versiosn统一继承根pom的定义,有子模块的项目必须拥有一个根pom,所有子模块必须继承根pom 根pom用于定义全局设置和dependencyManagement
1.1.0
junit
junit
4.8.2
test
2.子模块之间的depedency申明,不要使用具体的版本号定义,而使用
com.alibaba.china.shared
bingshare.dal.quotation
${project.version}
3.引用其他二方库的系列子模块,统一在根pom里定义版本号
com.alibaba.china.shared
pivot.common
${pivot.version}
4.引用三方包,在根pom的dependencyManagement里定义版本号和其他一些配置,如scope,不需要每个子模块单独定义三方包的版本号和配置,只需要定义groupID和artifactID即可
junit
junit
服务器
【推荐】给JVM环境参数设置-XX:+HeapDumpOnOutOfMemoryError参数,让JVM碰到OOM场景时输出dump信息。
说明:OOM的发生是有概率的,甚至相隔数月才出现一例,出错时的堆内信息对解决问题非常有帮助
【推荐】在线上生产环境,JVM的Xms和Xmx设置一样大小的内存容量,避免在GC后调整堆大小带来的压力。
编码
【强制】枚举中不能定义NAME的枚举值或name域变量。
说明:1. enum自带name()方法来获取枚举值的字符串值;2. NAME的枚举值获取是name()方法返回的是大写的NAME;3. 如果getName()方法来获取name域变量,容易与name()方法产生混淆。
反例:伪代码如下,关注两个命名点即可:
public enum Person {
// 枚举值不允许定义为NAME
NAME, SEX, AGE;
// 域变量不允许定义为name
private String name;
}
【推荐】如果模块、接口、类、方法使用了设计模式,在命名时体现出具体模式。
说明:将设计模式体现在名字中,有利于阅读者快速理解架构设计思想。
正例:public class OrderFactory;
public class LoginProxy;
public class ResourceObserver;
推荐】接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的javadoc注释。尽量不要在接口里定义变量,如果一定要定义变量,确定与接口方法相关,并且是整个应用的基础常量。
正例:接口方法签名:void commit();
接口基础常量表示:String COMPANY = "alibaba";
反例:接口方法定义:public abstract void commit();
说明:JDK8中接口允许有默认实现,那么这个default方法,是对所有实现类都有价值的默认实现。
【参考】各层命名规约:
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。
【强制】if/for/while/switch/do等保留字与左右括号之间都必须加空格。
【强制】任何二目、三目运算符的左右两边都必须加一个空格。
说明:包括赋值运算符=、逻辑运算符&&、加减乘除符号等。
【强制】采用4个空格缩进,禁止使用tab字符。
说明:如果使用tab缩进,必须设置1个tab为4个空格。IDEA设置tab为4个空格时,请勿勾选:Use tab character;而在eclipse中,必须勾选insert spaces for tabs。
【强制】注释的双斜线与注释内容之间有且仅有一个空格。
正例:
// 这是示例注释,请注意在双斜线之后有一个空格
String commentString = new String();
【强制】单行字符数不超过120个,超出则需要换行,换行时遵循如下原则:
1) 第二行相对第一行缩进4个空格,从第三行开始,不再继续缩进,参考示例。
2) 运算符与下文一起换行。
3) 方法调用的点符号与下文一起换行。
4) 方法调用中的多个参数需要换行时,在逗号后进行。
5) 在括号前不要换行,见反例。
正例:
StringBuilder sb = new StringBuilder();
// 超过120个字符的情况下,换行缩进4个空格,并且方法前的点号一起换行
sb.append("zi").append("xin")...
.append("huang")...
.append("huang")...
.append("huang");
反例:
StringBuilder sb = new StringBuilder();
// 超过120个字符的情况下,不要在括号前换行
sb.append("zi").append("xin")...append
("huang");
// 参数很多的方法调用可能超过120个字符,逗号后才是换行处
invoke(args1, args2, args3, ...
, argsX);
【强制】方法参数在定义和传入时,多个参数逗号后边必须加空格。
正例:下例中实参的args1,后边必须要有一个空格。
method(args1, args2, args3);
【强制】BigDecimal的等值比较应使用compareTo()方法,而不是equals()方法。
说明:equals()方法会比较值和精度,而compareTo()则会忽略精度。
BigDecimal a = new BigDecimal("1");
BigDecimal b = new BigDecimal("1.000");
if (a.equals(b)) {
System.out.println("true");
} else {
// 比较精度时就返回为false
System.out.println("false");
}
if (a.compareTo(b) == 0) {
// 忽略精度scale,输出为true
System.out.println("true");
} else {
System.out.println("false");
}
【强制】禁止使用构造方法BigDecimal(double)的方式把double值转化为BigDecimal对象。
说明:BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。如:BigDecimal g = new BigDecimal(0.1f); 实际的存储值为:0.100000001490116119384765625
正例:优先推荐入参为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的表示范围而溢出成为负数,此时数据库id不支持存入负数抛出异常产生P2故障。
【强制】定义DO/DTO/VO等POJO类时,不要设定任何属性默认值。
反例:某业务的DO的gmtCreate默认值为new Date();但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。
【强制】构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在init方法中。
强制】对于需要使用超大整数的场景,服务端一律使用String字符串类型返回,禁止使用Long类型。
说明:Java服务端如果直接返回Long整型数据给前端,JS会自动转换为Number类型(注:此类型为双精度浮点数,表示原理与取值范围等同于Java中的Double)。Long类型能表示的最大值是2的63次方-1,在取值范围之内,超过2的53次方 (9007199254740992)的数值转化为JS的Number时,有些数值会有精度损失。扩展说明,在Long取值范围内,任何2的指数次整数都是绝对不会存在精度损失的,所以说精度损失是一个概率问题。若浮点数尾数位与指数位空间不限,则可以精确表示任何整数,但很不幸,双精度浮点数的尾数位只有52位。
反例:通常在订单号或交易号大于等于16位,大概率会出现前后端单据不一致的情况,比如,"orderId": 362909601374617692,前端拿到的值却是: 362909601374617660
【推荐】使用索引访问用String的split方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛IndexOutOfBoundsException的风险。
说明:
String str = "a,b,c,,";
String[] ary = str.split(",");
// 预期大于3,结果是3
System.out.println(ary.length);
【强制】日期格式化时,传入pattern中表示年份统一使用小写的y。
说明:日期格式化时,yyyy表示当天所在的年,而大写的YYYY代表是week in which year(JDK7之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的YYYY就是下一年。
正例:表示日期和时间的格式如下所示:
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
某业务因使用YYYY/MM/dd进行日期格式化,2017/12/31执行结果为2018/12/31,导致P4P广告在2017年12月31日当日实时消耗跌0,造成P2故障
【强制】在日期格式中分清楚大写的M和小写的m,大写的H和小写的h分别指代的意义。
说明:日期格式中的这两对字母表意如下:
表示月份是大写的M;
表示分钟则是小写的m;
24小时制的是大写的H;
12小时制的则是小写的h。
【强制】获取当前毫秒数:System.currentTimeMillis(); 而不是new Date().getTime();
说明:如果想获取更加精确的纳秒级时间值,使用System.nanoTime的方式。在JDK8中,针对统计时间等场景,推荐使用Instant类。
【强制】不允许在程序任何地方中使用: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。
说明:参考hashCode 与 equals的辩证关系。String
正因为覆写了hashCode和equals方法,所以我们可以非常愉快地使用String对象作为key来使用。
【强制】使用Map的方法keySet()/values()/entrySet()返回集合对象时,不可以对其进行添加元素操作,否则会抛出UnsupportedOperationException异常(不支持操作异常)。
【强制】在使用java.util.stream.Collectors类的toMap()方法转为Map集合时,一定要使用含有参数类型为BinaryOperator,参数名为mergeFunction的方法,否则当出现相同key值时会抛出IllegalStateException异常。
说明:参数mergeFunction的作用是当出现key重复时,自定义对value的处理策略。
正例:
List> 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 map = pairArrayList.stream().collect(
// 生成的map集合中只有一个键值对:{version=13.14}
Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));
【强制】在使用java.util.stream.Collectors类的toMap()方法转为Map集合时,一定要注意当value为null时会抛NPE异常。
说明:在java.util.HashMap的merge方法里会进行如下的判断:
if (value == null || remappingFunction == null)
throw new NullPointerException();
反例:
List> pairArrayList = new ArrayList<>(2);
pairArrayList.add(new Pair<>("version1", 4.22));
pairArrayList.add(new Pair<>("version2", null));
Map map = pairArrayList.stream().collect(
// 抛出NullPointerException异常
Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2))
【强制】判断所有集合内部的元素是否为空,使用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异常。
【强制】ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException异常:java.util.RandomAccessSubList cannot be cast to java.util.ArrayList;
说明: subList()返回的是ArrayList的内部类SubList,并不是 ArrayList本身,而是ArrayList 的一个视图,对于SubList的所有操作最终会反映到原列表上。
【强制】在subList场景中,高度注意对父集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生ConcurrentModificationException 异常。
说明: 抽查表明,九成的开发同学对此知识点都有错误的认知。
强制】使用集合转数组的方法,必须使用集合的toArray(T[] array),传入的是类型完全一致、长度为0的空数组。
反例:直接使用toArray无参方法存在问题,此方法返回值只能是Object[]类,若强转其它类型数组将出现ClassCastException错误。
正例:
List list = new ArrayList<>(2);
list.add("guan");
list.add("bao");
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隐患。
【强制】在使用Collection接口任何实现类的addAll()方法时,都要对输入的集合参数进行NPE判断。
说明:在ArrayList#addAll方法的第一行代码即Object[] a = c.toArray(); 其中c为输入集合参数,如果为null,则直接抛出异常。
【强制】使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UnsupportedOperationException异常。
说明:asList的返回对象是一个Arrays内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组。
String[] str = new String[] { "a", "b" };
List list = Arrays.asList(str);
第一种情况:list.add("c"); 运行时异常。
第二种情况:str[0]= "changed"; 那么list.get(0)也会随之修改,反之亦然。
【强制】泛型通配符 extends T>来接收返回的数据,此写法的泛型集合不能使用add方法,而 super T>不能使用get方法,两者在接口调用赋值的场景中易出错。
说明:扩展说一下PECS (Producer Extends Consumer Super)原则:第一、频繁往外读取内容的,适合用 extends T>。第二、经常往里插入的,适合用 super T>。
【强制】不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator迭代器对象加锁。
反例:
List list = new ArrayList<>();
list.add("targetItem");
list.add("other");
for (String item : list) {
if ("targetItem".equals(item)) {
list.remove(item);
}
}
正例:
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (删除元素的条件) {
iterator.remove();
}
}
【强制】在JDK7版本以上,Comparator要满足如下三个条件,不然Arrays.sort,Collections.sort会抛IllegalArgumentException异常。
说明:
1) x,y的比较结果和y,x的比较结果相反。
2) x>y,y>z,则x>z。
3) x=y,则x,z比较结果和y,z比较结果相同。
反例:下例中没有处理相等的情况,交换两个对象判断结果并不互反,不符合第一个条件,在实际使用中可能会出现异常。
new Comparator() {
@Override
public int compare(Student o1, Student o2) {
return o1.getId() > o2.getId() ? 1 : -1;
}
}
【推荐】泛型集合使用时,在JDK7版本及以上,使用 diamond 语法或全省略。
说明:菱形泛型,即 diamond,直接使用<>来指代前边已经指定的类型。
正例:
// <> diamond 方式
HashMap userCache = new HashMap<>(16);
// 全省略方式
ArrayList users = new ArrayList(10);
【推荐】集合初始化时,指定集合初始值大小。
说明:HashMap使用如下构造方法进行初始化,如果暂时无法确定集合大小,那么指定默认值(16)即可
反例:HashMap需要放置1024个元素,由于没有设置容量初始大小,随着元素增加而被迫不断扩容,resize()方法总共会调用8次,反复重建哈希表和数据迁移。当放置的集合元素个数达千万级时会影响程序性能。
推荐】使用entrySet遍历Map类集合KV,而不是keySet方式进行遍历。如果只要key不要value那就不用换
说明:keySet其实是遍历了2次,一次是转为Iterator对象,另一次是从hashMap中取出key所对应的value。而entrySet只是遍历了一次就把key和value都放到了entry中,效率更高。如果是JDK8,使用Map.forEach方法。
正例:values()返回的是V值集合,是一个List集合对象;keySet()返回的是K值集合,是一个Set集合对象;entrySet()返回的是K-V值组合集合。
【参考】利用Set元素唯一的特性,可以快速对另一个集合进行去重操作,避免使用List的contains()进行遍历去重或者判断包含操作。
【参考】如果是JDK8的应用,可以使用Instant代替Date,LocalDateTime代替Calendar,DateTimeFormatter代替SimpleDateFormat 但是数据库存Instant还是有待验证坑
【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。
说明:线程一需要对表A、B、C依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是A、B、C,否则可能出现死锁。
【强制】在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。
说明一:如果在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();
}
【强制】在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。
说明:Lock对象的unlock方法在执行时,它会调用AQS的tryRelease方法(取决于具体实现类),如果当前线程不持有锁,则抛出IllegalMonitorStateException异常。
正例:
Lock lock = new XxxLock();
// ...
boolean isLocked = lock.tryLock();
if (isLocked) {
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}
}
【强制】并发修改同一记录时,避免更新丢失,要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用version作为更新依据。
说明:如果每次访问冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3次。
正例:集团很多业务使用TairManager方法:incr(namespace, lockKey, 1, 0, expireTime); 判断返回步长是否为
【推荐】避免Random实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一seed 导致的性能下降。
说明:Random实例包括java.util.Random 的实例或者 Math.random()的方式。
正例:在JDK7版本及以上,可以直接使用API ThreadLocalRandom;而在JDK7前,需要编码保证每个线程持有一个单独的Random实例。
强制】当switch括号内的变量类型为String并且此变量为外部参数时,必须先进行null判断。
反例:如下的代码输出是什么?
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");
}
}
}
空指针异常
【强制】在if/else/for/while/do语句中必须使用大括号。即使只有一行代码,禁止不采用大括号的编码方式:
if (condition) statements;
【强制】三目运算符condition? 表达式1 : 表达式2中,高度注意表达式1和2在涉及算术计算或数据类型转换时,可能抛出因自动拆箱导致的NPE异常。
说明:以下两种场景会触发类型对齐的拆箱操作:
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);
报空指针
【强制】在高并发场景中,避免使用“等于”判断作为中断或退出的条件。
说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替。
反例:某营销活动发奖,判断剩余奖品数量等于0时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数,活动无法终止,产生资损。
超过3层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现,其中卫语句示例如下:
【推荐】避免采用取反逻辑运算符。
说明:取反逻辑不利于快速理解,并且取反逻辑写法一般都存在对应的正向逻辑写法。
【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。
说明:不要在方法体内定义:Pattern pattern = Pattern.compile(规则);
private static final check =
"^[A-Za-z0-9_]+([-+.][A-Za-z0-9_]+)*@[A-Za-z0-9_]+([-.][A-Za-z0-9_]+)*.[A-Za-z0-9_]+([-.][A-Za-z0-9_]+)*$";
static final Pattern regex = Pattern.compile(check);
/**
* 描述:验证邮箱格式是否正确
* @param email
* @return
*/
public static boolean checkEmail(String email)
{
boolean flag = false;
Matcher matcher = regex.matcher(email);
flag = matcher.matches();
return flag;
}
【强制】避免用Apache Beanutils进行属性的copy。
说明:Apache BeanUtils性能较差,可以使用其他方案比如Spring BeanUtils, Cglib BeanCopier,注意均是浅拷贝。
【强制】注意 Math.random() 这个方法返回是double类型,注意取值的范围 0≤x<1(能够取到零值,注意除零异常),如果想获取整数类型的随机数,不要将x放大10的若干倍然后取整,直接使用Random对象的nextInt或者nextLong方法。