前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >编码时易忽略的坏习惯-优化编码(仅用于个人学习,不喜勿喷--持续更新)

编码时易忽略的坏习惯-优化编码(仅用于个人学习,不喜勿喷--持续更新)

作者头像
botkenni
发布2022-12-07 14:06:23
5350
发布2022-12-07 14:06:23
举报
文章被收录于专栏:IT码农

坏习惯一:调用低效的构造器,创建包装类型的对象。

反例:

正解:

解惑:使用 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,可以拿去一试,使用门槛几乎为零。

该如何写出优雅的代码?

 编码时:少一点不行 

坏习惯一:记录日志时,缺失参数。

反例:

正解:

  1. 日志打印时,占位符 {} 要严格与参数相对应,如果对应不上,按照截图示意,日志输出则不会打印 queryString 的参数,会直接输出 {},但是某些版本下会出现空指针异常。

2.说一句废话:图中的 isVarfiy 是什么鬼?莫非是 isVerify,单词好好拼,千万别拼错,不然易被后人拍砖。

坏习惯二:记录日志时,缺失占位符 {}。

反例:

正解:类似的这种问题,多数程序员都犯过。记录日志时占位符少,而参数值多,日志输出时想打印的参数,日志中却没有打印。

如上面截图中代码所示,想输出请求的 queryString,但是由于缺失对应的占位符 {},则不会打印到日志中。

坏习惯三:使用 switch 时,缺失 default。

正解:

  1. 采用 switch 时,当所有 case 都不匹配时,会走 default 逻辑。为了程序更完成、更优雅,在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有。
  2. 说一句废话:截图中的代码格式,尤其是 break 前的分号,你能忍受吗?

坏习惯四:使用 switch 时,缺失 break。

反例:

正解:

  1. 在一个 switch 块内,每个 case 要么通过 break/return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止;
  2. 注意 break 是退出 switch 语句块,而 return 是退出方法体。

 编码时:多一点不行 

毛病一:看似判 null 很严谨,实则多余。

反例:

正解:这应该是吃过空指针的亏,刚 new 出来的对象,二话不说又判断对象是否为 null,真是多余的判断。

这么写并不能彰显代码很 new B(),反而使代码有失大雅。

毛病二:担心对象使用出现空指针,就疯狂 new。

反例:

正解:创建对象而没有使用,除了白白的浪费内存空间,如果在高并发情况下,效率、内存占用可想而知。

难道上面代码是为了 new B(),百思不得其真解。

毛病三:多分支对应功能却是一模一样。

反例:

正解:

解惑:功能相同的分支进行合并到一起,代码确实能简化不少,优雅不少。

毛病四:闲置不用的对象,到处都是。

反例 1:

反例 2:

正解:闲置不用的对象,到处都是,若留着就是耗内存,而且影响雅观,不用的变量、代码段建议删除。

 寄语写最后 

常在河边站哪有不湿鞋,金无足赤人无完人,再牛逼的团队,编码都会有出 Bug 的时候。近期微信公众号推出了一个专辑功能,而我迫不及待的想体验。

谁成想,当我点击创建专辑时,输入专辑名称「码农心声」等信息,然后点击保存,却发现列表页面出现了多个「码农心声」,而且赶紧截了个图,不知道是不是个 Bug?

But who cares?多出来的直接删除就行啦,又不影响使用。

书接上篇,本次一起继续探讨一下,该如何写出优雅的代码?

 编码时:搞的复杂并不好 

坏习惯一:多余的 if/else。

反例:

类似上面这种写法,if/else 就显得有点高射炮打蚊子,有的同学就会按照下面方式进行简化。

代码语言:javascript
复制
addBool = (i == 0) ? true : false;

这种方式简化当然跑起来没问题,代码确实简化了不少,但是还是略显冗余啊。

正解:

代码语言:javascript
复制
addBool = (i == 0);

坏习惯二:多余的 else。

反例:

仅以上图为例,每次看到类似截图中的代码,心里都发毛,完全可以提前 return,进而干掉 else 分支。

正解:

心声:

  1. 简单就是美,代码写的越少,犯错的几率就越小。

2. 提前终止程序,绝大多数情况下,会节省很多不必要的开销(会减少很多无效的判断,减少无效变量、对象的创建)。

  1. 每种编程语言都离不开 if/else 进行条件判断,如果在编码时,存在过多的 if/else 嵌套,代码的可读性就会下降,后期维护难度就会大大提高。

 编码时:不善于用轮子 

毛病一:随处可见的判空逻辑。

反例:

