Ali Java 开发手册摘录

大致过了一番,有一些平时没注意到或比较重要的东西,简单记录一下。

团队上通常都有自己的开发规范,但通常不够细致,或只是提及应当避免的部分,而阿里这份文档就不仅仅是编码规范,同时提及了一些语言、用法上的坑,碰上没碰上都可以看看,了解一下有个印象也可以帮助自己在以后开发过程中避免类似的坑。

比较有意思的有 集合、并发处理等章节。

一些强制的东西感觉比较好,省的自己考虑了,也不用考虑别人怎么玩了,大家都一样

1. 编程规范

1.1 命名规范

  1. 代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束

    Java 整体编码风格上都不太使用 _$

  2. 【强制】 POJO 类中布尔类型的变量,都不要加 is,否则部分框架解析会引起序列化错误

  3. 【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。

    正例: 应用工具类包名为 com.alibaba.open.util、类名为 MessageUtils(此规则参考spring 的框架结构)

  4. 【强制】杜绝完全不规范的缩写, 避免望文不知义。

    反例: AbstractClass“缩写” 命名成 AbsClass; condition“缩写” 命名成 condi,此类随意缩写严重降低了代码的可阅读性。

    主要是阅读代码的人不能很好融入你开发时的状态。

  5. 【参考】各层命名规约:

    A) Service/DAO 层方法命名规约

    1) 获取单个对象的方法用 get 做前缀。
    2) 获取多个对象的方法用 list 做前缀。
    3) 获取统计值的方法用 count 做前缀。
    4) 插入的方法用 save(推荐) 或 insert 做前缀。
    5) 删除的方法用 remove(推荐) 或 delete 做前缀。
    6) 修改的方法用 update 做前缀。
    

    B) 领域模型命名规约

    1) 数据对象: xxxDO, xxx 即为数据表名。
    2) 数据传输对象: xxxDTO, xxx 为业务领域相关的名称。
    3) 展示对象: xxxVO, xxx 一般为网页名称。
    4) POJO 是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO。
    

1.2 常量定义

  1. 【强制】不允许出现任何魔法值(即未经定义的常量) 直接出现在代码中。

    这个还是要强制的,大部分魔法值的引入基本都会给后期维护造成困难

  2. 【强制】 long 或者 Long 初始赋值时,必须使用大写的 L,不能是小写的 l,小写容易跟数字1 混淆,造成误解。

  3. 【推荐】不要使用一个常量类维护所有常量,应该按常量功能进行归类,分开维护。如:缓存相关的常量放在类: CacheConsts 下; 系统配置相关的常量放在类: ConfigConsts 下。

    说明: 大而全的常量类,非得使用查找功能才能定位到修改的常量,不利于理解和维护。

    这个要注意,项目小的时候没注意,等到注意的时候要动到的部分就已经很多了。正在爬这个坑。缺乏经验啊~

  4. 【推荐】常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量。

    1) 跨应用共享常量:放置在二方库中,通常是 client.jar 中的 constant 目录下。

    2) 应用内共享常量:放置在一方库的 modules 中的 constant 目录下。

    反例: 易懂变量也要统一定义成应用内共享常量,两位攻城师在两个类中分别定义了表示“是”的变量:
    类 A 中: public static final String YES = "yes";
    类 B 中: public static final String YES = "y";
    A.YES.equals(B.YES),预期是 true,但实际返回为 false,导致产生线上问题。
    

    3) 子工程内部共享常量:即在当前子工程的 constant 目录下。

    4) 包内共享常量:即在当前包下单独的 constant 目录下。

    5) 类内共享常量:直接在类内部 private static final 定义。

