阿里Java开发规范手册学习记录(待更)

版权声明:发扬开源精神,欢迎大家转载对自己有用的文章(●'◡'●) https://blog.csdn.net/jacksonary/article/details/82289403

阿里Java代码规范阅读记录

这里只记录一些个人认为需要注意或经常忽略的点,并对部分点进行重点分析。

1. 编程规约

1.1 命名风格

  • 代码命名的开头与结尾都不能是下划线或美元符号;
  • 禁止中英文混合命名或中文命名,同时应避免中文拼音命名(除非国际公认,如alibaba);
  • 常量名全部大写,单词之间用下划线隔开,尽量语义完整(不要嫌名字长),如MAX _ STOCK _ COUNT而不能写成MAX _ COUNT
  • 【重要】:抽象类命名使用AbstractBase开头;异常类命名使用Exception结尾;测试类
    命名以它要测试的类的名称开始,以Test结尾;
  • 【重要】:POJO(Plain Ordinary Java Objec,其实就是JavaBean)类中布尔类型的变量,都不要加is前缀 ,否则部分框架解析会引起序列化错误,比如定义基本数据类型Boolean isDeleted的属性,那么该对象自动生成该属性的对应getter方法就是isDeleted(),RPC(Remote Procedure Call Protocol远程过程调用协议)框架框架在反向解析的时候,误以为对应的isDeleted属性名称是deleted,导致属性获取不到,进而抛出异常;
  • 【重要】:包名统一小写,并且使用
    单数形式,但是类名如果有复数含义,类名可以使用复数形式,比如应用工具类包名为com.alibaba.ai.util、类名为MessageUtils
  • 为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词
    组合来表达其意,比如int a这样的形式要避免出现;
  • 【重要】:如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式,比如public class OrderFactorypublic class LoginProxypublic class ResourceObserver等;
  • 【重要】:接口类中的方法和属性不要加任何修饰符号(public也不要加),保持代码的简洁性,并加上有效的Javadoc注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是与接口方法相关,并且是整个应用的基础常量;
  • 枚举类名建议带上Enum后缀,枚举成员名称需要全大写,单词间用下划线隔开;
  • 对于分层过程中的一些命名注意点:
    • Service/DAO层方法命名规约:
      • 获取单个对象的方法用get做前缀;
      • 【重要】:获取多个对象的方法用list做前缀,复数形式结尾如:listObjects
      • 获取统计值的方法用count做前缀;
      • 插入的方法用save/insert做前缀;
      • 删除的方法用remove/delete做前缀;
      • 修改的方法用update做前缀;
    • 领域模型命名规约:
      • 数据对象:xxxDOxxx即为数据表名;
      • 数据传输对象:xxxDTOxxx为业务领域相关的名称;
      • 展示对象:xxxVOxxx一般为网页名称;
      • POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO

1.2 常量定义

  • 【重要】:在对longLong赋值时,数值后面使用L进行标识,不要使用l标识,小写容易跟数字1混淆,造成误解;
  • 不使用仅有的一个常量类维护所有常量,要按常量功能进行归类,分开便于维护和查找,比如缓存相关常量放在类 CacheConsts下,系统配置相关常量放在类ConfigConsts下;

