Java编码规范相关-开发手册-IDEA插件-P3C-sonarLint-代码格式化

编码规范

一、大厂的规范手册

1、阿里巴巴《Java开发手册(嵩山版)》

广为流传的手册,我基本每个开发机都会放一本,用来做参考,想起来就看一下,反思自己之前的瑕疵,完善和修正自己的编码习惯。
而且,阿里之前推出了IDEA的P3C扫描插件,可以实时扫描类中的规范问题,作为入门和参考是很不错的选择,而且文档的用色和排版也很工整,看着很舒服。

2、华为《代码规范文档》

华为内部使用,但是华为对于信息管控很严格,所以对于流出的手册资料,时效性不及时。
但是,规范属于“前人栽树,后人乘凉。“,所以作为参考即可。
而且,华为内部开发有很多自研和集成的扫描插件,可以检测和扫描很多规范和建议类内容。

3、唯品会《唯品会Java开发手册》

https://github.com/DarLiner/vjtools
基于阿里开发手册等,由唯品会在github VJTools开源的一本手册
简略参考:https://mp.weixin.qq.com/s/cdmzMmE9j31P1Bak0PyXTg

4、wiki在线Java编码规范

https://wiki.sei.cmu.edu/confluence/display/java/SEI+CERT+Oracle+Coding+Standard+for+Java
SEI CERT Oracle Coding Standard for Java
一个在Wiki上在线编译的Java规范

二、参考书籍

1、《Clean Code》

2、《Effective Java》

三、IDEA插件

1、阿里编码规范实时监测工具

alibaba-java-coding-guidelines
插件下载即用,可以扩展

2、findbugs、checkstyle等

常见的几种规范插件

3、sonarLint

1. 最早是简单的checkstyle,后来有同样基于文本分析的PMD

2. 再后来又有了基于字节码分析的FindBugs

3. 再后来出现了Sonar,集成了三者

4. 又后来,Facebook基于Findbugs推出自己实战派的规则FB-Contrib,再次被Sonar集成

5. 最后,Sonar发展了自己的语法分析体系,改进并替换了Checkstyle 和 PMD的规则

四、IDEA代码格式化工具

1、eclipse code formatter

导入阿里的P3C代码格式化工具,并且支持自定义
https://github.com/alibaba/p3c
唯品会格式化模板
https://github.com/vipshop/vjtools

2、JavadDoc

可以用来生成JavaDoc注释

3、JUnitGenerator

生成单元测试代码

(一) 命名规约

Rule 1. 【强制】避免成员变量,方法参数,局部变量重名复写,避免引起混淆

  • 类的私有成员变量名,不与父类的成员变量重名

  • 方法的参数名/局部变量名,不与类的成员变量重名(getter/setter例外)

下面错误的地方,在编译时都是合法的,但给阅读者带来极大的障碍。

public class A {
  int foo;
}

public class B extends A {
  int foo; //WRONG
  int bar;

  public void hello(int bar) { //WRONG
    int foo = 0; //WRONG
  }

  public void setBar(int bar) { //OK
    this.bar = bar;
  }
}

(二) 格式规约

Rule 1. 【强制】使用项目组统一的代码格式模板,基于IDE自动的格式化
基于阿里P3C插件,也可以安装阿里的模板,建议统一
10)IDE的默认代码格式模板,能简化绝大部分关于格式规范(如空格,括号)的描述。

2)统一的模板避免不同开发者之间,因为格式不统一,产生代码合并冲突。另外,代码变更日志中因为格式不同引起的变更,也会掩盖了真正的逻辑变更。

3)设定项目组统一的行宽,建议120。

4)设定项目组统一的缩进方式(Tab或二空格,四空格均可),基于IDE自动转换。


Rule 2. 【推荐】通过空行进行逻辑分段

一段代码也是一段文章,需要合理的分段。

不同组的变量之间,不同业务逻辑的代码行之间,插入一个空行,起逻辑分段的作用。

而联系紧密的变量之间、语句之间,则尽量不要插入空行。