1.4 OOP 规范

  1. 【强制】外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加@Deprecated 注解,并清晰地说明采用的新接口或者新服务是什么

    重点是1.不能删除 2. 标注新接口

  2. 【强制】所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。

    说明: 对于 Integer var = ?在-128 至 127 之间的赋值, Integer 对象是在IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。

    比较老的考点了,还是要注意。

  3. 关于基本数据类型与包装数据类型的使用标准如下:

    1) 【强制】 所有的 POJO 类属性必须使用包装数据类型。

    2) 【强制】 RPC 方法的返回值和参数必须使用包装数据类型。

    3) 【推荐】 所有的局部变量使用基本数据类型。

    说明: POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何NPE 问题,或者入库检查,都由使用者来保证。

    正例: 数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。
    反例: 比如显示成交总额涨跌情况,即正负 x%, x 为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示: 0%,这是不合理的,应该显示成中划线-。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。

  4. 【强制】定义 DO/DTO/VO 等 POJO 类时,不要设定任何属性默认值。

  5. 【强制】序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败; 如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。

    说明: 注意 serialVersionUID 不一致会抛出序列化运行时异常。

    Intellij Idea 15 生成serialVersionUID的方法

  6. 【强制】 POJO 类必须写 toString 方法。使用 IDE 的中工具: source> generate toString时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString。

    说明: 在方法执行抛出异常时,可以直接调用 POJO 的 toString()方法打印其属性值,便于排
    查问题。

  7. 【推荐】 setter 方法中,参数名称与类成员变量名称一致, this.成员名 = 参数名。在getter/setter 方法中, 不要增加业务逻辑,增加排查问题的难度。

    反例:
    public Integer getData() {
        if (true) {
            return data + 100;
            } else {
            return data - 100;
        }
    }
    

    getter/setter 不增加业务逻辑

  8. 【推荐】类成员与方法访问控制从严:

    1) 如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private。
    2) 工具类不允许有 public 或 default 构造方法。
    3) 类非 static 成员变量并且与子类共享,必须是 protected。
    4) 类非 static 成员变量并且仅在本类使用,必须是 private。
    5) 类 static 成员变量如果仅在本类使用,必须是 private。
    6) 若是 static 成员变量,必须考虑是否为 final。
    7) 类成员方法只供类内部调用,必须是 private。
    8) 类成员方法只对继承类公开,那么限制为 protected。
    说明: 任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块解耦。
    
    思考:如果是一个 private 的方法,想删除就删除,可是一个 public 的 service 方法,或者一个 public 的成员变量,删除一下,不得手心冒点汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大,如果无限制的到处跑,那么你会担心的。
    

1.5 集合处理

  1. 【强制】使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一样的数组,大小就是 list.size()。

    说明: 使用 toArray 带参方法,入参分配的数组空间不够大时, toArray 方法内部将重新分配
    内存空间,并返回新数组地址; 如果数组元素大于实际所需,下标为[ list.size() ]的数组元素将被置为 null,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素
    个数一致。
    正例:
    List<String> list = new ArrayList<String>(2);
    list.add("guan");
    list.add("bao");
    String[] array = new String[list.size()];
    array = list.toArray(array);
    反例: 直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它
    类型数组将出现 ClassCastException 错误
    
  2. 【强制】使用工具类 Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。

    说明: asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList
    体现的是适配器模式,只是转换接口,后台的数据仍是数组。
    String[] str = new String[] { "a", "b" };
    List list = Arrays.asList(str);阿里巴巴 Java 开发手册
    ——禁止用于商业用途,违者必究—— 12 / 34
    第一种情况: list.add("c"); 运行时异常。
    第二种情况: str[0] = "gujin"; 那么 list.get(0)也会随之修改。        
    
  3. 【强制】不要在 foreach 循环里进行元素的 remove/add 操作。 remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。

        反例:
        List<String> a = new ArrayList<String>();
        a.add("1");
        a.add("2");
        for (String temp : a) {
        if ("1".equals(temp)) {
        a.remove(temp);
        }
        }
        说明: 以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的
        结果吗?
        正例:
        Iterator<String> it = a.iterator();
        while (it.hasNext()) {
        String temp = it.next();
        if (删除元素的条件) {
        it.remove();
        }
        }
    
  4. 【推荐】使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。

    说明: keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的 value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8,使用 Map.foreach 方法。
    
    正例: values()返回的是 V 值集合,是一个 list 集合对象; keySet()返回的是 K 值集合,是一个 Set 集合对象; entrySet()返回的是 K-V 值组合集合。
    

1.6 并发处理

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

  2. 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

  3. 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

    说明: Executors 返回的线程池对象的弊端如下:
    1) FixedThreadPool 和 SingleThreadPool:
    允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
    2) CachedThreadPool 和 ScheduledThreadPool:
    允许的创建线程数量为 Integer.MAX_VALUE, 可能会创建大量的线程,从而导致 OOM。
    

    一直以为通过 Executors 创建线程池是好习惯 – -。。。

  4. 【强制】 SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为static,必须加锁,或者使用 DateUtils 工具类。

    正例: 注意线程安全,使用 DateUtils。亦推荐如下处理:
    private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
    @Override
    protected DateFormat initialValue() {
    return new SimpleDateFormat("yyyy-MM-dd");
    }
    };
    说明: 如果是 JDK8 的应用,可以使用 Instant 代替 Date, LocalDateTime 代替 Calendar,
    DateTimeFormatter代替Simpledateformatter,官方给出的解释:simple beautiful strong
    immutable thread-safe。
    
  5. 【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁; 能
    锁区块,就不要锁整个方法体; 能用对象锁,就不要用类锁。

  6. 【强制】多线程并行处理定时任务时, Timer 运行多个 TimeTask 时,只要其中之一没有捕获
    抛出的异常,其它任务便会自动终止运行,使用 ScheduledExecutorService 则没有这个问题。

  7. 【推荐】避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一
    seed 导致的性能下降。

    说明: Random 实例包括 java.util.Random 的实例或者 Math.random()的方式。
    正例: 在 JDK7 之后,可以直接使用 API ThreadLocalRandom, 而在 JDK7 之前, 需要编码保
    证每个线程持有一个实例。
    

