学习《阿里巴巴JAVA开发手册》之(一)编程规约

学习《阿里巴巴JAVA开发手册》系列

1、命名规约

(1)类名

  • 类名使用 UpperCamelCase 风格,但DO / BO / DTO / VO等领域模型相关的命名例外;
  • 正例:UserDO / XmlService / TcpUdpDeal / TaPromotion

(2)方法名、参数名、成员变量、局部变量

  • 方法名、参数名、成员变量、局部变量统一使用lowerCamelCase 风格;

(3)抽象类、异常类、测试类

  • 抽象类命名以Abstract 或 Base 开头;
  • 异常类命名使用 Exception 结尾;
  • 测试类命名以它要测试的类的名称开始,以 Test 结尾;

(4)POJO类中的布尔类型变量

  • POJO 类中布尔类型的变量,都不要加 is,否则部分框架解析会引起序列化错误;
  • 如:定义为基本数据类型boolean isSuccess;的属性,它的方法也是isSuccess(),RPC 框架在反向解析的时候,“以为”对应的属性名称是 success,导致属性获取不到,进而抛出异常;

(5)包名

  • 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词;
  • 包名统一使用单数形式(类名如果有复数含义,可以用复数形式);

(6)接口

  • 如果是形容能力的接口,取对应的形容词(通常是-able)命名;
  • 接口中的方法和属性不要加任何修饰符号(接口中的方法默认是public abstract,接口中的变量默认是public static final);

(7)枚举类

  • 枚举类命名建议加上ENUM后缀;
  • 枚举成员名称需要全部大写,单词间用下划线隔开;

(8)Service/DAO层方法

  • 获取单个对应的方法用get前缀;
  • 获取多个对象的方法用list前缀;
  • 获取统计值的方法用count前缀;
  • 插入方法用save/insert前缀;
  • 删除方法用remove/delete前缀;
  • 修改方法用update前缀;

(9)领域模型

  • 数据对象:xxxDO,其中xxx为数据表名;
  • 数据传输对象:xxxDTO,其中xxx为业务领域相关的名称;
  • 展示对象:xxxVO,其中xxx一般为网页名称;
  • POJO是DO/DTO/BO/VO的统称,禁止命名成xxx POJO;

2、常量定义

(1)魔法值

  • 不允许出现任何魔法值;
  • 魔法值:即未经定义的常量;
  • 反例: String key="Id#taobao_"+tradeId;

(2)long/Long赋初始值

  • 赋初始值时,使用大写L,而非小写l,小写容易与数字1混淆;

(3)常量归类维护

  • 不要使用一个大而全的常量类维护所有的常量,应该按照功能进行分类维护;
  • 如:系统配置相关的常量放在类ConfigConsts下;

(4)常量复用分层

  • 常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量;
  • 跨应用共享常量:放在二方库中的constant目录下;
  • 应用内共享常量:放在一方库中的modules中的constant目录下(如util模块下的constant目录);
  • 子工程内部共享常量:放在当前子工程的constant目录下;
  • 包内共享常量:放在当前包下单独的constant目录下;
  • 类内共享常量:直接在类内部private static final定义;

3、格式规约

(1)运算符

  • 任何运算符左右必须加一个空格;
  • 运算符包括赋值运算符=、逻辑运算符、加减乘除符号、三目运行符等;

(2)换行

  • 单行字符数限制不超过 120 个,超出需要换行;
  • 运算符与下文一起换行;
  • 方法调用的点符号与下文一起换行;
  • 在多个参数超长,逗号后进行换行;
  • 在括号前不要换行;

(3)缩进

  • 缩进采用4个空格;

(4)方法参数

  • 方法参数在定义和传入时,多个参数逗号后边必须加空格;

4、OOP规约

(1)静态变量/方法

  • 避免通过类的对象访问类的静态变量或静态方法,直接用类名访问即可;

(2)方法覆写

  • 所有的覆写加上@Override注解;
  • 加上@Override注解有助于准确判断是否方法覆盖成功;
  • 如果在抽象类中对方法签名进行修改,则实现类会马上编译报错;