int width; 
int height; 

String name;

Rule 3.【推荐】避免IDE格式化

对于一些特殊场景(如使用大量的字符串拼接成一段文字,或者想把大量的枚举值排成一列),为了避免IDE自动格式化,常常会把注释符号//加在每一行的末尾,这会导致很多无意义的视觉干扰。

可以使用@formatter:off和@formatter:on来包装这段不需要格式化的代码,让IDE就会跳过这段的格式化。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YNJfoIh9-1624848159255)(C:\Users\86134\AppData\Roaming\Typora\typora-user-images\image-20210624094409061.png)]

// @formatter:off
...
// @formatter:on

(三) 注释规约

Rule 1.【推荐】基本的注释要求

代码将被大量后续维护,注释如果对阅读者有帮助,不要吝啬在注释上花费的时间。

除了特别简单特别清晰的类(见规则2,3),每个类都尽量编写注释,说明类的目的和使用方法。

对外提供的公有方法,同样需要清晰的描述,期待的输入,对应的输出,错误的处理和返回码,尽量把可能的异常都一一列明。


Rule 2. 【推荐】通过更清晰的代码来避免注释

在编写注释前,考虑是否可以通过更好的命名,更清晰的代码结构,更好的函数和变量的抽取,让代码不言自明,此时不需要额外的注释。


Rule 3. 【推荐】删除空注释,无意义注释

《Clean Code》建议,如果没有想说的,不要留着IDE自动生成的,空的@param,@return,@throws 标记,让代码更简洁。

反例:方法名为put,加上两个有意义的变量名elephant和fridge,已经说明了这是在干什么,不需要任何额外的注释。

/**
 * put elephant into fridge.
 * 
 * @param elephant
 * @param fridge
 * @return 
 */
public void put(Elephant elephant, Fridge fridge);

Rule 4.【推荐】避免创建人,创建日期,及更新日志的注释

代码后续还会有多人多次维护,让我们相信源码版本控制系统能做得更好。


Rule 5. 【强制】代码修改的同时,注释也要进行相应的修改。尤其是参数、返回值、异常、核心逻辑等的修改


Rule 6. 【强制】类、类成员变量、类方法的注释必须使用Javadoc规范,使用/**xxx*/格式,不得使用//xxx方式

正确的JavaDoc格式可以用在很多地方,比如在IDE中,查看调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。


Rule 7. 【推荐】JavaDoc中不要为了HTML格式化而大量使用HTML标签和转义字符

如果为了HTML版JavaDoc的显示,大量使用<p\> <pre\>这样的html标签,以及&lt &quot 这样的html转义字符,对严重影响直接阅读代码时的直观性,而直接阅读代码的几率其实比看Html版JavaDoc大得多。

有时候Java Doc的格式化也要求<p>之类的标签来换行,可以配置让IDE不进行Java Doc的自动格式化。


Rule 8. 【推荐】TODO标记,清晰说明代办事项和处理人

清晰描述待修改的事项,保证过几个月后仍然能够清楚要做什么修改。

如果近期会处理的事项,写明处理人。

通过标记扫描,经常清理此类标记,线上故障经常来源于这些标记但未处理的代码。

正例:
//TODO:calvin use xxx to replace yyy.

反例:
//TODO: refactor it

Rule 9. 【推荐】合理处理注释掉的代码

如果后续会恢复此段代码,在目标代码上方用///注释详细说明,而不是简单的注释掉。

如果无用,则直接删除(版本管理工具保存了历史代码)。

(四) 方法设计

Rule 1. 【推荐】方法的长度度量

方法尽量不要超过100行,或其他团队共同商定的行数。华为是50,但是对于大一些的业务和注解较多的情况,感觉80较为合理

另外,方法长度超过8000个字节码时,将不会被JIT编译成二进制码。


Rule 2. 【推荐】方法的语句在同一个抽象层级上

反例:一个方法里,前20行代码在进行很复杂的基本价格计算,然后调用一个折扣计算函数,再调用一个赠品计算函数。