1.7 控制语句

  1. 【强制】在 if/else/for/while/do 语句中必须使用大括号。 即使只有一行代码,避免使用
    单行的形式: if (condition) statements;

  2. 【推荐】 表达异常的分支时, 少用 if-else 方式, 这种方式可以改写成:

        if (condition) {
        ...
        return obj;
        }
        // 接着写 else 的业务逻辑代码;
        说明: 如果非得使用 if()...else if()...else...方式表达逻辑,【强制】请勿超过 3 层,
        超过请使用状态设计模式。
        正例: 逻辑上超过 3 层的 if-else 代码可以使用卫语句,或者状态模式来实现。
    
  3. 【参考】 下列情形, 不需要进行参数校验

    1) 极有可能被循环调用的方法。但在方法说明里必须注明外部参数检查要求。

    2) 底层调用频度比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。一般 DAO 层与 Service 层都在同一个应用中,部署在同一台服务器中,所以 DAO 的参数校验,可以省略。

    3) 被声明成 private 只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数。

1.8 注释规约

这个全都很重要…

其他

  1. 【强制】注意 Math.random() 这个方法返回是 double 类型,注意取值的范围 0≤x<1(能够取到零值,注意除零异常) ,如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后取整,直接使用 Random 对象的 nextInt 或者 nextLong 方法。

  2. 【强制】获取当前毫秒数 System.currentTimeMillis(); 而不是 new Date().getTime();说明: 如果想获取更加精确的纳秒级时间值, 使用 System.nanoTime()的方式。在 JDK8 中,针对统计时间等场景,推荐使用 Instant 类。

2. 异常日志

2.1 异常处理

  1. 【强制】 Java 类库中定义的一类 RuntimeException 可以通过预先检查进行规避,而不应该通过 catch 来处理,比如: IndexOutOfBoundsException, NullPointerException 等等。说明: 无法通过预检查的异常除外,如在解析一个外部传来的字符串形式数字时,通过 catchNumberFormatException 来实现。

    正例: if (obj != null) {...}
    反例: try { obj.method() } catch (NullPointerException e) {...}
    
  2. 【推荐】方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。调用方需要进行 null 判断防止 NPE 问题。

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

  3. 【推荐】防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:

    1) 返回类型为基本数据类型, return 包装数据类型的对象时,自动拆箱有可能产生 NPE。
    反例: public int f() { return Integer 对象}, 如果为 null,自动解箱抛 NPE。
    2) 数据库的查询结果可能为 null。
    3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
    4) 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
    5) 对于 Session 中获取的数据,建议 NPE 检查,避免空指针。
    6) 级联调用 obj.getA().getB().getC(); 一连串调用,易产生 NPE。
    正例: 可以使用 JDK8 的 Optional 类来防止 NPE 问题。
    

2.2 日志规约

  1. 【强制】应用中不可直接使用日志系统(Log4j、 Logback) 中的 API,而应依赖使用日志框架SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    private static final Logger logger = LoggerFactory.getLogger(Abc.class)
    
  2. 【强制】对 trace/debug/info 级别的日志输出,必须使用条件输出形式或者使用占位符的方式。

    说明: logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
    如果日志级别是 warn,上述日志不会打印,但是会执行字符串拼接操作,如果 symbol 是对象,
    会执行 toString()方法,浪费了系统资源,执行了上述操作,最终日志却没有打印。
    正例: (条件)
    if (logger.isDebugEnabled()) {
    logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
    }
    正例: (占位符)
    logger.debug("Processing trade with id: {} symbol : {} ", id, symbol);
    
  3. 【强制】避免重复打印日志,浪费磁盘空间,务必在 log4j.xml 中设置 additivity=false。

    正例: <logger name="com.taobao.dubbo.config" additivity="false">
    
  4. 【参考】可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。注意日志输出的级别, error 级别只记录系统逻辑出错、异常等重要的错误信息。如非必要,请不要在此场景打出 error 级别。

手册下载


版权声明:本文为a06_kassadin原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/a06_kassadin/article/details/72190112