Skip to content

java guides

leizh edited this page Sep 12, 2018 · 10 revisions

概述

以阿里巴巴开发手册1.4.0版本为索引,结合广为流传的Google Java Style Guide,以及标杆经典著作 Effective Java,交叉参考形成此评述性文档

参考

内容

以阿里的开发手册1.4版(以下简称手册)中的编程规约和异常日志部分为蓝本及基础,并结合其他较为知名的guidelines,形成一下文本

如果遵循阿里开发手册中的意见(page 1-22),这里不再特别列出,以下,仅记录对手册的修改和补充部分

对手册中内容的调整和补充

(一) 1 关于标识符的名称规则

原文:

【强制】代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。 反例:name / name / $name / name / name$ / name

说明:

  • 按说这个规则应该是没有争议的,拎出来是内部讨论时候有提到下划线是会被用到的,所以这里需要进行补充说明
  • 美元符号实际上已经认为是内部类等系统内部使用了
  • 下划线有很大一部分是源于其他语言,比如python,在java中不适用
  • 有听过老程序员提到,有代码生成工具通过下划线来生成类似setter或getter这类的代码,java世界有如果有,现在也已经废弃了
  • 这条guideline没有对中间的下划线做出规定,从后面的对于驼峰命名法的几条约束来看,它不鼓励蛇形命名法及其和驼峰的混用,但在Google Java Style Guide 中提到JUnit的test method有一种例外,混合驼峰和蛇形命名法是被接受的命名模式

(一)3 当缩写遇到驼峰命名法

原文:

【强制】类名使用 UpperCamelCase 风格,但以下情形例外:DO / BO / DTO / VO / AO / PO / UID 等。
正例:MarcoPolo / UserDO / XmlService / TcpUdpDeal / TaPromotion
反例:macroPolo / UserDo / XMLService / TCPUDPDeal / TAPromotion

修改为:

  • 【强制】类名使用UpperCamelCase风格,遇到缩写和不常见结构体时,遵循google java style guides中规则

说明:

阿里这条比较拧巴,为什么TCP UDP就要按驼峰而这些例外不要?豁免权的来源完全搞不清楚,估计是其内部对现状的妥协,这么去落实,实际上是在违反一致性原则;如果真是遗留代码的问题,应当给一个对旧代码的整体例外约束比较好,这一点在Google的 C++ Style Guide中有明确的章节来说明,所以说java还是年轻?

(四)14,split和空字符串的问题

原文:

【推荐】使用索引访问用 String 的 split 方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛IndexOutOfBoundsException 的风险。

说明:

String str = "a,b,c,,";  
String[] ary = str.split(",");  
// 预期大于 3,结果是 3 
System.out.println(ary.length);

修改并补充:

  • 【推荐】使用索引访问用 String 的 split 方法得到的数组时,要对数组长度做检查,否则会有抛IndexOutOfBoundsException 的风险
  • 【推荐】如果允许split结果数组中有空字符串,那么可以使用split(regex,-1)来进行切分来达到设计目的

说明:

  • 手册意图比较明确,但是说错了;实际上应该检查的是数组长度,也就是生产代码中如何防范这种非受控异常,手册说明部分本意是这个问题
  • 手册应当还有一个意图,就是它期望能取到最后那个空字符串,这时靠检查是没用,而应当对正则的匹配限定limit,补充部分强调的就是这部分
  • 另外不要对split加limit为-1做出限定,否则就会失去一个功能,split(regex)是两个参数split的一个特例,详见JDK的Java doc

(四)15,方法重载的顺序问题

原文:

【推荐】当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起, 便于阅读,此条规则优先于第 16 条规则。

修改:

  • 【推荐】重载方法要放置在一起,中间不要插入其他任何内容,此条规则优先于第 16 条规则。

说明:

  • 阿里的同学真的怕重载是个大家不懂的名词?
  • 推荐按顺序又没给出应该按什么顺序,参数的字母顺序?参数数量?和关联的16条一看大概明白他的意思了,这两条合在一起应该都是给一个通用准则的狗尾续貂

(四)16,类内要素的顺序问题

原文:

【推荐】 类内方法定义的顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter 方法。

说明:公有方法是类的调用者和维护者最关心的方法,首屏展示最好;保护方法虽然只是子类 关心,也可能是“模板设计模式”下的核心方法;而私有方法外部一般不需要特别关心,是一个 黑盒实现;因为承载的信息价值较低,所有 Service 和 DAO 的 getter/setter 方法放在类体最后