此时可将前20行也封装成一个价格计算函数,使整个方法在同一抽象层级上。


Rule 3. 【推荐】为了帮助阅读及方法内联,将小概率发生的异常处理及其他极小概率进入的代码路径,封装成独立的方法

if(seldomHappenCase) {
  hanldMethod();
}

try {
  ...
} catch(SeldomHappenException e) {
  handleException();
}

Rule 4. 【推荐】尽量减少重复的代码,抽取方法

超过5行以上重复的代码,都可以考虑抽取公用的方法。


Rule 5. 【推荐】方法参数最好不超过3个,最多不超过7个

1)如果多个参数同属于一个对象,直接传递对象。

例外: 你不希望依赖整个对象,传播了类之间的依赖性。

2)将多个参数合并为一个新创建的逻辑对象。

例外: 多个参数之间毫无逻辑关联。

3)将函数拆分成多个函数,让每个函数所需的参数减少。


Rule 6.【推荐】下列情形,需要进行参数校验

1) 调用频次低的方法。

2) 执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退,或者错误,代价更大。

3) 需要极高稳定性和可用性的方法。

4) 对外提供的开放接口,不管是RPC/HTTP/公共类库的API接口。

如果使用Apache Validate 或 Guava Precondition进行校验,并附加错误提示信息时,注意不要每次校验都做一次字符串拼接。

//WRONG
Validate.isTrue(length > 2, "length is "+keys.length+", less than 2", length);
//RIGHT
Validate.isTrue(length > 2, "length is %d, less than 2", length);

Rule 7.【推荐】下列情形,不需要进行参数校验

1) 极有可能被循环调用的方法。

2) 底层调用频度比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。

比如,一般DAO层与Service层都在同一个应用中,所以DAO层的参数校验,可以省略。

3) 被声明成private,或其他只会被自己代码所调用的方法,如果能够确定在调用方已经做过检查,或者肯定不会有问题则可省略。

即使忽略检查,也尽量在方法说明里注明参数的要求,比如vjkit中的@NotNull,@Nullable标识。


Rule 8.【推荐】禁用assert做参数校验

看到代码中比较多,可以商议

assert断言仅用于测试环境调试,无需在生产环境时进行的校验。因为它需要增加-ea启动参数才会被执行。而且校验失败会抛出一个AssertionError(属于Error,需要捕获Throwable)

因此在生产环境进行的校验,需要使用Apache Commons Lang的Validate或Guava的Precondition。


Rule 9.【推荐】返回值可以为Null,可以考虑使用JDK8的Optional类

不强制返回空集合,或者空对象。但需要添加注释充分说明什么情况下会返回null值。

本手册明确防止NPE是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回null的情况。

JDK8的Optional类的使用这里不展开。


Rule 10.【推荐】返回值可以为内部数组和集合

如果觉得被外部修改的可能性不大,或没有影响时,不强制在返回前包裹成Immutable集合,或进行数组克隆。


Rule 11.【推荐】不能使用有继承关系的参数类型来重载方法

因为方法重载的参数类型是根据编译时表面类型匹配的,不根据运行时的实际类型匹配。

class A {
  void hello(List list);
  void hello(ArrayList arrayList);
}

List arrayList = new ArrayList();

// 下句调用的是hello(List list),因为arrayList的定义类型是List
a.hello(arrayList);  

Rule 12.【强制】正被外部调用的接口,不允许修改方法签名,避免对接口的调用方产生影响

只能新增新接口,并对已过时接口加@Deprecated注解,并清晰地说明新接口是什么。


Rule 13.【推荐】不使用@Deprecated的类或方法

接口提供方既然明确是过时接口并提供新接口,那么作为调用方来说,有义务去考证过时方法的新实现是什么。

比如java.net.URLDecoder 中的方法decode(String encodeStr) 这个方法已经过时,应该使用双参数decode(String source, String encode)。


Rule 14.【推荐】不使用不稳定方法,如com.sun.*包下的类,底层类库中internal包下的类

