软件质量,不但依赖于架构及项目管理,更与代码质量紧密相关。简洁高效的代码不但易于阅读,更能避免潜在 BUG 与风险,提高代码质量。近期,一位 Oracle 程序员在 Hacker News 上吐槽自己的工作,引起了热议。
这个工程师的核心痛点是,Oracle 经历长期的产品线迭代,代码异常庞大、逻辑复杂,整个代码中充斥着神秘的宏命令。每新增一个特性或者修复 BUG,该工程师都需要大量的调研,小心谨慎的进行着日常的工作。而 Oracle 每次的版本发布都经历数百万次的测试,脑补一下,如噩梦一般。那么我们应该如何编写简洁高效的代码呢?其实业内有过很多相关书籍,比如经典的书籍有《代码整洁之道》、《编写可读代码的艺术》、《重构:改善既有代码的设计》,可用于修炼内功。以及我们有严格的代码规范以及方便的静态代码扫描工具,可用于加强研发代码质量能力。
其实代码规范和静态代码扫描工具能够帮助我们完成很多代码简洁的工作。诸如:注释、命名、方法、异常、单元测试等多个方面。但却无法总结了一些代码简洁最佳实践,其实 Java 是面向对象语音,而面向对象的特征是封装、继承、多态,巧妙的运用这三大特性、理解 Java 的一些关键字特性、语音特性、阅读 JDK 源码,就可以写出相对简洁的代码了。
// 修改前
if(list.size()>0) {
return true;
} else {
return false;
}
// 修改后
return list.size()>0;
(1) if/else 语法:if 语句包含一个布尔表达式,当 if 语句的布尔表达式值为 false 时,else 语句块会被执行;
(2) return 关键字:返回一个任意类型的值;
(3) list.size()>0 表达式:list.size()方法本身是一个返回 int 类型数值的函数,而与>0 组成了一个布尔表达式;
(1) 局部变量 list 的数据类型与该方法的返回值类型一致,而多余的变量也将会增加 JVM 垃圾回收的消耗;
(2) 局部变量 list 只是负责接收了 mapper.queryList(params)的返回值,而并没有其他逻辑处理;
(3) 此代码存在于 service 层和 mapper 层之间,可以在框架层面进一步抽象,利用注解、java8 default 方法等进一步改进;
代码中 if else 的存在只是因为 sendMessage 函数的第二个参数会有两种情况(成功 /失败),尽量让判断最小化;
( 1 )大坨的 set 方法很影响代码可读性,可封装成特定方法或者使用 lombok 工具简化代码;
( 2 )局部变量就近声明,增加可读性,局部变量声明和使用地方距离遥远,会导致的读者频繁滑动;
( 3 )可不声明变量尽量不要声明多余的变量,冗余代码;(如 date、time 两段代码);
( 1 )遍历集合(List、Map 等)、Sum、Max、Min、Avg、Sort、Distinct 等等
( 2 )函数接口
( 3 )谓词(Predicate)使用
( 4 )实现 Map 和 Reduce
( 5 )实现事件处理 /简化多线程
( 1 )只能顺序处理 list 中的数据(process one by one)
( 2 )不能充分利用多核 cpu
( 3 )不利于编译器优化(jit )
( 1 )不一定需要顺序处理 List 中的元素,顺序可以不确定
( 2 )可以并行处理,充分利用多核 CPU 的优势
( 3 )有利于 JIT 编译器对代码进行优化
( 4 )代码看起来更简洁,完全交给编译器内部循环
在 Java8 中,接口中的方法可以被实现,用关键字 default 作为修饰符来标识,接口中被实现的方法叫做 default 方法。使用 default 方法,当接口发生改变的时候,实现类不需要做改动,所有的子类都会继承 default 方法。
( 1 )完全无视默认方法(直接继承上级接口的默认方法)
( 2 )重新申明默认方法为抽象方法(无实现,具体子类必需再次实现该方法)
( 3 )重新实现默认方法(重写了默认方法的实现,依然是一个默认方法)
Java8 中新增了 LocalDate 和 LocalTime 接口,为什么要搞一套全新的处理日期和时间的 API ?因为旧的 java.util.Date 实在是太难用了。
( 1 ) java.util.Date 月份从 0 开始,一月是 0,十二月是 11,变态吧! java.time.LocalDate 月份和星期都改成了 enum,就不可能再用错了。
( 2 ) java.util.Date 和 SimpleDateFormatter 都不是线程安全的,而 LocalDate 和 LocalTime 和最基本的 String 一样,是不变类型,不但线程安全,而且不能修改。
( 3 ) java.util.Date 是一个“万能接口”,它包含日期、时间,还有毫秒数,如果你只想用 java.util.Date 存储日期,或者只存储时间,那么,只有你知道哪些部分的数据是有用的,哪些部分的数据是不能用的。在新的 Java8 中,日期和时间被明确划分为 LocalDate 和 LocalTime,LocalDate 无法包含时间,LocalTime 无法包含日期。
当然,LocalDateTime 才能同时包含日期和时间。
新接口更好用的原因是考虑到了日期时间的操作,经常发生往前推或往后推几天的情况。用 java.util.Date 配合 Calendar 要写好多代码,而且一般的开发人员还不一定能写对。
1、Clock 时钟。Clock 类提供了访问当前日期和时间的方法,Clock 是时区敏感的,可以用来取代 System.currentTimeMillis(),来获取当前的微秒数。某一个特定的时间点也可以使用 Instant 类(为 Final 类)来表示,Instant 类也可以用来创建老的 java.util.Date 对象。
2、LocalDate 和 LocalTime、LocalDateTime(均为 Final 类,不带时区)的一系列计算。LocalDateTime 和 Instant 两者很像都是不带时区的日期和时间,Instant 中是不带时区的即时时间点。比如:两个人都是 2018 年 4 月 14 日出生的,一个出生在北京,一个出生在纽约;看上去他们是一起出生的(LocalDateTime 的语义),其实他们是有时间差的(Instant 的语义)
Stream 是对集合的包装,通常和 lambda 一起使用。使用 lambdas 可以支持许多操作。如 map,filter,limit,sorted,count,min,max,sum,collect 等等。 同样,Stream 使用懒运算,他们并不会真正地读取所有数据。遇到像 getFirst()这样的方法就会结束链式语法,通过下面一系列例子介绍:比如我有个 Person 类,就是一个简单的 pojo, 针对这个对象,我们可能有这样一系列的运算需求。
//sumAll 算法很简单,完成的是将 List 中所有元素相加。
public static int sumAll(List<integer> numbers) {</integer>
int total = 0;
for (int number : numbers) {
total += number;
}
return total;
}
sumAll 算法很简单,完成的是将 List 中所有元素相加。某一天如果我们需要增加一个对 List 中所有偶数求和的方法 sumAllEven,那么就产生了 sumAll2,如下: public static int sumAll2(List<integer> numbers) {</integer>
int total = 0;
for (int number : numbers) {
if (number % 2 == 0) {
total += number;
}
}
return total;
}
又有一天,我们需要增加第三个方法:对 List 中所有大于 3 的元素求和,那是不是继续加下面的方法呢?sumAll3
public static int sumAll3(List<integer> numbers) {</integer>
int total = 0;
for (int number : numbers) {
if (number > 3) {
total += number;
}
}
return total;
}
观察这三个方法我们发现,有很多重复内容,唯一不同的是方法中的 if 条件不一样(第一个可以看成 if(true)),如果让我们优化,可能想到的第一种重构就是策略模式吧,代码如下:
这无疑使用设计模式的方式优化了冗余代码,但是可能要额外增加几个类,以后扩展也要新增,下面看看使用 lambda 如何实现,声明方法:第一个参数还是我们之前传递的 List 数组,第二个看起来可能有点陌生,通过查看 jdk 可以知道,这个类是一个谓词(布尔值的函数)
public static int sumAllByPredicate(List<integer> numbers, Predicate<integer> p) {</integer></integer>
int total = 0;
for (int number : numbers) {
if (p.test(number)) {
total += number;
}
}
return total;
}
//调用:
sumAllByPredicate(numbers, n -> true);
sumAllByPredicate(numbers, n -> n % 2 == 0);
sumAllByPredicate(numbers, n -> n > 3);
代码是不是比上面简洁了很多?语义也很明确,重要的是不管以后怎么变,都可以一行代码就修改了。。。万金油啊。
JAVA8 还推出了很多特性,来简化代码。比如 String.join 函数、Objects 类、Base64 编码类。
好的代码需要不停的打磨,作为一个优秀的工程师,我们应该严格遵守,每次提交的代码要比迁出的时候更好。经常有人说,作为工程师一定要有团队精神,但这种精神并不是说说而已的,需要实际的行动来体现的。设计模式、JDK 的新特性都是我们可以借助的经验,编码完成后思考一下,还可不可以在简化、优化,不要成为一个“作恶”的工程师。
马铁利,随行付架构部负责人 & TGO 鲲鹏会北京分会会员,10 年全栈工程师,擅长微服务分布式架构设计。主要负责随行付架构部日常管理;参与构建微服务平台周边基础设施及中间件;负责随行付对外开源等事宜。
更多内容请关注微信公众号:黑少聊微服务