重写:

  • 【推荐】 类的成员要按照逻辑规则进行组织

说明:

这个问题也是容易引起争议的地方,主要是具体的规则都非常难以操作;考虑到手册仅仅是推荐,因此不对它这里缺少原则、涉及内容不全面、令人疑惑的说明做出修改描述(手册很难修正了,只能重写这条规则)

先细说一下手册的问题:

  • 只涉及了方法顺序,没涉及成员变量,那就更不用提静态初始化块儿的了
  • 可能是阿里得同学都不用包可见级别
  • getter/setter的顺序解释很奇特,按说阿里这份guidelines是很注重工程实践的,比如拆装箱部分,就对effective java中的item做出了很有实践意义的加强,所以这里写成这样确实很难理解,这里详细说下
    • 这个世代需要在service或这DAO这两层类型写accessor这种方法的情况应该已经很少了,通常是基于注解并通过DI框架来完成功能,如果是为了保持一种纯粹性,注解使用@Resource
    • 大量需要getter/setter方法的类通常也只有getter/setter(典型的贫血模型+事务脚本这种实现模式)
    • 大量需要getter/setter方法的类是值对象,贫血模型,通常现在应当鼓励使用类似Lombok或者AutoValue这类类库来减少撰写和维护时无谓的工作量,当然损失掉的是这部分类型源码和字节码一一对应这个Java特性

唯一能确定的顺序是顺序必须按照某种类内逻辑关系来组织,但是问题就在于这种逻辑关系实际上一般不具有普适性,所以很难具体规定出来,这里摘抄翻译的 google style guide关于这里的建议如下:

3.4.2 类内部定义的顺序

类中成员和初始化函数顺序的选择对于该类设计的理解上有很大的影响。然而是没有唯一正确的守则来确定如何去做;不同的类可能用不同的顺序来组织他们内容的顺序才是合适的 重要的是每个类要使用某种确定的逻辑顺序来进行组织,他的维护者要能解释清楚这种逻辑顺序;例如,新的方法不能习惯性的在类的最下面一放就完事了,因为这样就变成了“明摆着的按时间添加”顺序,而不是逻辑顺序

3.4.2.1 重载:绝不要拆开

当一个类有多个构造函数,或者多个方法使用了相同的方法名,那么他们应该紧挨着撰写,中间不要插入任何代码(就算是私有的成员也不要不要)

这种原则性描述基本上够了,再往上添加很有可能变成立场之争,浪费生命;回过头来再看一下手册的说明,能探出它基本思路是鼓励封装和隔离的,但是理由阐述的并不充分甚至可能是错的和混乱的:

  • 调用者和维护者立场不同最关心的不可能,也不应该一样:
    • 维护者最关心对外接口要甚于实现细节?
    • 对外接口视图难道不应该是interface去集中表达?
    • 调用者难道不该使用接口?
  • 等到最后service/DAO的accessor之论已经乱套的一塌糊涂了,这个上面已经罗列过了

如果需要一个更有可操作性的指导,可以考虑在满足更具内在逻辑表达的情况下,可见性高的放前面,如果有public的因为其他public的method关联性更强的private挤到好几屏以后了,那应该考虑内聚性的问题,该拆类了

五(6)关于Java泛型的协变问题

原文:

【强制】泛型通配符<? extends T>来接收返回的数据,此写法的泛型集合不能使用 add 方 法,而<? super T>不能使用 get 方法,作为接口调用赋值时易出错。

说明:扩展说一下 PECS(Producer Extends Consumer Super)原则:第一、频繁往外读取内 容的,适合用<? extends T>。第二、经常往里插入的,适合用<? super T>。

修改:

  • [推荐]遵从Effective Java 2nd Edition 的 Item 28: Use bounded wildcards to increase API flexibility,如果你能看懂它的话

说明:

  • 能完全看懂原书这条的不一定要奉为天人,也可以肃然起敬了
  • 这里手册的问题是,本来就难懂的问题,经过手册这么一番不说人话,彻底凌乱了
  • extend的时候,add 不可以被使用,这是因为只知道里面是T或T派生的类型,但是不知道具体是哪种类型,可能会引发在原始初始化类型被引用的其他场合是否合适;
  • super时候,get当然也是可以的,只是get出来的对象是不能给T型变量赋值的,而只能给他的超类,
  • 只是说这个有什么意义呢?编译器就可以帮助检查出来
  • 按说Java泛型不支持协变,然后搞出一个有限通配符类型来指导实践,是为了实现更具灵活性的API,但是成了java 基本语法里面最难理解的规则之一,我个人的倾向最好的办法其实还是不考虑协变设计,甚至应当较少的考虑使用参数化类型来设计自己的API,除非真的知道自己在干什么;但这是一种设计退化的思路,偷偷的做还行,不能编为正式guideline去提倡的
  • 阿里手册写了这么一条,不知用意何在,实际上Effective Java 2nd Edition泛型这章Item 23、24、25都是比较好理解且值得贯彻的,从item 26开始看不懂不应该踩