1.3 代码格式

  • 【重要】:左小括号和字符(左小括号的左侧字符)间不要出现空格,而左大括号前需要空格,比如main(String[] args) {
  • 【重要】:if/for/while/switch/do等保留字与括号之间都必须加空格,比如if (1 == 1)
  • 【重要】:任何二目、三目运算符(包括赋值运算符=、逻辑运算符&&、加减乘除符号等)的左右两边都需要加一个空格,比如if (1 == 1)
  • 采用 4 个空格缩进,禁止使用tab字符,如果使用tab缩进,必须设置1个tab为4个空格。IDEA设置tab为4个空格时,请勿勾选Use tab character;
  • 注释的双斜线与注释内容之间有且仅有一个空格;
  • 单行字符数限制不超过120个,超出需要换行,换行时遵循如下原则:
    • 第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进(与第二行保持一致);
    • 运算符与下文一起换行;
    • 方法调用的点符号与下文一起换行;
    • 方法调用中的多个参数需要换行时,在逗号后进行;
    • 在括号前(可以理解为(前换行)不要换行;
  • 【重要】:IDE的编码统一设置为UTF-8,文件的换行符使用Unix格式,不要使用Windows格式;
  • 单个方法的总行数不超过80行,包括方法签名、结束右大括号、方法内代码、注释、空行、回车及任何不可见字符;
  • 不同逻辑、不同语义、不同业务的代码之间插入一个空行分隔开来以提升可读性,任何情形,没有必要插入多个空行进行隔开;

1.4 OOP规约

  • 避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可;
  • 【重要】:所有的覆写方法,必须加@Override注解(我经常性的不写(●ˇ∀ˇ●));
  • 相同参数类型,相同业务含义,才可以使用Java的可变参数(即f(int... a)这样的形式,这个方法可以传入多个int类型的参数,不是固定死的,只能传几个参数,注意和数组的区别,这两货无法重载,奇葩啊,然后重载方法时,固定参数的方法比可变参数的优先等级高),避免使用Object,同时提倡不使用可变参数变成;
  • 【重要】:外部正在调用或者二方库依赖的接口,所以不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加@Deprecated注解,并清晰地说明采用的新接口或者新服务是什么,(因为接口可能被其他人调用了,所以能想改就改,只能丢弃,不能修改!);
  • 不使用过时的类或方法;
  • Objectequals方法容易抛空指针异,应使用常量或确定有值的对象来调用equals,推荐使用Objects.equals(str1, str2),这样可以避免空指针异常;
  • 【重要!!!】:所有的相同类型的包装类对象之间值的比较,全部使用equals方法比较,对于Integer var = ?在-128至127范围内的赋值,Integer对象是在
    IntegerCache.cache产生,会复用已有对象,这个区间内的Integer值可以直接使用==进行
    判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,
    推荐使用equals方法进行判断。长见识了U•ェ•*U,具体测试
  • 基本数据类型和包装类的使用标准:

    • 【重要】:所有的POJO类属性必须使用包装数据类型(数据库的查询结果可能是null,因为自动拆箱,用基本数据类型接收有NPE风险,即空指针异常,下同);
    • RPC方法的返回值和参数必须使用包装数据类型;
    • 所有的局部变量使用基本数据类型;
  • 定义DO/DTO/VO等POJO类时,不要设定任何属性默认值,因为属性在数据提取时可能并没有置入具体值,在更新其它字段时又附带更新了此字段;

  • 序列化类新增属性时,请不要修改serialVersionUID字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么修改serialVersionUID值,serialVersionUID不一致会抛出序列化运行时异常;
  • 构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,放在init方法中;
  • POJO类必须写toString方法,如果继承了另一个POJO类,注意在前面加一下super.toString,主要是为了便于排查问题;
  • 禁止在POJO类中,同时存在对应属性xxxisXxx()getXxx()方法;
  • 类内方法定义的顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter方法;
  • 慎用Objectclone方法来拷贝对象,对象的clone方法默认是浅拷贝,若想实现深拷贝需要重写 clone方法实现域对象的深度遍历式拷贝;
  • 类成员与方法的访问控制:
    • 如果不允许外部直接通过new来创建对象,那么构造方法必须是private
    • 工具类不允许有publicdefault构造方法;
    • 类非static成员变量并且与子类共享,必须是protected
    • 类非static成员变量并且仅在本类使用,必须是private
    • static成员变量如果仅在本类使用,必须是private
    • 若是static成员变量,考虑是否为final
    • 类成员方法只供类内部调用,必须是private
    • 类成员方法只对继承类公开,那么限制为protected

1.5 集合处理

  • 关于hashCodeequals的处理:
    • 【重要】:只要重写equals,就必须重写hashCode
    • 【重要】:Set存储的是不重复的对象,依据hashCodeequals进行判断,所以Set存储的对象必须重写这两个方法;
    • 果自定义对象作为Map的键,那么必须重写hashCodeequals
  • 使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UnsupportedOperationException异常(【重要】:asList的返回对象是一个 Arrays内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组);
  • 泛型通配符<? extends T >来接收返回的数据,此写法的泛型集合不能使用add方法,而<? super T>不能使用get方法,作为接口调用赋值时易出错。PECS(Producer Extends Consumer Super) 原则:第一、频繁往外读取内容的,适合用<? extends T >,第二、经常往里插入的,适合用<? super T>
  • 不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁;
  • 在JDK7版本及以上,Comparator实现类要满足如下三个条件,不然Arrays.sortCollections.sort 会报IllegalArgumentException异常,三个条件:
    • x , y 的比较结果和 y , x 的比较结果相反;
    • x > y , y > z ,则 x > z;
    • x = y ,则 x , z 比较结果和 y , z 比较结果相同;
  • 集合泛型定义时,在JDK7及以上,使用diamond语法(即菱形泛型,使用<>来指代前边已经指定的类型)或全省略,比如:List<String> list1 = new ArrayList<>();List<String> list2 = new ArrayList();
  • 集合初始化时,直接指定集合初始值大小,如Map map = new HashMap(20);HashMap(int initialCapacity)初始化时,initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即loaderfactor)默认为0.75, 如果暂时无法确定初始值大小,请设置为16(即默认值),如果一个HashMap需要放置 1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容量7次被迫扩大, resize需要重建hash表,严重影响性能,具体扩容参看HashMap的源码;
  • 【重要】:使用entrySet遍历Map类集合KV,而不是keySet方式进行遍历,keySet其实是遍历了2次,一次是转为Iterator对象,另一次是从hashMap中取出key所对应的value。而entrySet只是遍历了一次就把keyvalue都放到了entry中,效率更高。如果是JDK 8,使用Map.foreach方法;
  • 理利用好集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响,有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次序是一定的。如: ArrayListorder/unsortHashMapunorder/unsortTreeSetorder/sort
  • 利用Set元素唯一的特性,可以快速对一个集合进行去重操作,避免使用Listcontains方法进行遍历、对比、去重操作;

1.6 并发处理

  • 获取单例对象需要保证线程安全,其中的方法也要保证安全(资源驱动类、工具类、单例工厂类等);
  • 【重要】:线程资源必须通过线程池提供,不允许在应用中显式创建线程(使用线程池可以减少创建和销毁线程池时资源消耗,解决资源不足的问题,如果不使用线程池,有可能造成系统创建大量同类线程而消耗内存或“过度切换”的问题);
  • 【重要】:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样可以让写的同学更加明确线程池的运行规则,规避资源消耗的风险,说明
  • SimpleDateFormat是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用 DateUtils工具类,说明
  • 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁;
  • 多线程加锁的顺序必须一致,否则很容易出现死锁,比如线程1对A、B、C依次加锁,那么线程2也必须是A、B、C,否则容易出现死锁;
  • 并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用version(也可以使用时间戳)作为更新依据,如果每次访问冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3次;
  • 多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用ScheduledExecutorService则没有这个问题;
  • 使用CountDownLatch进行异步转同步操作,每个线程退出前必须调用countDown方法(创建的时候new CountDownLatch(int x),每次调用countDown方法则x减一,在调用countDownLatch.await()方法时,将会等待所有调用countDown方法的线程才会继续执行),线程执行代码注意catch异常,确保countDown方法被执行到,避免主线程无法执行至await方法,直到超时才返回结果。
  • volatile解决多线程内存不可见问题。对于一写多读的场景,是可以解决变量同步问题,但是如果多写的场景,同样无法解决线程安全问题,说明
  • HashMap在容量不够进行resize时由于高并发可能出现死链,导致CPU飙升,在开发过程中可以使用其它数据结构或加锁来规避此风险;
  • ThreadLocal无法解决共享对象的更新问题,ThreadLocal对象建议使用static修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量;

1.7 控制语句

  • 在一个switch块内,每个case要么通过break/return等来终止,要么注释说明程序将继续执行到哪一个 case为止;在一个switch块内,都必须包含一个default语句并且放在最后,即使空代码;
  • 【重要】:在高并发场景中,避免使用“等于”判断作为中断或退出的条件,如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替;
  • 表达异常的分支时,少用if-else方式,这种方式的可以做一些优化
  • 除常用方法(如getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性,说明
  • 避免采用取反逻辑运算符(取反不易逻辑的理解);
  • 下面这些方法通常需要进行参数校验:
    • 调用频次低的方法;
    • 执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退,或者错误,那得不偿失;
    • 需要极高稳定性和可用性的方法;
    • 对外提供的开放接口,不管是RPC/API/HTTP接口;
    • 敏感权限入口;
  • 下面这些方法通常需要不需要进行参数校验:
    • 极有可能被循环调用的方法。但在方法说明里必须注明外部参数检查要求;
    • 底层调用频度比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。一般DAO层与Service层都在同一个应用中,部署在同一台服务器中,所以DAO的参数校验,可以省略;
    • 被声明成private只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数;

1.8 注释规约

  • 所有的抽象方法(包括接口中的方法)必须要用Javadoc注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能;
  • 所有的枚举类型字段必须要有注释,说明每个数据项的用途;

1.9 其他

  • 后台输送给页面的变量必须加$!{var}(必须加中间的感叹号,如果var等于null或者不存在,那么${var}会直接显示在页面上,而加了!就不会);
  • 注意Math.random()这个方法返回是double类型,注意取值的范围0≤x<1(能够取到零值,注意除零异常 ),如果想获取整数类型的随机数,不要将x放大10的若干倍然后取整,直接使用Random对象的nextInt或者nextLong方法;
  • 获取当前毫秒数System.currentTimeMillis(); 而不是new Date().getTime();,如果想获取更加精确的纳秒级时间值,使用System.nanoTime(),在JDK8中,针对统计时间等场景,推荐使用Instant类;
  • 任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存;
  • 及时清理不再使用的代码段或配置信息,对于暂时被注释掉,后续可能恢复使用的代码片断,在注释代码上方,统一规定使用三个斜杠(///)来说明注释掉代码的理由;

【问题】

问题1:在foreach循环里进行元素的remove/add操作时,给出的案例如下:

// 正例
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    if ("1".equals(item)) {
        iterator.remove();
    }
}

// 反例
for (String item : list) {
    if ("1".equals(item)) {
        list.remove(item);
    }
}

确实没有看出区别,作者说

以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的
结果吗?

问题2:在控制语句那一块,有一条规约为:

在高并发场景中,避免使用“等于”判断作为中断或退出的条件,如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替;

使用==可能被击穿可以理解,为什么用区间就不会出现被击穿?

【分析】
#### A1 包装类测试

测试代码:

Integer integer1 = 128;
Integer integer2 = 128;
System.out.println(integer1==integer2);
System.out.println(integer1.equals(integer2));

结果是falsetrue,长见识了……

A2 使用Executors返回线程池的弊端

  • FixedThreadPoolSingleThreadPool
    • 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM(内存溢出异常);
  • CachedThreadPoolScheduledThreadPool
    • 允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM;

A3 线程不安全的SimpleDateFormat的处理

一般推荐如下处理:

private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
    @ Override
    protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd");
    }
};