(3)可变参数

  • 相同参数类型、相同业务含义,才使用java的可变参数;
  • 避免使用object;
  • 可变参数列表放置在参数列表的最后;

(4)过时接口

  • 对外暴露的接口,原则上不允许修改方法签名,避免对调用方产生影响;
  • 过时接口必须加上@Deprecated注解,并说明采用的新接口/新服务是什么(接口提供方既然明确了过时接口,那么就有义务同时提供新接口);
  • 不能使用过时的类或方法;

(5)equals方法

  • 应该使用常量或确定有值的对象来调用equals方法,否则容易抛空指针异常;

(6)包装类对象比较大小

  • 相同类型的包装类对象之间比较大小,使用euqals方法;
  • 对于Integer var=?在-128至127之间的赋值,Integer对象是在 IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行 判断;但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断;

(7)基本数据类型&包装数据类型

  • 所有的POJO类属性必须使用包装数据类型(数据库的查询结果可能为null,因为自动拆箱,用基本数据类型接收时会有NPE风险);
  • RPC方法的返回值和参数必须使用包装数据类型;
  • 所有的局部变量使用基本数据类型;
  • 数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险;

(8)POJO类属性默认值

  • 定义DO/DTO/VO等POJO类时,不要设定任何属性默认值;

(9)类内方法

  • 定义顺序:公有方法或保护方法 > 私有方法 > getter/setter方法;
  • 当一个类内有多个构造函数或多个同名函数时,这些方法应按顺序放置在一起;

(10)final用法

  • final可提高程序响应效率;
  • 应声明成final的情况:
    a、不需要重新赋值的变量,包括类属性、局部变量;
    b、对象参数前加final,表示不允许修改引用的指向;
    c、类方法确定不允许被重写;
  • 总体上来说,final指的是“这是不可变的”,其用法如下:
    a、修饰数据:用final关键字修饰的变量,只能进行一次赋值操作,并且在生存期内不可以改变它的值
    b、修饰方法参数:表示不会改变参数的值,如:
public class FinalTest {
    public void finalFunc(final int i, final Value value) {
        // i = 5; 不能改变i的值
        // v = new Value(); 不能改变v的值
        value.v = 5; // 可以改变引用对象的值
    }
}

c、修饰方法:表示该方法不能被覆盖;
d、修饰类:表示该类是无法被继承的;

(11)访问权限

  • 如果不允许外部直接通过new来创建对象,那么构造方法必须是private;
  • 任何类、方法、参数、变量,严控访问范围;过宽泛的访问范围,不利于模块解耦;
  • 工具类不允许有public或default构造方法;
  • 类非static成员变量并且与子类共享,必须是protected;
  • 类非static成员变量并且仅在本类使用,必须是private;
  • 类static成员变量如果仅在本类使用,必须是private;
  • 若是static成员变量,必须考虑是否为final;
  • 类成员方法只供类内部调用,必须是private。
  • 类成员方法只对继承类公开,那么限制为protected;
  • java四种访问权限:
访问权限 本类 本包的类 子类 非子类的外包类
public
protected
default
private

5、集合处理

(1)hashCode和equals方法

  • 只要重写equals,就必须重写hashCode;
  • Set存储的是不重复的对象,依据hashCode和equals方法进行判断,因此Set存储的对象必须重写这两个方法;
  • 如果自定义对象做Map的键,则必须重写这两个方法;

(2)subList方法

  • ArrayList的subList结果不能转化成ArrayList,否则抛ClassCastException异常,如下:
    在这里插入图片描述
  • subList返回的是ArrayList的内部类subList,而不是ArrayList;
    在这里插入图片描述
  • subList返回的是是ArrayList的一个视图,对subList子列表的所有操作会反映到原列表上;
public class Sublist {
    private static void printList(List<Integer> list) {
        StringBuffer sb = new StringBuffer("~");
        list.stream().forEach(e -> sb.append(e).append("~"));
        System.out.println(sb.toString());
    }