com.sun.*sun.*包下的类,或者底层类库中名称为internal的包下的类,都是不对外暴露的,可随时被改变的不稳定类。

(五) 类设计

Rule 1.【推荐】 减少类之间的依赖

比如如果A类只依赖B类的某个属性,在构造函数和方法参数中,只传入该属性。让阅读者知道,A类只依赖了B类的这个属性,而不依赖其他属性,也不会调用B类的任何方法。

a.foo(b);     //WRONG

a.foo(b.bar); //RIGHT

Rule 2. 【推荐】类的长度度量

类尽量不要超过300行,或其他团队共同商定的行数。华为是2000,可以商议

对过大的类进行分拆时,可考虑其内聚性,即类的属性与类的方法的关联程度,如果有些属性没有被大部分的方法使用,其内聚性是低的。


(六) 控制语句

Rule 1. 【强制】if, else, for, do, while语句必须使用大括号,即使只有单条语句

曾经试过合并代码时,因为没加括号,单条语句合并成两条语句后,仍然认为只有单条语句,另一条语句在循环外执行。

其他增加调试语句等情况也经常引起同样错误。

可在IDE的Save Action中配置自动添加。

if (a == b) {
  ...
}

例外:一般由IDE生成的equals()函数

Rule 2.【推荐】少用if-else方式,多用哨兵语句式以减少嵌套层次

if (condition) {
  ...
  return obj;
}

// 接着写else的业务逻辑代码;
  • Facebook-Contrib: Style - Method buries logic to the right (indented) more than it needs to be

Rule 3.【推荐】限定方法的嵌套层次

所有if/else/for/while/try的嵌套,当层次过多时,将引起巨大的阅读障碍,因此一般推荐嵌套层次不超过4。 华为是5,方法大小,类大小,嵌套深度和圈复杂度等代码复杂度分析也是一种很重要的手段

通过抽取方法,或哨兵语句(见Rule 2)来减少嵌套。

public void applyDriverLicense() {
  if (isTooYoung()) {
    System.out.println("You are too young to apply driver license.");
    return;
  }
    
  if (isTooOld()) {
    System.out.println("You are too old to apply driver license.");
    return;
  }
    
  System.out.println("You've applied the driver license successfully.");
  return;
}

Rule 4.【推荐】布尔表达式中的布尔运算符(&&,||)的个数不超过4个,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性

//WRONG
if ((file.open(fileName, "w") != null) && (...) || (...)|| (...)) {
  ...
}

//RIGHT
boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed || (...)) {
  ...
}

Rule 5.【推荐】简单逻辑,善用三元运算符,减少if-else语句的编写

s != null ? s : "";

Rule 6.【推荐】减少使用取反的逻辑

不使用取反的逻辑,有利于快速理解。且大部分情况,取反逻辑存在对应的正向逻辑写法。

//WRONG
if (!(x >= 268) { ... }

//RIGHT
if (x < 268) { ... }

Rule 7.【推荐】表达式中,能造成短路概率较大的逻辑尽量放前面,使得后面的判断可以免于执行

if (maybeTrue() || maybeFalse()) { ... }

if (maybeFalse() && maybeTrue()) { ... }

Rule 8.【强制】switch的规则

1)在一个switch块内,每个case要么通过break/return等来终止,要么注释说明程序将继续执行到哪一个case为止;

2)在一个switch块内,都必须包含一个default语句并且放在最后,即使它什么代码也没有。

String animal = "tomcat";

switch (animal) {
case "cat":
  System.out.println("It's a cat.");
  break;
case "lion": // 执行到tiger
case "tiger":
  System.out.println("It's a beast.");
  break;
default: 
  // 什么都不做,也要有default
  break;
}

Rule 9.【推荐】循环体中的语句要考量性能,操作尽量移至循环体外处理

1)不必要的耗时较大的对象构造;

2)不必要的try-catch(除非出错时需要循环下去)。


Rule 10.【推荐】能用while循环实现的代码,就不用do-while循环