JDK8中可以使用Instant代替DateLocalDateTime代替CalendarDateTimeFormatter代替 SimpleDateFormat

A4 变量同步自加操作的处理

 推荐使用原子类解决变量在多写场景下的线程安全问题,以count++操作为例,主要实现如下:

AtomicInteger count = new AtomicInteger();
count.addAndGet(1);

JDK8,推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)。

A5 if-else语句的优化

 正常的if-else语句可以改写为下面的这种形式:

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

这样做的目的是便于维护,如果非得使用if()...else if()...else...方式表达逻辑,避免后续代码维护困难,禁止超过3层;超过3层的if-else的逻辑判断代码可以使用卫语句(可能是因为圈复杂度)、策略模式、状态模式等来实现,其中卫语句示例如下:

public void today() {
    if (isBusy()) {
        System.out.println(“change time.”);
        return;
    }
    if (isFree()) {
        System.out.println(“go to travel.”);
        return;
    }
    System.out.println(“stay at home to learn Alibaba Java Coding Guidelines.”);
    return;
}

A6 条件判断的优化

if判断中跟太多逻辑判断,影响代码阅读,不如将复杂的条件抽离出来,给一个判断的结果传到if语句中,比如:

// 正确的写法
final boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed) {
    ...
}

// 错误的写法
if ((file.open(fileName, "w") != null) && (...) || (...)) {
    ...
}

猜你喜欢

转载自blog.csdn.net/jacksonary/article/details/82289403