代码语言:javascript
复制
if(merId == null || "".equals(merId)) {      //do something}

程序为了避免 NPE,很多时候都需要做非空检查,当然上面这种检查方式很有效,只是项目中有太多的属性字段等待去校验,如果到处都是类似的判断,确实有点不太雅观。

很多同学会想着,自己封装 StringUtils 工具类,其实更推荐大家使用三方的轮子。

推荐1:Apache commons-lang 工具包

代码语言:javascript
复制
if(StringUtils.isBlank(merId)) {      //do something}

推荐2:谷歌的 Guava 工具包

代码语言:javascript
复制
if(Strings.isNullOrEmpty(merId)) {       //do something}

心声:

1. Apache Commons 下面的工具包,用熟了,确实很香。

  1. 谷歌的 Guava 工具包也不错,该类库经过高度的优化,方便我们快速编码,能规避不少编码错误。

毛病二:完成对象间的属性 Copy,编写冗长的代码。

反例:

代码语言:javascript
复制
... ...batchEntity.setNotifyType(batchEntityOld.getNotifyType());batchEntity.setUpdatedTime(batchEntityOld.getUpdatedTime());batchEntity.setBizType(batchEntityOld.getBizType());batchEntity.setMerchId(batchEntityOld.getMerchId());... ...

正解:

方式 1:采用 Apache BeanUtils 完成属性赋值。

代码语言:javascript
复制
BeanUtils.copyProperties(batchEntityOld,batchEntity);

方式 2:采用 Spring BeanUtils 完成属性赋值。

代码语言:javascript
复制
BeanUtils.copyProperties(batchEntity,batchEntityOld);

对的,你没看错,方法名称、参数都一样,但是 target、source要注意(稍有不慎,就入坑啊!)

不过,这里更推荐使用 Spring BeanUtils,而且在阿里开发规约中也明确强制使用 Spring BeanUtils 完成属性的 copy。

另外,为什么不建议使用 Apache BeanUtils 呢?看看源码就知道啦。

性能问题,估计跟日志输出、类型判断、用 + 号进行字符串拼接等脱不了关系。

 寄语写最后 

精妙的代码简洁明了,如果将这个代码给其他程序员看,他们会说:“哇,这代码写得真好。”那感觉很像在写一首诗。

我等采石之人当心怀大教堂之愿景——《程序员修炼之道》。

在一个项目的整体结构之内,总有空间展示个性和匠心……百年之后,我们的技艺或许如今日的土建工程师看待中世纪大教堂建造者使用的技法一样陈旧,但是我们的匠心却会得到尊重——匠人精神。

本次继续探讨一下,该如何写出健壮的代码?

 编码时:看似顺眼,实则不然。 

举个栗子🌰:

代码语言:javascript
复制
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 输入为空时)

输出:

代码语言:javascript
复制
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 值。

心声:

身边老码农真真的排查了好长时间,问题场景与此类似,直接阻断了程序后续的流程。

问题二:同样是传入数字,结果咋就匪夷所思。

输入:

代码语言:javascript
复制
6666.66(当 amount 输入为 6666.66)

当 amount 输入为 6666.66 时,amount 的值校验通过。真的是看到的这个样子吗?换个数试试呗。

输入:

代码语言:javascript
复制
8888.88(当 amount 输入为 8888.88)

输出:

代码语言:javascript
复制
交易金额错误

分析:

容我拆解一下代码,当 amount 传入为 8888.88 时:​​​​​​

代码语言:javascript
复制
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 没有啥关系,到底该怎么解决呢?

无脑实现方式:

代码语言:javascript
复制
if (amount.contains(".") && amount.substring(amount.indexOf(".") + 1).length() > 2) {      System.out.println("校验失败 2");      // do something ... ...}

如上面代码段所示,直接判断传入的 amount 字符串小数点后面的位数就可以啦。

当然,仁者见仁智者见智,实现方式有很多,不去多深究。

 编码时:时间转换也作祟 

举个栗子🌰:​​​​​​

代码语言:javascript
复制
public static long convertDaysToMilliseconds(int days) {    return 1000 * 3600 * 24 * days;}

分析:

1000 * 3600 * 24 * days 结果默认为 int 类型,最大值为 2147483647,如果超过 int 范围,则会出现截断,程序不会出错,但是结果却匪夷所思。

例如:

代码语言:javascript
复制
当 days 输入为 30 时,程序输出:-1702967296。

改进方式一:

改进方式二:

再举个栗子🌰:​​​​​​​

代码语言:javascript
复制
public static Date getDate(int seconds) {    return new Date(seconds * 1000);}

分析:

当 seconds * 1000 值为 int 类型,当超过 int 最大值为2147483647 时,程序不会出错,但是结果却匪夷所思。

改进方式:

分享一下心声:

  1. 禁止使用 double 直接参与金额运算,会出现意想不到的结果。

浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。 二进制无法精确表示大部分的十进制小数。 —— 请自行科普,留作业。

  1. 禁止使用构造方法 BigDecimal(double)的方式把 double 值转化为 BigDecimal 对象。

BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。  如:BigDecimal g = new BigDecimal(0.1f); 实际的存储值为:0.10000000149  优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法,此方法内部其实执行了 Double 的 toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。 —— 阿里开发手册

  1. 那些看似顺眼的代码,或者线上跑着的代码,未必就没问题,只是没有走到异常分支上去,随着时间的推移,定时炸弹迟早会爆,定期审查代码,以及充分的测试是非常的必要。

该如何利用注解写出精简的代码?

 编码时:重复的校验,随处可见。 

举个栗子🌰:

再举个栗子🌰:

如栗子示意,项目中参数校验随处可见,面对如此简单而又繁琐的工作量,你有何高见?

招式一:带你生撸一个验证框架

招式二:API参数如何验证?别纠结,拿去用就是

招式一不多说,徒手造轮子而已;重点说说招式二,引用 Hibernate Validator 类库替换参数校验,如下图示意,只需通过注解就轻松实现参数的基本验证。

建议:参数校验直接引用三方的类库实现,例如引用 Hibernate Validator,能让业务代码简化不少,代码 B 格略有提升。

敢问,你会用注解吗?注解还能怎么用?

莫急,容我慢慢讲来。

 编码时:注解还可以这么用。 

举个栗子🌰:

系统要根据上面常量类中的商户号来判断:是否需要进行校验账户信息、是否需要进行通知商户 ... ...

实现方式有很多种,看看项目中采用注解怎么实现的。

首先定义 PayAccInfoValidator 注解,用来标注是否需要校验账户信息。​​​​​​​

代码语言:javascript
复制
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.FIELD)public @interface PayAccInfoValidator {        boolean verify() default true;}

然后在商户号属性上加入注解标识。

采用反射+注解,编写验证工具类,主要关注代码中的关注点一、关注点二。​​​​​​​

代码语言:javascript
复制
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) {                }            }        }    }}