    public static void main(String[] args) {
        List<Integer> numList = Arrays.asList(1, 2, 3, 4, 5);
        System.out.println("****numList****");
        printList(numList);
        List<Integer> subList = numList.subList(0, 2);
        System.out.println("****subList****");
        printList(subList);
        for (int i = 0; i < subList.size(); i++) {
            subList.set(i, 10 * subList.get(i));
        }
        System.out.println("****numList****");
        printList(numList);
        System.out.println("****subList****");
        printList(subList);
    }
}

在这里插入图片描述

(3)toArray

  • 使用集合转数组的方法,必须使用集合的toArray(T[] array),传入的是类型完全一样的数组,大小就是 list.size();
  • 直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现 ClassCastException 错误;
  • 使用 toArray 带参方法,入参分配的数组空间不够大时,toArray 方法内部将重新分配内存空间,并返回新数组地址;
  • 如果数组元素大于实际所需,下标为[ list.size() ]的数组元素将被置为 null,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素个数一致。
public class ToArray {
    /**
     * 打印列表
     *
     * @param list
     */
    private static void printList(List<Integer> list) {
        StringBuffer sb = new StringBuffer("-");
        list.stream().forEach(e -> sb.append(e).append("-"));
        System.out.println(sb.toString());
    }

    public static void main(String[] args) {
        List<Integer> numList = Arrays.asList(1, 2, 3, 4, 5);
        System.out.println("****numList****");
        printList(numList);
        Integer[] array = new Integer[numList.size() * 2];
        array = numList.toArray(array);
        printList(Arrays.asList(array));
        System.out.println("****array****");
        printList(Arrays.asList(array));
        Integer[] brray = new Integer[numList.size()];
        brray = numList.toArray(brray);
        printList(Arrays.asList(brray));
        System.out.println("****brray****");
        printList(Arrays.asList(brray));
        Integer[] crray = new Integer[numList.size() / 2];
        crray = numList.toArray(crray);
        printList(Arrays.asList(crray));
        System.out.println("****crray****");
        printList(Arrays.asList(crray));
    }
}

在这里插入图片描述
(4)asList

  • 使用Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方 法,它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常;
  • 说明:asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组;
String[] str = new String[] { "a", "b" };
List list = Arrays.asList(str);

第一种情况:list.add(“c”); 运行时异常。
第二种情况:str[0]= “gujin”; 那么list.get(0)也会随之修改;

  • 不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁;
  • 在 JDK7 版本以上,Comparator 要满足自反性,传递性,对称性,不然 Arrays.sort, Collections.sort 会报 IllegalArgumentException 异常,说明:
    a、自反性:x,y的比较结果和y,x的比较结果相反;
    b、传递性:x>y,y>z,则x>z;
    c、对称性:x=y,则x,z比较结果和y,z比较结果相同;

(5)集合初始化

  • 集合初始化时,尽量指定集合初始值大小;
  • ArrayList尽量使用ArrayList(int initialCapacity) 初始化。

(6)Map

  • 使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历;
  • keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的 value;
  • entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高;
  • values()返回的是 V 值集合,是一个 list 集合对象;keySet()返回的是 K 值集合,是一个 Set 集合对象;entrySet()返回的是 K-V 值组合集合;
  • 高度注意 Map 类集合 K/V 能不能存储 null 值的情况,如下表格:
    在这里插入图片描述
  • 由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,注意存储 null 值时会抛出 NPE 异常;

(7)集合排序

  • 合理利用好集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响;
  • 稳定性指集合每次遍历的元素次序是一定的;
  • 有序性是指遍历的结果是按某种比较规则依次排列的。如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet 是 order/sort。

(8)Set

  • 利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的 contains 方法进行遍历、对比、去重操作;

6、并发处理

(1)线程

  • 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯;
  • 正例:
public class TimerTaskThread extends Thread { 
	public TimerTaskThread(){
	super.setName("TimerTaskThread"); 
	...
 }
  • 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程;
  • 说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题;
  • 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者 “过度切换”的问题;
  • 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险;
  • 说明:Executors 返回的线程池对象的弊端如下:
    a、FixedThreadPool 和 SingleThreadPool:
    允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM;
    b、CachedThreadPool 和 ScheduledThreadPool:
    允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

(2)锁

