代码质量实战
Any fool can write code that a computer can understand. Good programmers
write code that humans can understand. -- Martin Fowler
The only valid measurement of Code Quality: WTFs/minute
参考资料:
1.《代码大全》
2.《重构:改善既有代码的设计》
3.《代码整洁之道》
3.《Effective Java 3rd》
4.《阿里巴巴 Java 开发手册》
1. 不要迷信静态代码扫描工具检测的结果
例如 SonarLint 部分检测项不准确,需要甄别。
其中一条,‘String 方法单个字符使用‘‘比""效率高’,该条目有问题,用‘‘和""效率差距不大,随便使用哪个,参考:
https://stackoverflow.com/questions/33646781/java-performance-string-indexofchar-vs-string-indexofsingle-string
2. 考虑使用阿里 p3c 插件
该插件和 SonarLint 不冲突,可同时使用。
3. switch 必须有 default
[阿里手册] 在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使空代码。
原因:
1. 捕获意想不到的值
2. 处理默认情况
3. 告诉阅读代码的人你已经考虑了那种情况
参考:
https://stackoverflow.com/questions/4649423/should-switch-statements-always-contain-a-default-clause
4. Optional 值必须在调用 isPresent()后访问
说明:不调用 isPresent()方法,而直接调用 get()方法,可能会抛出 NoSuchElementException。
5. 不允许魔法值
[阿里手册] 不允许任何魔法值 ( magic number,即未经预先定义的常量 ) 直接出现在代码中。
原因:
1. 数值的意义难以理解
2. 数值需要变动时,可能要改不只一个地方
参考:
https://zh.wikipedia.org/wiki/%E9%AD%94%E8%A1%93%E6%95%B8%E5%AD%97_(%E7%A8%8B%E5%BC%8F%E8%A8%AD%E8%A8%88)
6. 工具类要有私有构造器
工具类是一些静态成员的集合,不希望被初始化,实例化对它没有任何意义。可以在私有构造器内部添加 throw new AssertionError(), 防止其被内部调用。
参考:Effective Java 第二版 第四条 通过私有构造器强化不可实例化的能力
7. 不要使用同步的类:Vector、Hashtable、Stack、StringBuffer
说明:
ArrayList | LinkedList -> Vector
Deque -> Stack
HashMap | ConcurrentHashMap -> Hashtable
StringBuilder -> StringBuffer
8. 不要用构造器初始化 Integer
说明:
根据 Integer.valueOf()的 java Doc:
Returns an Integer instance representing the specified int value. If a new Integer instance is not
required, this method should generally be used in preference to the constructor Integer(int), as
this method is likely to yield significantly better space and time performance by caching
frequently requested values. This method will always cache values in the range -128 to 127,
inclusive, and may cache other values outside of this range. 应该使用 Integer i = 1 或 Integer i = Integer.valueOf(1)的形式构造整形包装类型,由于整型的
缓存机制,这样可以提高时间性能且节省内存空间。
Integer i = 1 是语法糖,反编译后是 Integer i = Integer.valueOf(1),所以用 Integer i = 1 这种形式。
9. 所有的包装类对象之间值的比较全部使用 equals
判断 Integer 和 int 用 ==,Integer 会自动拆箱。判断 Integer 和 Integer 用 equals。
[阿里手册] 对于Integer var=?在-128至127之间的赋值,Integer对象是在IntegerCache.cache产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals方法进行判断。
10. 日志取代 System.out.println 和 printStackTrace
11. 遵守普遍接受的命名惯例
[阿里手册] 常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
[Effective Java] 把标准的命名惯例当作一种内在的机制来看待,并且学者用它们作为第二特性。
Effective Java 第二版 第 56 条 遵守普遍接受的命名惯例
12. BigDecimal.valueOf(double val) 取 代 new
BigDecimal(double val)
说明:
new BigDecimal(double x)会导致精度丢失。
用 BigDecimal(String val)也可以。
BigDecimal.valueOf()底层实现:
public static BigDecimal valueOf(double val) {
return new BigDecimal(Double.toString(val));
}
13. 不要忽略没有副作用的函数返回值
没有副作用的函数返回值被忽略,这种情况要么方法调用是没意义的,应该删除,要么源码的行为和预期不符。
14. 不要使用 catch 块中的“instanceof”测试异常类型
15. 集合操作前要判 null
说明:
此处 payApplyEntities 传入方法中,可能会被设为 null,要防止 NPE。
16. 避免不必要的初始化
17. 了解和使用类库
参考:
Effective Java 第二版 第 47 条 了解和使用类库
18. 提炼分解过长的方法
说明:
此问题在我们项目中出现频繁。
[阿里手册]
单个方法的总行数不超过 80 行。 说明:包括方法签名、结束右大括号、方法内代码、注释、
空行、回车及任何不可见字符的总行数不超过 80 行。
[重构]
我们要遵守这样一条原则:每当感觉需要以注释说明点什么的时候,我们就把需要说明的东
西写进一个独立的方法中,并以其用途命名。
[代码整洁之道]
函数的第一规则是短小,第二条规则还要更短小。
函数应该做一件事。做好这件事。只做这一件事。
参考:
《重构:改善既有的代码设计》 第三章 代码的坏味道
《代码整洁之道》
19. 移除不用的导入
20. 考虑“public static final”取代"public static" 说明:
因为被声明为"public static"的成员变量可以被任何对象修改,应该用 final 使其不可变。上面
这个例子可以把其设为 private。
21. 可合并的 if
说明:
合并可折叠的 if 可提高代码可读性
22. 坚持使用 Override 注解
说明:
[阿里手册]
所有的覆写方法,必须加@Override 注解。
反例:getObject()与 get0bject()的问题。一个是字母的 O,一个是数字的 0,加@Override 可
以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,其实现类会马上编
译报错。
参考:
Effective Java 第二版 第 36 条 坚持使用 Override 注解
23. Map 的 key 是枚举时用 EnumMap
说明:
根据 Java Doc:Implementation note: All basic operations execute in constant time. They are
likely (though not guaranteed) to be faster than their HashMap counterparts. EnumMap 可能比
HashMap 快。
24. 不要声明局部变量然后返回或抛出
25. “<>”取代”<...>”
26. Boolean 值不要冗余
27. 考 虑 用 @GetMapping, @PostMapping 等 取 代
@RequestMapping
28. 不要在同一行声明多个变量
说明:
在同一行声明多个变量不方便注释。
参考:
https://www.oracle.com/technetwork/java/javase/documentation/codeconventions-141270.htm
l#2991
29. 避免用对象引用访问静态成员或静态方法
说明:
[阿里手册]
避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直
接用类名来访问即可。
30. 按正确顺序声明修饰符
说明:
Java 语言规范推荐按以下顺序声明修饰符:
1. Annotations
2. public
3. protected
4. private
5. abstract
6. static
7. final
8. transient
9. volatile
10. synchronized
11. native
12. strictfp
31. 需要同时遍历 Map 的 Key 和 Value 时用 entrySet()
修改前:
修改后:
32. 不要忽略异常
说明:
至少包含一条说明,解释为什么可以忽略这个异常。
参考:
Effective Java 第二版 第 65 条 不要忽略异常
33. 不要将只包含一条语句的 lambda 嵌套在一个块中
34. 不要在循环中使用“+”连接字符串
说明:
[阿里手册] 循环体内,字符串的联接方式,使用 StringBuilder 的 append 方法进行扩展。 说
明:反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,然后进行
append 操作,最后通过 toString 方法返回 String 对象,造成内存资源浪费。
35. 使 用 computeIfPresent() 和 computeIfAbsent() 取 代
"Map.get"和值判断
说明:
在向 map 中添加或改变值之前,判断 java.util.Map.get()的值是不是 null 是一种常见的模式。
但 java.util.Map API 提供一个更好的选择,使用 computeIfPresent()和 computeIfAbsent()方法。
不符合 sonarQube 规则的代码:
V value = map.get(key);
if (value == null) {
// Noncompliant
value = V.createFor(key);
if (value != null) {
map.put(key, value);
}
} return value;
符合 sonarQube 规则的代码:
return map.computeIfAbsent(key, k -> V.createFor(k));
36. 不抛出原生异常
说明:
不抛出原生异常,包括 Error, RuntimeException, Throwable, and Exception,抛出单独的异常
类型可以让调用代码时控制如何处理每个异常。此处应该抛出我们自定义的异常
BusinessException。
37. 谨慎注释掉代码
说明:
[代码整洁之道]
直接把代码注释掉是讨厌的做法。别这么干!
其他人不敢注释掉的代码。他们会想,代码依然放在那儿,一定有其原因,而且这段代码很
重要,不能删除。注释掉的代码堆积在一起,就像破酒瓶底的渣滓一般。
我们已经拥有优良的源代码控制系统如此之久,这些系统可以为我们记住不要的代码。我们
无需用注释来标记,删掉即可,它们丢不了。
[阿里手册]
谨慎注释掉代码。在上方详细说明,而不是简单地注释掉。如果无用,则删除。代码被注释
掉有两种可能性:
(1) 后续会恢复此段代码逻辑。
(2) 永久不用。前者如果没有备注信息,难以知晓注释动机。后者建议直接删掉 ( 代码
仓库保存了历史代码 ) 。
----------------- 新增条目 ------------------ 38. long 或者 Long 初始赋值时必须用大写的 L
说明:
[阿里手册] long 或者 Long 初始赋值时,必须使用大写的 L,不能是小写的 l,小写容易跟
数字 1 混淆,造成误解。
39. 在 if/else/for/while/do 语句中必须使用大括号
说明:
[阿里手册] 在 if/else/for/while/do 语句中必须使用大括号,即使只有一行代码,避免使用下
面的形式:if (condition) statements;
40. 使用常量或确定有值的对象来调用 equals
说明:
[阿里手册] Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用
equals。
41. 不要使用行尾注释
说明:
[阿里手册] 方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注
释使用/* */注释。注意与代码对齐。
42. 简化 stream API 调用链
说明:
stream API 调用链可以被简化,这样可以在遍历集合时避免创建重复的临时对象。
以下调用链可以被替代:
collection.stream().forEach() → collection.forEach()
collection.stream().collect(toList/toSet/toCollection()) → new CollectionType<>(collection)
collection.stream().toArray() → collection.toArray()
Arrays.asList().stream() → Arrays.stream() or Stream.of()
IntStream.range(0, array.length).mapToObj(idx -> array[idx]) → Arrays.stream(array)
IntStream.range(0, list.size()).mapToObj(idx -> list.get(idx)) → list.stream()
Collections.singleton().stream() → Stream.of()
Collections.emptyList().stream() → Stream.empty()
stream.filter().findFirst().isPresent() → stream.anyMatch()
stream.collect(counting()) → stream.count()
stream.collect(maxBy()) → stream.max()
stream.collect(mapping()) → stream.map().collect()
stream.collect(reducing()) → stream.reduce()
stream.collect(summingInt()) → stream.mapToInt().sum()
stream.mapToObj(x -> x) → stream.boxed()
stream.map(x -> {...; return x;}) → stream.peek(x -> ...)
!stream.anyMatch() → stream.noneMatch()
!stream.anyMatch(x -> !(...)) → stream.allMatch()
stream.map().anyMatch(Boolean::booleanValue) -> stream.anyMatch()
IntStream.range(expr1, expr2).mapToObj(x -> array[x]) -> Arrays.stream(array, expr1, expr2)
Collection.nCopies(count, ...) -> Stream.generate().limit(count)
stream.sorted(comparator).findFirst() -> Stream.min(comparator)
43. 类、类属性、类方法的注释必须使用 javadoc 规范
说明:
[阿里手册] 类、类属性、类方法的注释必须使用 javadoc 规范,使用/**内容*/格式,不得使
用//xxx 方式和/*xxx*/方式。 在 IDE 编辑窗口中,javadoc 方式会提示相关注释,生成 javadoc
可以正确输出相应注释;在 IDE 中,工程调用方法时,不进入方法即可悬浮提示方法、参数、
返回值的意义,提高阅读效率。
44. 集合初始化时指定集合初始值大小
说明:HashMap 使用 new HashMap(int initialCapacity)构造方法进行初始化,如果暂时无法确
定集合大小,那么指定默认值(16)即可。
45. sql.xml 配置参数使用#{}
说明:
[阿里手册] sql. xml 配置参数使用:#{},# param # 不要使用${} 此种方式容易出现 SQL 注
入。
46. SQL 模糊查询参数绑定考虑使用 bind 标签
说明:
模糊查询一般有三种方式:
1. Java 代码里拼接匹配符: 代码和 SQL 耦合度高;查看 xml 不能直接看出查询条件,降低开
发效率;有可能在 service 和 facade 层多次加%_ 2. SQL 里用 concat 拼接匹配符:增加数据库运算
3. 使用<bind>:Java 代码做连接,推荐使用
47. 使用 Optional 取代 null
修改前:
修改后:
48. 考虑使用不可变集合
修改前:
修改后:
说明:
不可变对象有很多优点,包括:
1.当对象被不可信的库调用时,不可变形式是安全的;
2.不可变对象被多个线程调用时,不存在竞态条件问题
3.不可变集合不需要考虑变化,因此可以节省时间和空间。所有不可变的集合都比它们的可
变形式有更好的内存利用率(分析和测试细节);
4.不可变对象因为有固定不变,可以作为常量来安全使用。
创建对象的不可变拷贝是一项很好的防御性编程技巧。Guava 为所有 JDK 标准集合类型和
Guava 新集合类型都提供了简单易用的不可变版本。
JDK 也提供了 Collections.unmodifiableXXX 方法把集合包装为不可变形式,但我们认为不够
好:
1.笨重而且累赘:不能舒适地用在所有想做防御性拷贝的场景;
2.不安全:要保证没人通过原集合的引用进行修改,返回的集合才是事实上不可变的;
3.低效:包装过的集合仍然保有可变集合的开销,比如并发修改的检查、散列表的额外空间,
等等。
如果你没有修改某个集合的需求,或者希望某个集合保持不变时,把它防御性地拷贝到
不可变集合是个很好的实践。
关于防御式编程,《代码大全》里“防御式编程”这一章有详细的介绍。防御式编程的
概念来自于防御式驾驶,在防御式驾驶中要建立这样一种思维,那就是你永远不能确定另一
位司机要做什么。这样才能确保其他人做出危险动作时你也不会受到伤害。对于不可变集合,
当作为参数传递到其他方法时,不用担心该集合会被改变,在当前方法可以放心继续使用该
集合。防御式编程的核心思想是承认程序都会有问题,都需要被修改。
参考:
https://github.com/google/guava/wiki/ImmutableCollectionsExplained
《代码大全》
49. 避免采用取反逻辑运算符
说明:
[阿里手册] 取反逻辑不利于快速理解,并且取反逻辑写法必然存在对应的正向逻辑写法。
50. 所有枚举类型字段必须要有注释
说明:
[阿里手册] 所有的枚举类型字段必须要有注释,说明每个数据项的用途
51. 遇到多个构造器参数时要考虑用构建器
项目中的例子:
说明:
[Effective Java] 如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder 模式
就是种不错的选择,特别是当大多数参数都是可选的时候。与使用传统的重叠构造器模式相
比,使用 Builder 模式的客户端代码将更易于阅读和编写,构建器也比 JavaBeans 更加安全。
参考:
Effective Java 第二版 第 2 条 遇到多个构造器参数时要考虑用构建器
--------- Effective Java Third Edition --------- 52. try-with-resources 优于 try-finally
说明:
在处理必须关闭的资源时,使用 try-with-resources 语句替代 try-finally 语句。 生成的代码更
简洁,更清晰,并且生成的异常更有用。try-with-resources 语句在编写必须关闭资源的代码
时会更容易,也不会出错,而使用 try-finally 可能会出错。
参考:
Effective Java 3
rd
Item 9: Prefer try-with-resources to try-finally
53. lambda 表达式优于匿名类
例子(非项目里的):
(可用方法引用继续优化)
说明:
从 Java8 开始,lambda 表达式是表示小函数对象的最佳方式。除非必须创建非函数式接
口类型的实例,否则不要使用匿名类作为函数对象。另外,lambda 表达式使表示小函数对
象变得如此容易,可以使用函数式编程。
lambda 表达式没有名称和文档; 如果计算不是显而易见的,或者超过几行,则不要将其
放入 lambda 表达式中。一行代码对于 lambda 说是理想的,三行代码是合理的最大值。如
果违反这一规定,可能会严重损害程序的可读性。
参考:
Effective Java 3
rd
Item 42: Prefer lambdas to anonymous classes
54. 方法引用优先于 Lambda 表达式
说明:
方法引用通常为 lambda 提供一个更简洁的选择。 如果方法引用看起来更简短更清晰,请
使用它们;否则,还是坚持 lambda
参考:
Effective Java 3
rd
Item 43: Prefer method references to lambdas
55. 优先使用标准的函数式接口
现在 Java 已经有了 lambda 表达式,因此必须考虑 lambda 表达式来设计你的 API。在输
入 上 接 受 函 数 式 接 口 类 型 并 在 输 出 中 返 回 它 们 。 一 般 来 说 , 最 好 使 用
java.util.function.Function 中提供的标准接口,但请注意,在相对罕见的情况下需要编写自己
的函数式接口。
在 java.util.Function 中有 43 个接口。不能指望全部记住它们,但是如果记住了六个基本
接口,就可以在需要它们时派生出其余的接口。六种基本函数式接口概述如下:
接口 方法 示例
UnaryOperator T apply(T t) String::toLowerCase
BinaryOperator T apply(T t1, T t2) BigInteger::add
Predicate boolean test(T t) Collection::isEmpty
Function<T,R> R apply(T t) Arrays::asList
Supplier T get() Instant::now
Consumer void accept(T t) System.out::println
参考:
Effective Java 3
rd
Item 44: Favor the use of standard functional interfaces
56. 谨慎地使用 streams
Stream API 具有足够的通用性,实际上任何计算都可以使用 Stream 执行,但仅仅是可
以,并不意味着应该这样做。如果使用得当,流可以使程序更短更清晰;如果使用不当,过
度使用流会使程序难以阅读且难以维护。过度使用流使程序难于阅读和维护。
有些任务最好使用流来完成,有些任务最好使用迭代来完成。将这两种方法结合起来,
可以最好地完成许多任务。对于选择使用哪种方法进行任务,没有硬性规定,但是有一些有
用的启发式方法。在许多情况下,使用哪种方法将是清楚的;在某些情况下,则不会很清楚。
如果不确定一个任务是通过流还是迭代更好地完成,那么尝试这两种方法,看看哪一种效果
更好。
参考:
Effective Java 3
rd
Item 45: Use streams judiciously
57. 优先考虑流中无副作用的函数
管道流编程的本质是无副作用的函数对象。这适用于传递给流和相关对象的所有许多函
数对象。终结操作 forEach 仅应用于报告流执行的计算结果,而不是用于执行计算。 为了
正确使用流,必须了解收集器。最重要的收集器工厂是 toList,toSet,toMap,groupingBy
和 join。
参考:
Effective Java 3
rd
Item 46: Prefer side-effect-free functions in streams
58. 优先使用集合而不是流作为返回类型
在编写返回元素序列的方法时,请记住,某些用户可能希望将它们作为流处理,而其他
用户可能希望以迭代的方式来处理。尽量适应这两类人。如果返回集合是可行的,请执行此
操作。Collection 接口是 Iterable 的子类型,并且具有 stream 方法,因此它提供迭代和流访
问。因此,Collection 或适当的子类型通常是公共序列返回方法的最佳返回类型。如果返回
的元素是基本类型或有严格的性能要求,则使用数组,数组还使用 Arrays.asList 和 Stream.of
方法提供简单的迭代和流访问。如果返回集合是不可行的,则返回流或可迭代的,无论哪个
看起来更自然。
参考:
Effective Java 3
rd
Item 47: Prefer Collection to Stream as a return type
59. 谨慎使用并行流
通常,并行性带来的性能优势在 ArrayList、HashMap、HashSet 和 ConcurrentHashMap
实例、数组、int 类型范围和 long 类型的范围的流上最好。这些数据结构的共同之处在于,
它们都可以精确而廉价地分割成任意大小的子程序,这使得在并行线程之间划分工作变得很
容易。
并行化一个流不仅会导致糟糕的性能,包括活性失败,还会导致不正确的结果和不可预
知的行为(安全故障)。使用映射器(mappers),过滤器(filters)和其他程序员提供的不符
合其规范的功能对象的管道并行化可能会导致安全故障。
总之,甚至不要尝试并行化管道流,除非你有充分的理由相信它将保持计算的正确性并
提高其速度。不恰当地并行化流的代价可能是程序失败或性能灾难。如果您认为并行性是合
理的,那么请确保您的代码在并行运行时保持正确,并在实际情况下进行仔细的性能度量。
如果您的代码是正确的,并且这些实验证实了您对性能提高的怀疑,那么只有这样才能在生
产环境代码中并行化流。
参考:
Effective Java 3
rd
Item 48: Use caution when making streams parallel
60. 谨慎地返回 optionals
如果你发现在写一个不会总是返回一个值的方法,并且你认为重要的是,使用该方法的
人每次调用该方法时都要考虑到这种可能性,那么你就应该返回一个 Optional。但是,你应
该意识到返回 Optional 会影响性能,Optional 是一个需要被分配空间和初始化的对象,并且
从 Optional 中读取值需要额外的间接操作,所以对于性能敏感的方法返回 Optional 是不合适
的,最好是返回 null 或者抛出异常。最后,除了作为返回值以外,几乎不应该以任何其他方
式使用 Optional。
参考:
Effective Java 3
rd
Item 55: Return optionals judiciously
原文地址:https://www.cnblogs.com/weixiaotao/p/10383639.html