代码不做详细解释,工具类用起来很简单,简易示例如下。

程序输出:

代码语言:javascript
复制
20020:需要进行验证

另外,校验是否需要通知商户该怎么实现呢?其实套路是一样的,照葫芦画瓢而已,拿去先用起来体会体会。

​​​​​​​

还有哪些奇淫技巧,能助力写出精简的代码?

 编码时:删除多余,才会更精简。 

很多项目流转到你手中时,很多功能模块已废弃 ... ...

胆放大,心留细,一定要敢于动手去重构。少即是多,只有去除多余的代码,方能让代码更精简更完美。

精简代码时,能想到的事项如下:

  • 多余的 Maven 依赖删除;
  • 多余的配置信息删除;
  • 废弃的 Module、API 、包、类、方法 删除;
  • 多余的 常量、变量 删除;
  • 多余的 导入、注解、注释、日志 删除;
  • 多余的 TODO 删除;
  • 举个栗子🌰

多余的 TODO,搞的后人不知所措。

再举个栗子🌰🌰

由于功能下线,导致配置文件中依然维护多余的配置,每次上线还担心线上网络不通,解除后顾之忧,势必要去除多余的配置信息。

再举个栗子🌰🌰🌰

如截图示意,深入分析一下:

  • 标注 3:接口中的方法默认都是 public 修饰,可以去除;
  • 标注 2、4:子类都没有实现定义的方法,可以去除;
  • 标注 1:子类没有实现该接口,则该接口可以去除。

当然,写出精简的代码,仁者见仁智者见智,主要与团队的开发规范有一定关系。

在项目研发中,还有哪些可以简化代码的地方呢?

  • 利用反射进行对象赋值,可以简化大批量的赋值代码(计划:单独开篇去讨论);
  • 利用设计模式,例如工厂模式、模板方法模式,可以消除大量的重复代码,甚至 if else 语句;
  • 利用属性拷贝工具,例如 BeanUtils,可以消除大量赋值代码(以往已经提及过);
  • 利用封装好的工具类,例如 StringUtils、CollectionUtils等可以简化大量的判断语句;
  • 利用增强的 for 循环遍历集合和数组,会简化代码;
  • 善于使用 return 关键字提前终止流程,会简化程序结构。

其实,还有很多,不一一枚举,下面简单看几张图,看看这些坏习惯,你是否犯过?

 编码时:坏习惯,让代码显得冗长。 

举个栗子🌰:

正解:采用 for-each 进行循环遍历集合,代码会相对简化一点。

以往分享过的栗子🌰:

正解:else 略显多余,可以去除。

心里话:提前终止语句,快速失败,会让代码简化不少,效率提升不少。

以往分享过的栗子🌰:

正解:在 return 前的判断,貌似略显多余,可以修改为。

心里话:在编码时,利用好 return 关键字,可以提前让函数返回,避免定义很多中间变量。

拿个项目中的栗子🌰:

正解:利用 return 关键字,可以适当调整如下。

心里话:在编码时,提前终止程序,会减少圈的复杂度,结构更清晰。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-12-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
腾讯云代码分析
腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,助力维护团队卓越代码文化。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档