  • 高并发时,同步调用应该去考量锁的性能损耗;
  • 能用无锁数据结构,就不要用锁;
  • 能锁区块,就不要锁整个方法体;
  • 能用对象锁,就不要用类锁;
  • 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁;
  • 说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序 也必须是 A、B、C,否则可能出现死锁。
  • 并发修改同一记录时,避免更新丢失,要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据;
  • 说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次;

7、控制语句

(1)条件控制

  • 在一个 switch 块内,每个 case 要么通过 break/return 等来终止,要么注释说明程序将继续执行到哪一个case 为止;
  • 在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有;
  • 在 if/else/for/while/do 语句中必须使用大括号,即使只有一行代码;
  • 如果非得使用if()…else if()…else…方式表达逻辑,请勿超过3层;
  • 逻辑上超过 3 层的 if-else 代码可以使用卫语句,或者状态模式来实现;
  • 卫语句(guard clauses)就是把复杂的条件表达式拆分成多个条件表达式,比如一个很复杂的表达式,嵌套了好几层的if-then-else语句,转换为多个if语句,实现它的逻辑,这多条的if语句就是卫语句。用法如下:

有时候真正的业务代码可能在嵌套多次才执行,其他分支只是简单报错返回的情况。对于这种情况,应该单独检查报错返回的分支,当条件为真时立即返回,这样的单独检查就是应用了卫语句。

if(obj != null){
  doSomething();
}
 
转换成卫语句以后的代码如下:
if(obj == null){
   return;
}
doSomething();
  • 循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的 try-catch 操作(这个 try-catch 是否可以移至循环体外);

(2)参数校验

  • 方法中需要进行参数校验的场景:
    a、调用频次低的方法。
    b、执行时间开销很大的方法,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退,或者错误,那得不偿失;
    c、需要极高稳定性和可用性的方法。
    d、对外提供的开放接口,不管是RPC/API/HTTP接口;
    e、敏感权限入口;
  • 方法中不需要参数校验的场景:
    a、极有可能被循环调用的方法,不建议对参数进行校验。但在方法说明里必须注明外部参数检查;
    b、底层的方法调用频度都比较高,一般不校验。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题;
    c、一般 DAO 层与 Service 层都在同一个应用中,部署在同一台服务器中,所以 DAO 的参数校验,可以省略;
    d、被声明成private只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数;

8、注释规约

  • 类、类属性、类方法的注释必须使用 Javadoc 规范,使用/*内容/格式,不得使用 //xxx 方式;
  • 在 IDE 编辑窗口中,Javadoc 方式会提示相关注释,生成 Javadoc 可以正确输出相应注释;
  • 在 IDE 中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率;
  • 所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、 异常说明外,还必须指出该方法做什么事情,实现什么功能; - 所有的枚举类型字段必须要有注释,说明每个数据项的用途;

9、其他

  • 注意 Math.random() 这个方法返回是 double 类型,注意取值的范围 0≤x<1(能够取到零值,注意除零异常),如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后 取整,直接使用 Random 对象的 nextInt 或者 nextLong 方法;
  • 获取当前毫秒数 System.currentTimeMillis();而不是new Date().getTime();原因如下:
public Date() {
        this(System.currentTimeMillis());
    }

从源码可以看出,new Date()其实就是调用了System.currentTimeMillis(),再传入自己的有参构造函数。不难看出,如果只是仅仅获取时间戳,即使是匿名的new Date()对象也会有些许的性能消耗, 从提升性能的角度来看,只是仅仅获取时间戳,直接调用System.currentTimeMillis()会更好一些;

  • new Date()来获取当前时间更多的是因为我们使用习惯导致经常第一时间想到用它来获取当前时间; java.util.Date其实是设计来作为格式化时间,以面向对象的方式获取与时间有关的各方面信息,例如:获取年月份、小时、分钟等等比较丰富的信息;
  • 任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存;

猜你喜欢

转载自blog.csdn.net/lydia_cmy/article/details/96276794