1. 代码评审的成长
大家好,我是方圆
。最近针对近半年的代码评审做了一次总结分享,主要是一些很简单的问题及代码优化注意事项。
2. 详细问题及注意事项
2.1 Integer, Long, String 类型要使用equals来判断相等
为什么Integer
和Long
多数情况下用==
比较结果也正确呢?
int
和Integer
类型比较,都为true,因为会把Integer拆箱后再去比- 两个
非new
出来的Integer(eg: Integer a = 5
),使用==
比较且大小在-128 ~ 127之间,则为true,否则为false。
这个范围可以使用-XX:AutoBoxCacheMax=?
来进行调整 - 两个Integer进行比较,
其中一个是new出来的
,==
比较的话为false,因为==比较在对引用类型比较时比较的是存放在堆中的地址值
2.2 加密异常需要抛出来
这种情况下如果加密出现异常被catch住之后,它还会让代码继续向下执行,所以应该在catch块中把异常抛出来
2.3 double类型加减运算精度丢失问题
这个是老生常谈的问题,在《Effective Java》这本书中有过关于浮点数计算的提醒:浮点数不用于精确计算,可用在科学计数取近似值,所以在代码中进行小数运算要使用BigDecimal
2.4 count(字段)替换为count(0)
如果想在字段值为null的时候同样也计数,需要使用count(0),count(字段)当字段为null时不计数
2.5 事务注解需要加上rollbackFor
如果不加的话,则只针对RuntimeException
回滚
2.6 集合转数组
- 使用集合转数组的方法,必须使用toArray(T[] array),传入类型完全一样的数组
List<Integer> list = new ArrayList<>();
list.add(1);
Integer[] ints = (Integer[]) list.toArray(new Integer[0]);
其中数组空间大小的length
有以下几种情况
- 等于0,动态创建与size相同的数组,性能最好
- 大于0但小于size,重新创建大小等于size的数组,增加GC负担
- 等于size,在高并发情况下,数组创建完成之后,size正在变大的情况下, 负面影响与2相同
- 大于size,空间浪费,且在size处插入null值,存在NPE隐患
2.7 数组转集合
- 使用
Arrays.asList()
把数组转换成集合时,不能使用其修改集合相关的方法(add/remove/clear),否则会抛出UnsupportedOperationException
异常
因为Arrays.asList()
使用的是适配器模式,返回的对象是Arrays内部类,它只是做了转换,而后台的数据仍然是数组。
2.8 ArrayList的subList
- ArrayList的
subList
结果不可强转成ArrayList,否则会抛出ClassCastException
异常
subList
返回的是ArrayList
的内部类SubList
,并不是ArrayList而是ArrayList的一个视图,对于SubList子列表的所有操作最终会反映到原列表上
subList
场景中,高度注意对原列表的修改,否则会导致子列表的遍历、增加、删除产生ConcurrentModificationException
异常
ArrayList 创建SubList时,此时它们的modCount值一致。当ArrayList增加或者删除时会修改modCount,这时子列表中的modCount是没有被修改的。所以当子列表遍历时判断subList的modCount和ArrayList的modCount不一致,这就会抛出ConcurrentModificationException
异常
2.9 double转换BigDecimal
- 禁止使用构造方法
BigDecimal(double)
的方式把double
值转化为BigDecimal
对象
BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。
如:BigDecimal g = new BigDecimal(0.1f); 实际的存储值为:0. 1000000000000000055511151231257827021181583404541015625
优先使用入参为String的构造方法new BigDecimal("0. 1")
或BigDecimal.valueOf
方法
2.10 线程工厂
- 创建线程或线程池时需要指定有意义的线程名,方便排查问题,如下为南网异步线程池线程工厂的应用
public class WSServiceThreadFactory implements ThreadFactory {
private final String namePrefix;
private final AtomicInteger nextId = new AtomicInteger(0);
public WSServiceThreadFactory(String threadPoolName) {
namePrefix = "From WSServiceThreadFactory's " + threadPoolName + "-Worker-";
}
@Override
public Thread newThread(@NotNull Runnable r) {
String name = namePrefix + nextId.getAndIncrement();
return new Thread(null, r, name, 0);
}
}
2.11 SimpleDateFormat是线程不安全的
- 不要定义为static变量,如果定义 为static,必须加锁,或者使用JDK8中的
DateTimeFormatter
与LocalDateTime
- 测试用例如下
public class TestSimpleDateFormat {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
Date now = new Date();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
String format1 = dateFormat.format(now);
Date parse1 = dateFormat.parse(format1);
String format2 = dateFormat.format(parse1);
// 理论上format1和format2是一样的
System.out.println(format2.equals(format1));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
测试输出结果不是都为true,也会出现false。因为在SimpleDateFormat的format方法中,有calendar对象是线程不安全的
,多线程调用时被并发修改。
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// 这里并发修改calendar
calendar.setTime(date);
...
}
3. 代码优化
3.1 通用方法复用
例子中是计算两个时间间隔天数的逻辑,这种通用方法需要加到工具类中复用
3.2 操作资源用try-with-resources资源
3.3 SQL 条件字段不要加函数
- 否则会使索引失效
3.4 代码注释和方法拆分复用
- 代码注释描述清晰,其他小方法进行拆分,尽可能复用代码使逻辑清晰
3.5 工作中代码复用的样例
项目中对其他平台接口调用的代码结构是这样儿的,它有3层,给大家简单解释一下。
- 接口层:调用外部接口的方法
- 抽象层:通用的同步、异步调用逻辑
- 实现层:外部接口方法的业务实现
我们需要照样子,为新平台的接口对接,再搬一套类似的结构,像下边儿这样
这个结构照抄起来非常的简单,但是当我写到具体接口实现的时候出现问题了:
我们拿同步派车单异常接口来举例子,两个平台都想要这份数据,而且它们的执行逻辑是一样的,我就在想:“如果不写重复的代码,该怎么复用之前的代码把这个接口开发完?”
当然我们复制一下放到新添加的接口实现类中是可以,但是不优雅,而且还是被idea提示代码重复。
我们先看下这个要复用的方法,它在OutProxyServiceImpl
里,是私有的。如果我们想复用它,就需要把它抽出来,抽出来放在哪儿又是一个问题。如果不抽出来,把私有方法变成公开的(public)方法,又需要将调用电网管理平台的实现类注入到调用都匀局的实现类中,业务不相关的bean注入,这样也不好。
public class OutProxyServiceImpl extends AbstractOutProxyService {
...
private ShipmentErrorDto initShipmentErrorDto(BuException exception) {
ShipmentErrorDto errorDto = new ShipmentErrorDto();
BuShipment shipment = shipmentService.getById(exception.getShipmentId());
// 派车单号
errorDto.setShipmentPlanCode(shipment.getCode());
// 车牌号
errorDto.setPlateNo(exception.getVehicleLp());
// 司机姓名
errorDto.setDriverName(shipment.getDriverName());
// 是否影响到达时间
errorDto.setIsAffectArrivalTime(exception.getIsAffectArrivalTime());
// 异常等级
errorDto.setExceptionLevel(exception.getExceptionLevel());
// 异常类别
errorDto.setExceptionType(exception.getReason().toString());
// 异常内容,取备注
errorDto.setExceptionText(exception.getRemark());
// 发生时间,格式 yyyy-MM-dd HH:mm:ss
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
errorDto.setTime(format.format(exception.getEventTime()));
return errorDto;
}
...
}
所以,我想了一个新的代码结构,来放公用的代码,像下边儿这样
还是三层:
- 接口直接被具体实现类实现,不再关联抽象层
- 抽象层现在被大家公用,其中放的就是这些同步、异步调用方法和需要被复用的业务方法
那么这样,我们把上述赋值方法拿到抽象类里,用protected修饰,就可以复用它了
虽然解决了问题,但是我还是觉得它不够好,一是业务代码放在抽象层里,不合适;二是这个代码结构变化比较大,在这个基础上又想了另一种方法,如下
这下也实现了代码复用,而且保持代码结构基本不变,只需在抽象层做一层拓展即可。把所有想要复用的方法都拿出来往里边儿放用protected修饰,非常的方便。
“That’s all”