while语句能在循环开始的时候就看到循环条件,便于帮助理解循环内的代码;

do-while语句要在循环最后才看到循环条件,不利于代码维护,代码逻辑容易出错。

(七) 字符串

Rule 1. 字符串拼接的原则

1.1 【推荐】 当字符串拼接不在一个命令行内写完,而是存在多次拼接时(比如循环),使用StringBuilder的append()

String s  = "hello" + str1 +  str2;  //Almost OK,除非初始长度有问题,见第3点.

String s  = "hello";  //WRONG
if (condition) {
  s += str1;
}

String str = "start";       //WRONG
for (int i = 0; i < 100; i++) {
  str = str + "hello";
}

反编译出的字节码文件显示,其实每条用+进行字符拼接的语句,都会new出一个StringBuilder对象,然后进行append操作,最后通过toString方法返回String对象。所以上面两个错误例子,会重复构造StringBuilder,重复toString()造成资源浪费。

1.2 【强制】 字符串拼接对象时,不要显式调用对象的toString()

如上,+实际是StringBuilder,本身会调用对象的toString(),且能很好的处理null的情况。

//WRONG
str = "result:" + myObject.toString();  // myObject为Null时,抛NPE

//RIGHT
str = "result:" + myObject;  // myObject为Null时,输出 result:null

1.3【强制】使用StringBuilder,而不是有所有方法都有同步修饰符的StringBuffer

因为内联不成功,逃逸分析并不能抹除StringBuffer上的同步修饰符

1.4 【推荐】当拼接后字符串的长度远大于16时,指定StringBuilder的大概长度,避免容量不足时的成倍扩展

1.5 【推荐】如果字符串长度很大且频繁拼接,可考虑ThreadLocal重用StringBuilder对象

参考BigDecimal的toString()实现,及vjkit中的StringBuilderHolder。


Rule 2. 【推荐】字符操作时,优先使用字符参数,而不是字符串,能提升性能

没用过,所以摘出来了

//WRONG
str.indexOf("e");

//RIGHT
stringBuilder.append('a'); 
str.indexOf('e');
str.replace('m','z');

其他包括split等方法,在JDK String中未提供针对字符参数的方法,可考虑使用Apache Commons StringUtils 或Guava的Splitter。



(八) 集合处理

Rule 1. 【推荐】底层数据结构是数组的集合,指定集合初始大小

底层数据结构为数组的集合包括 ArrayList,HashMap,HashSet,ArrayDequeue等。

数组有大小限制,当超过容量时,需要进行复制式扩容,新申请一个是原来容量150% or 200%的数组,将原来的内容复制过去,同时浪费了内存与性能。HashMap/HashSet的扩容,还需要所有键值对重新落位,消耗更大。

默认构造函数使用默认的数组大小,比如ArrayList默认大小为10,HashMap为16。因此建议使用ArrayList(int initialCapacity)等构造函数,明确初始化大小。

HashMap/HashSet的初始值还要考虑加载因子:

为了降低哈希冲突的概率(Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条需要遍历的Entry链),默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。vjkit的MapUtil的Map创建函数封装了该计算。

如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。


Rule 2. 【推荐】尽量使用新式的foreach语法遍历Collection与数组

foreach是语法糖,遍历集合的实际字节码等价于基于Iterator的循环。

foreach代码一来代码简洁,二来有效避免了有多个循环或嵌套循环时,因为不小心的复制粘贴,用错了iterator或循环计数器(i,j)的情况。


Rule 3. 【强制】不要在foreach循环里进行元素的remove/add操作,remove元素可使用Iterator方式

//WRONG
for (String str : list) {
  if (condition) {
    list.remove(str);
  }
}

//RIGHT
Iterator<String> it = list.iterator();
while (it.hasNext()) {
  String str = it.next();
  if (condition) {
    it.remove();
  }
} 
  • Facebook-Contrib: Correctness - Method modifies collection element while iterating
  • Facebook-Contrib: Correctness - Method deletes collection element while iterating