(五)7,迭代容器时候的细节问题

原文:

【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。

修改:

  • 【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式;如果并发操作,考虑使用CopyOnWriteArrayList等线程安全集合;

说明:

  • 手册原文其实三句话,第一句说的特别对,第二句勉强,第三句牵强了
  • 从举得例子里面可以看出它所想表达的头两句话的意思,但是具体的细节它没有给出解释,这就是集合框架对非线程安全集合快速失败的设计,非线程安全集合明显也不该在非线程安全场景下使用,所以第三句是牵强的
  • 那么例子里面非并发场景这个问题又是怎么回事,最好的避免方法是当原对象是不可变容器,便利过滤后生成新的容器然后赋值给原变量即可,JDK 8的 stream API 可以很容易的处理这类问题
  • List语义上的并发增删其实非常复杂,否则就没有fast-fail机制了,仔细考虑设计场景,是否生产消费模型的阻塞队列(甚至是阻塞的优先级队列)才是真正想要的,不要重复造轮子

(五)9,钻石表达式

修改:

  • 【强制】集合泛型定义时,在 JDK7 及以上,使用 diamond 语法或全省略

说明:

  • 没有任何理由不用

(五)13,有序概念的区分:数据的逻辑顺序和插入顺序

原文

【参考】合理利用好集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和 不稳定性(unorder)带来的负面影响。

说明:有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次 序是一定的。如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet 是 order/sort

修改:

  • 【参考】分清插入顺序和逻辑顺序(自然顺序)

说明:

  • 此条目虽然是参考,但是非常重要,只是手册用词非常不准确:
  • 稳定性通常是用来指排序过程是否会影响原有元素次序的,是用来衡量排序算法的,这里是名词滥用
  • 已排序的也应该叫sorted;关键概念应该叫 insertion-order 或 natural ordering;这个问题在招聘面试时跟很多候选人谈到,大多说不清楚,说明对相关概念理解不深,在实际工作中选用工具时难免会遇到问题

(六)2,线程名自定义

原文:

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

正例:

public class TimerTaskThread extends Thread { public TimerTaskThread() { super.setName("TimerTaskThread"); ... } }

修改:

  • 可以使用Spring的CustomizableThreadFactory类来完成名称自定义

说明:

  • 这一条其实也真是没什么错,唯独例子很拧巴,和下面那条有冲突

(六)4,线程池的实例化

修改:

  • 【建议】推荐使用Spring框架中的线程池实现,如使用Executors中带参数的工厂方法来创建线程池,需重点考虑任务淘汰机制

说明:

  • 它这个条目写的略武断,意图很明确,但是理由不充分
  • OOM是由于堆积,但堆积的原因是因为处理能力不够
  • 这里的问题是任务超时淘汰策略和入队淘汰策略之间的选择问题,这里又没有给出一个有操作性的解决方案(也许是限于篇幅)

(六)8,并发修改的锁问题

说明:

  • 应用层怎么加锁?分布式应用需要依赖中间件服务做分布式锁
  • 缓存层加锁几个意思?
  • 数据库层加锁用version可能是繁琐的做法,可以考虑用修改前状态来作为附加条件来执行幂等操作,避免多余的字段来干扰数据库

(七)4,代码复杂度问题

说明:

  • 不光是if...else,嵌套的循环也要注意提取方法

(七)7,

说明:

  • 为什么不强制?

(八)3,JavaDoc标明始作俑者

说明:

  • 与其要求这个按手印,不如花时间多写设计意图

(八)8,使用注释做版本控制的问题

说明:

  • 确实他们很多地方在用svn
  • 真有可能在被用到,请加个开关

(九)5,时间戳

说明:

  • nanoTime是个相对值,不应该独立使用

(九)7,???

说明:

  • 不知道在说什么,指定了初始化大小就不会增长了

(九)8,同(八)8

手册内容的补充

Clone this wiki locally