坏习惯一:调用低效的构造器,创建包装类型的对象。
反例:
正解:
解惑:使用 Long.valueOf(long) 代替 new Long(long),可以提高性能。
如 Long 源码所示,如果当传入的值介于 -128~127 时,会优先从缓存中返回缓存的值,而不是进行 new,充分利用空间换取时间,所以当值介于 -128~127 时,采取 Long.valueOf(long) 的效率要比 new Long(long) 快很多。
建议:
a)凡是涉及到 Long, Integer, Short, Character 以及 Byte 创建对象时,优先采用高效的 valueOf() 方法,而不是直接用低效构造器创建实例。
b)享元设计模式在这儿用到了,什么是享元模式?
坏习惯二:使用 keySet 迭代器迭代 Map,获取对应的 value。
反例:
正解:
解惑:keySet 方式遍历 Map 的性能不如 entrySet 性能好。
如果采用 keySet 的方式获取 Map 中 key,然后通过 key 获取 Map 对应的 value,如上图 HashMap 源码所示,每次都需要通过 key 去计算对应的 hash 值,然后再通过 hash 值获取对应的 value,效率会低不少。
建议:
a)如果想获取 Map 对应的 key 和 value,则推荐使用 entrySet。
b)如果只是单纯获取 Map 对应的 key,则推荐使用 keySet。
坏习惯三:使用 new Date().getTime() 获取当前时间戳。
反例:
正解:
解惑:如下图 Date 源码所示,Date 构造方法中最终还是调用了 System.currentTimeMillis() 方法来获取时间戳。
建议:
a)获取当前毫秒数采用 System.currentTimeMillis(),而不是new Date().getTime();
b)获取更加精确的纳秒级时间值,采用 System.nanoTime;
c)在 JDK8 中,针对统计时间等场景,建议使用 Instant 类。
坏习惯四:循环中使用 ”+“ 号拼接字符串。
反例:
正解:推荐使用 StringBuilder/StringBuffer 进行字符串拼接。
解惑:参考「Java 程序该怎么优化?技巧篇」本次不赘述。
编码时易犯的一些小毛病
毛病一:变量作为 equals() 方法的调用方。
反例:
正解:
解惑:totalCount 应该作为方法 equals() 的调用方,而不是参数 作为调用方,因为参数作为调用方会出现空指针异常。
建议:
a)字符串的比较,常量建议当做 equals() 方法的调用方;
b)字符串判断空,建议用项目中的工具类。
毛病二:对象为 null 的检查滞后。
反例1:
正解:请在使用 data 对象前,做好是否为 null 的判断。
反例2:
正解:请提前检查对象 fos、fis 是否为 null,应该在第一次使用前就做空值检查。
解惑:后置对象为空的检查,可能会导致空指针异常的发生。
毛病三:要求传入非空的方法,传入空值。
反例:
正解:signInfo 变量的值可能存在为空的情形,导致发生空指针异常。
建议:发生异常的时候,方法该终止就终止;尽量做好防御性编程,该校验的参数进行必要的校验。
寄语写最后
常在河边站哪有不湿鞋,再牛逼的码农,编码也会有失误的时候,很有必要借助一款代码检查工具,做最后一道防线。
在这里,推荐 FindBugs、Checkstyle、SonarQube 三款代码检查工具,不过我用的最多的当属 FindBugs,可以拿去一试,使用门槛几乎为零。
该如何写出优雅的代码?
编码时:少一点不行
坏习惯一:记录日志时,缺失参数。
反例:
正解:
2.说一句废话:图中的 isVarfiy 是什么鬼?莫非是 isVerify,单词好好拼,千万别拼错,不然易被后人拍砖。
坏习惯二:记录日志时,缺失占位符 {}。
反例:
正解:类似的这种问题,多数程序员都犯过。记录日志时占位符少,而参数值多,日志输出时想打印的参数,日志中却没有打印。
如上面截图中代码所示,想输出请求的 queryString,但是由于缺失对应的占位符 {},则不会打印到日志中。
坏习惯三:使用 switch 时,缺失 default。
正解:
坏习惯四:使用 switch 时,缺失 break。
反例:
正解:
编码时:多一点不行
毛病一:看似判 null 很严谨,实则多余。
反例:
正解:这应该是吃过空指针的亏,刚 new 出来的对象,二话不说又判断对象是否为 null,真是多余的判断。
这么写并不能彰显代码很 new B(),反而使代码有失大雅。
毛病二:担心对象使用出现空指针,就疯狂 new。
反例:
正解:创建对象而没有使用,除了白白的浪费内存空间,如果在高并发情况下,效率、内存占用可想而知。
难道上面代码是为了 new B(),百思不得其真解。
毛病三:多分支对应功能却是一模一样。
反例:
正解:
解惑:功能相同的分支进行合并到一起,代码确实能简化不少,优雅不少。
毛病四:闲置不用的对象,到处都是。
反例 1:
反例 2:
正解:闲置不用的对象,到处都是,若留着就是耗内存,而且影响雅观,不用的变量、代码段建议删除。
寄语写最后
常在河边站哪有不湿鞋,金无足赤人无完人,再牛逼的团队,编码都会有出 Bug 的时候。近期微信公众号推出了一个专辑功能,而我迫不及待的想体验。
谁成想,当我点击创建专辑时,输入专辑名称「码农心声」等信息,然后点击保存,却发现列表页面出现了多个「码农心声」,而且赶紧截了个图,不知道是不是个 Bug?
But who cares?多出来的直接删除就行啦,又不影响使用。
书接上篇,本次一起继续探讨一下,该如何写出优雅的代码?
编码时:搞的复杂并不好
坏习惯一:多余的 if/else。
反例:
类似上面这种写法,if/else 就显得有点高射炮打蚊子,有的同学就会按照下面方式进行简化。
addBool = (i == 0) ? true : false;
这种方式简化当然跑起来没问题,代码确实简化了不少,但是还是略显冗余啊。
正解:
addBool = (i == 0);
坏习惯二:多余的 else。
反例:
仅以上图为例,每次看到类似截图中的代码,心里都发毛,完全可以提前 return,进而干掉 else 分支。
正解:
心声:
2. 提前终止程序,绝大多数情况下,会节省很多不必要的开销(会减少很多无效的判断,减少无效变量、对象的创建)。
编码时:不善于用轮子
毛病一:随处可见的判空逻辑。
反例:
if(merId == null || "".equals(merId)) { //do something}
程序为了避免 NPE,很多时候都需要做非空检查,当然上面这种检查方式很有效,只是项目中有太多的属性字段等待去校验,如果到处都是类似的判断,确实有点不太雅观。
很多同学会想着,自己封装 StringUtils 工具类,其实更推荐大家使用三方的轮子。
推荐1:Apache commons-lang 工具包
if(StringUtils.isBlank(merId)) { //do something}
推荐2:谷歌的 Guava 工具包
if(Strings.isNullOrEmpty(merId)) { //do something}
心声:
1. Apache Commons 下面的工具包,用熟了,确实很香。
毛病二:完成对象间的属性 Copy,编写冗长的代码。
反例:
... ...batchEntity.setNotifyType(batchEntityOld.getNotifyType());batchEntity.setUpdatedTime(batchEntityOld.getUpdatedTime());batchEntity.setBizType(batchEntityOld.getBizType());batchEntity.setMerchId(batchEntityOld.getMerchId());... ...
正解:
方式 1:采用 Apache BeanUtils 完成属性赋值。
BeanUtils.copyProperties(batchEntityOld,batchEntity);
方式 2:采用 Spring BeanUtils 完成属性赋值。
BeanUtils.copyProperties(batchEntity,batchEntityOld);
对的,你没看错,方法名称、参数都一样,但是 target、source要注意(稍有不慎,就入坑啊!)
不过,这里更推荐使用 Spring BeanUtils,而且在阿里开发规约中也明确强制使用 Spring BeanUtils 完成属性的 copy。
另外,为什么不建议使用 Apache BeanUtils 呢?看看源码就知道啦。
性能问题,估计跟日志输出、类型判断、用 + 号进行字符串拼接等脱不了关系。
寄语写最后
精妙的代码简洁明了,如果将这个代码给其他程序员看,他们会说:“哇,这代码写得真好。”那感觉很像在写一首诗。
我等采石之人当心怀大教堂之愿景——《程序员修炼之道》。
在一个项目的整体结构之内,总有空间展示个性和匠心……百年之后,我们的技艺或许如今日的土建工程师看待中世纪大教堂建造者使用的技法一样陈旧,但是我们的匠心却会得到尊重——匠人精神。
本次继续探讨一下,该如何写出健壮的代码?
编码时:看似顺眼,实则不然。
举个栗子🌰:
String amount = request.getParameter("amount");// 校验金额小数点后最多两位小数BigDecimal a = new BigDecimal(amount);if (a.doubleValue() * 100 - Math.floor(a.doubleValue() * 100) != 0) { System.out.println("交易金额错误"); // do something ... ...}
摘一段跑在生产环境上的代码,代码咋一看没啥问题,主要功能是获取请求参数;然后完成数据校验。
看似很顺眼,但是你细品,就会发现其中之奥秘,下面一起在本地跑跑代码,来分析一下到底会存在什么问题?
问题一:坑死人的 NPE
输入:null(当 amount 输入为空时)
输出:
Exception in thread "main" java.lang.NullPointerException at java.math.BigDecimal.<init>(BigDecimal.java:806) at PayController.main(PayController.java:300)
分析:
根据上面异常信息,见 BigDecimal 的源码第 806 行,如下图所示,很显然 BigDecimal 构造不会判断传入的 val 是否为空,所以会出现空指针异常。
目前没有出现问题,那只能算庆幸,不过终究是个定时炸弹。切记调用 BigDecimal 的构造时,请勿传入 null 值。
心声:
身边老码农真真的排查了好长时间,问题场景与此类似,直接阻断了程序后续的流程。
问题二:同样是传入数字,结果咋就匪夷所思。
输入:
6666.66(当 amount 输入为 6666.66)
当 amount 输入为 6666.66 时,amount 的值校验通过。真的是看到的这个样子吗?换个数试试呗。
输入:
8888.88(当 amount 输入为 8888.88)
输出:
交易金额错误
分析:
容我拆解一下代码,当 amount 传入为 8888.88 时:
double d1 = a.doubleValue() * 100;double d2 = Math.floor(a.doubleValue() * 100);System.out.println(d1); // 输出:888887.9999999999System.out.println(d2); // 输出:888887.0System.out.println(d1 - d2); // 输出:0.9999999998835847
很显然, d1 - d2 的值 != 0,那么如下表达式的值则满足,会输出交易金额错误。
为什么呢?归根揭底是 double 运算时精度丢失而导致程序处理出错,虽然在 Java 中提倡用 BigDecimal 进行四则运算,但是上面的校验实现,貌似跟 BigDecimal 没有啥关系,到底该怎么解决呢?
无脑实现方式:
if (amount.contains(".") && amount.substring(amount.indexOf(".") + 1).length() > 2) { System.out.println("校验失败 2"); // do something ... ...}
如上面代码段所示,直接判断传入的 amount 字符串小数点后面的位数就可以啦。
当然,仁者见仁智者见智,实现方式有很多,不去多深究。
编码时:时间转换也作祟
举个栗子🌰:
public static long convertDaysToMilliseconds(int days) { return 1000 * 3600 * 24 * days;}
分析:
1000 * 3600 * 24 * days 结果默认为 int 类型,最大值为 2147483647,如果超过 int 范围,则会出现截断,程序不会出错,但是结果却匪夷所思。
例如:
当 days 输入为 30 时,程序输出:-1702967296。
改进方式一:
改进方式二:
再举个栗子🌰:
public static Date getDate(int seconds) { return new Date(seconds * 1000);}
分析:
当 seconds * 1000 值为 int 类型,当超过 int 最大值为2147483647 时,程序不会出错,但是结果却匪夷所思。
改进方式:
分享一下心声:
浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。 二进制无法精确表示大部分的十进制小数。 —— 请自行科普,留作业。
BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。 如:BigDecimal g = new BigDecimal(0.1f); 实际的存储值为:0.10000000149 优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法,此方法内部其实执行了 Double 的 toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。 —— 阿里开发手册
该如何利用注解写出精简的代码?
编码时:重复的校验,随处可见。
举个栗子🌰:
再举个栗子🌰:
如栗子示意,项目中参数校验随处可见,面对如此简单而又繁琐的工作量,你有何高见?
招式一:带你生撸一个验证框架
招式一不多说,徒手造轮子而已;重点说说招式二,引用 Hibernate Validator 类库替换参数校验,如下图示意,只需通过注解就轻松实现参数的基本验证。
建议:参数校验直接引用三方的类库实现,例如引用 Hibernate Validator,能让业务代码简化不少,代码 B 格略有提升。
敢问,你会用注解吗?注解还能怎么用?
莫急,容我慢慢讲来。
编码时:注解还可以这么用。
举个栗子🌰:
系统要根据上面常量类中的商户号来判断:是否需要进行校验账户信息、是否需要进行通知商户 ... ...
实现方式有很多种,看看项目中采用注解怎么实现的。
首先定义 PayAccInfoValidator 注解,用来标注是否需要校验账户信息。
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.FIELD)public @interface PayAccInfoValidator { boolean verify() default true;}
然后在商户号属性上加入注解标识。
采用反射+注解,编写验证工具类,主要关注代码中的关注点一、关注点二。
import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;
public class BusinessUtil {
private static final MerIdConstant MERID_CONSTANT = new MerIdConstant();
/** * 存放要验证支付账户信息的商户号 */ public static final Map<String, Boolean> VERIFY_MERID_MAP = new HashMap<String, Boolean>();
static { Class<?> clazz = MERID_CONSTANT.getClass(); for (Field field : clazz.getFields()) { // 关注点一 PayAccInfoValidator validator = field.getAnnotation(PayAccInfoValidator.class); if (validator != null) { try { // 关注点二 VERIFY_MERID_MAP.put(field.get(MERID_CONSTANT).toString(), validator.verify()); } catch (IllegalAccessException e) { } } } }}
代码不做详细解释,工具类用起来很简单,简易示例如下。
程序输出:
20020:需要进行验证
另外,校验是否需要通知商户该怎么实现呢?其实套路是一样的,照葫芦画瓢而已,拿去先用起来体会体会。
还有哪些奇淫技巧,能助力写出精简的代码?
编码时:删除多余,才会更精简。
很多项目流转到你手中时,很多功能模块已废弃 ... ...
胆放大,心留细,一定要敢于动手去重构。少即是多,只有去除多余的代码,方能让代码更精简更完美。
精简代码时,能想到的事项如下:
多余的 TODO,搞的后人不知所措。
再举个栗子🌰🌰
由于功能下线,导致配置文件中依然维护多余的配置,每次上线还担心线上网络不通,解除后顾之忧,势必要去除多余的配置信息。
再举个栗子🌰🌰🌰
如截图示意,深入分析一下:
当然,写出精简的代码,仁者见仁智者见智,主要与团队的开发规范有一定关系。
在项目研发中,还有哪些可以简化代码的地方呢?
其实,还有很多,不一一枚举,下面简单看几张图,看看这些坏习惯,你是否犯过?
编码时:坏习惯,让代码显得冗长。
举个栗子🌰:
正解:采用 for-each 进行循环遍历集合,代码会相对简化一点。
以往分享过的栗子🌰:
正解:else 略显多余,可以去除。
心里话:提前终止语句,快速失败,会让代码简化不少,效率提升不少。
以往分享过的栗子🌰:
正解:在 return 前的判断,貌似略显多余,可以修改为。
心里话:在编码时,利用好 return 关键字,可以提前让函数返回,避免定义很多中间变量。
拿个项目中的栗子🌰:
正解:利用 return 关键字,可以适当调整如下。
心里话:在编码时,提前终止程序,会减少圈的复杂度,结构更清晰。