Rule 4. 【强制】使用entrySet遍历Map类集合Key/Value,而不是keySet 方式进行遍历

keySet遍历的方式,增加了N次用key获取value的查询。


Rule 5. 【强制】当对象用于集合时,下列情况需要重新实现hashCode()和 equals()

1) 以对象做为Map的KEY时;

2) 将对象存入Set时。

上述两种情况,都需要使用hashCode和equals比较对象,默认的实现会比较是否同一个对象(对象的引用相等)。

另外,对象放入集合后,会影响hashCode(),equals()结果的属性,将不允许修改。


Rule 6. 【强制】高度注意各种Map类集合Key/Value能不能存储null值的情况

MapKeyValue
HashMapNullableNullable
ConcurrentHashMapNotNullNotNull
TreeMapNotNullNullable

由于HashMap的干扰,很多人认为ConcurrentHashMap是可以置入null值。同理,Set中的value实际是Map中的key。


Rule 7. 【强制】长生命周期的集合,里面内容需要及时清理,避免内存泄漏

长生命周期集合包括下面情况,都要小心处理。

1) 静态属性定义;

2) 长生命周期对象的属性;

3) 保存在ThreadLocal中的集合。

如无法保证集合的大小是有限的,使用合适的缓存方案代替直接使用HashMap。

另外,如果使用WeakHashMap保存对象,当对象本身失效时,就不会因为它在集合中存在引用而阻止回收。但JDK的WeakHashMap并不支持并发版本,如果需要并发可使用Guava Cache的实现。


Rule 8. 【强制】集合如果存在并发修改的场景,需要使用线程安全的版本

  1. 著名的反例,HashMap扩容时,遇到并发修改可能造成100%CPU占用。

推荐使用java.util.concurrent(JUC)工具包中的并发版集合,如ConcurrentHashMap等,优于使用Collections.synchronizedXXX()系列函数进行同步化封装(等价于在每个方法都加上synchronized关键字)。

例外:ArrayList所对应的CopyOnWriteArrayList,每次更新时都会复制整个数组,只适合于读多写很少的场景。如果频繁写入,可能退化为使用Collections.synchronizedList(list)。

  1. 即使线程安全类仍然要注意函数的正确使用。

例如:即使用了ConcurrentHashMap,但直接是用get/put方法,仍然可能会多线程间互相覆盖。

//WRONG
E e = map.get(key);
if (e == null) {
  e = new E();
  map.put(key, e); //仍然能两条线程并发执行put,互相覆盖
}
return e;

//RIGHT 
E e = map.get(key);
if (e == null) {
  e = new E();
  E previous = map.putIfAbsent(key, e);
  if(previous != null) {
    return previous;
  }
}
return e;

Rule 9. 【推荐】正确使用集合泛型的通配符

List<String>并不是List<Object>的子类,如果希望泛型的集合能向上向下兼容转型,而不仅仅适配唯一类,则需定义通配符,可以按需要extends 和 super的字面意义,也可以遵循PECS(Producer Extends Consumer Super)原则:

  1. 如果集合要被读取,定义成<? extends T>
Class Stack<E>{
  public void pushAll(Iterable<? extends E> src){
    for (E e: src)
      push(e);
  }
}

Stack<Number> stack = new Stack<Number>();
Iterable<Integer> integers = ...;
stack.pushAll(integers);
  1. 如果集合要被写入,定义成<? super T>
Class Stack<E>{
  public void popAll(Collection<? super E> dist){
     while(!isEmpty())
   	   dist.add(pop);   
  }
}

Stack<Number> stack = new Stack<Number>();
Collection<Object> objects = ...;
stack.popAll(objects);

Rule 10. 【推荐】List, List<?>List<Object>的选择

定义成List,会被IDE提示需要定义泛型。 如果实在无法确定泛型,就仓促定义成List<?>来蒙混过关的话,该list只能读,不能增改。定义成List<Object>呢,如规则10所述,List<String> 并不是List<Object>的子类,除非函数定义使用了通配符。

因此实在无法明确其泛型时,使用List也是可以的。


Rule 11. 【推荐】如果Key只有有限的可选值,先将Key封装成Enum,并使用EnumMap

没用过,所以摘出来了

EnumMap,以Enum为Key的Map,内部存储结构为Object[enum.size],访问时以value = Object[enum.ordinal()]获取值,同时具备HashMap的清晰结构与数组的性能。

public enum COLOR {
  RED, GREEN, BLUE, ORANGE;
}
  
EnumMap<COLOR, String> moodMap = new EnumMap<COLOR, String> (COLOR.class);

Rule 12. 【推荐】Array 与 List互转的正确写法

// list -> array,构造数组时不需要设定大小
String[] array = (String[])list.toArray(); //WRONG;
String[] array = list.toArray(new String[0]); //RIGHT
String[] array = list.toArray(new String[list.size()]); //RIGHT,但list.size()可用0代替。


// array -> list
List list = Arrays.asList(array); //WRONG
List list = new ArrayList(array); //RIGHT

Arrays.asList(array),如果array是原始类型数组如int[],会把整个array当作List的一个元素,String[] 或 Foo[]则无此问题,安全起见统一不使用。

  • Facebook-Contrib: Correctness - Impossible downcast of toArray() result
  • Facebook-Contrib: Correctness - Method calls Array.asList on an array of primitive values

(九) 并发处理

Rule 1. 【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯

这个还是挺重要的,尤其是在自定义线程池中

1)创建单条线程时直接指定线程名称

Thread t = new Thread();
t.setName("cleanup-thread");

2) 线程池则使用guava或自行封装的ThreadFactory,指定命名规则。

//guava 或自行封装的ThreadFactory
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(threadNamePrefix + "-%d").build();

ThreadPoolExecutor executor = new ThreadPoolExecutor(..., threadFactory, ...);

(十) 异常处理

Rule 1. 【强制】创建异常的消耗大,只用在真正异常的场景

上份工作有点滥用异常捕获,归根结底是代码坏味道

构造异常时,需要获得整个调用栈,有一定消耗。

不要用来做流程控制,条件控制,因为异常的处理效率比条件判断低。

发生概率较高的条件,应该先进行检查规避,比如:IndexOutOfBoundsException,NullPointerException等,所以如果代码里捕获这些异常通常是个坏味道。

//WRONG
try { 
  return obj.method();
} catch (NullPointerException e) {
  return false;
}

//RIGHT
if (obj == null) {
  return false;
}     

Rule 2. 【推荐】异常日志应包含排查问题的足够信息

很重要,要打印出重要信息才能还原生产问题出现的场景,处理生产问题时,日志是很重要的参考手段

异常信息应包含排查问题时足够的上下文信息。

捕获异常并记录异常日志的地方,同样需要记录没有包含在异常信息中,而排查问题需要的信息,比如捕获处的上下文信息。

//WRONG
new TimeoutException("timeout");
logger.error(e.getMessage(), e);


//RIGHT
new TimeoutException("timeout:" + eclapsedTime + ", configuration:" + configTime);
logger.error("user[" + userId + "] expired:" + e.getMessage(), e);
  • Facebook-Contrib: Style - Method throws exception with static message string

Rule 3. 异常抛出的原则

3.1 【推荐】尽量使用JDK标准异常,项目标准异常

尽量使用JDK标准的Runtime异常如IllegalArgumentExceptionIllegalStateExceptionUnsupportedOperationException,项目定义的Exception如ServiceException

3.2 【推荐】根据调用者的需要来定义异常类,直接使用RuntimeException是允许的

是否定义独立的异常类,关键是调用者会如何处理这个异常,如果没有需要特别的处理,直接抛出RuntimeException也是允许的。


Rule 4. 异常捕获的原则

4.1 【推荐】按需要捕获异常,捕获ExceptionThrowable是允许的

如果无特殊处理逻辑,统一捕获Exception统一处理是允许的。

捕获Throwable是为了捕获Error类异常,包括其实无法处理的OOM StackOverflow ThreadDeath,以及类加载,反射时可能抛出的NoSuchMethodError NoClassDefFoundError等。

4.2【推荐】多个异常的处理逻辑一致时,使用JDK7的语法避免重复代码**

try {
  ...
} catch (AException | BException | CException ex) {
  handleException(ex);
}

Rule 5.异常处理的原则

5.1 【强制】捕获异常一定要处理;如果故意捕获并忽略异常,须要注释写明原因

方便后面的阅读者知道,此处不是漏了处理。

//WRONG
try {
} catch(Exception e) {
}

//RIGHT
try {
} catch(Exception ignoredExcetpion) {
	//continue the loop
}

5.2 【强制】异常处理不能吞掉原异常,要么在日志打印,要么在重新抛出的异常里包含原异常

 //WRONG
throw new MyException("message");

//RIGHT 记录日志后抛出新异常,向上次调用者屏蔽底层异常
logger.error("message", ex); 
throw new MyException("message"); 

//RIGHT 传递底层异常
throw new MyException("message", ex); 

5.3 【强制】如果不想处理异常,可以不进行捕获。但最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容


Rule 6. finally块的处理原则

因为finally语句块的特殊性,确实会出现问题,之前没注意

6.1 【强制】必须对资源对象、流对象进行关闭,或使用语法try-with-resource

关闭动作必需放在finally块,不能放在try块 或 catch块,这是经典的错误。

更加推荐直接使用JDK7的try-with-resource语法自动关闭Closeable的资源,无需在finally块处理,避免潜在问题。

try (Writer writer = ...) {
  writer.append(content);
}

6.2 【强制】如果处理过程中有抛出异常的可能,也要做try-catch,否则finally块中抛出的异常,将代替try块中抛出的异常

//WRONG
try {
  ...
  throw new TimeoutException();
} finally {
  file.close();//如果file.close()抛出IOException, 将代替TimeoutException
}

//RIGHT, 在finally块中try-catch
try {
  ...
  throw new TimeoutException();
} finally {
  IOUtil.closeQuietly(file); //该方法中对所有异常进行了捕获
}

6.3 【强制】不能在finally块中使用return,finally块中的return将代替try块中的return及throw Exception

//WRONG
try {
  ...
  return 1;
} finally {
  return 2; //实际return 2 而不是1
}
	
try {
  ...
  throw TimeoutException();
} finally {
  return 2; //实际return 2 而不是TimeoutException
}
  • [Sonar-1143: Jump statements should not occur in “finally” blocks](

(十一) 日志规约

Rule 1. 【强制】应用中不可直接使用日志库(Log4j、Logback)中的API,而应使用日志框架SLF4J中的API

使用门面模式的日志框架,有利于维护各个类的日志处理方式统一。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

private static Logger logger = LoggerFactory.getLogger(Foo.class);

Rule 6. 【强制】禁止配置日志框架输出日志打印处的类名,方法名及行号的信息

之前不知道,可以作为参考

日志框架在每次打印时,通过主动获得当前线程的StackTrace来获取上述信息的消耗非常大,尽量通过Logger名本身给出足够信息。

Rule 7. 【推荐】谨慎地记录日志,避免大量输出无效日志,信息不全的日志

是的,尤其是在公共类中,不要JSON工具类对整个对象进行打印,尤其是循环中,非常耗费性能

大量地输出无效日志,不利于系统性能,也不利于快速定位错误点。

记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?

(十二) 其他规约

Rule 1. 【参考】尽量不要让魔法值(即未经定义的数字或字符串常量)直接出现在代码中

 //WRONG
 String key = "Id#taobao_"+tradeId;
 cache.put(key, value);

例外:-1,0,1,2,3 不认为是魔法数

-1,0,1 作为正负数等判断或其他流程控制场景时,是否需要定义为常量,有待商榷

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

S. Dylan

站着恰饭不腰疼

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值