《阿里巴巴开发手册》读书笔记-编程规约

命名风格

  • 类名使用UpperCamelCase风格
  • 方法名,参数名,成员变量,局部变量都统一使用lowerCamelcase风格
  • 常量命名全部大写,单词间用下划线隔开, 力求语义表达完整清楚,不要嫌名字长
  • 抽象类命名使用Abstract或者Base开头
  • 异常类命名使用Exception结尾
  • 测试类命名要以要测试的类的名称命名,以Test结尾
  • 类型与中括号紧挨来表示数组
  • POJO类中布尔类型的变量都不要加is前缀,在部分框架中会引起序列化错误
  • 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词.包名统一使用单数形式.但是类名如果有复数含义,可以使用复数形式
  • 杜绝不规范的缩写,避免望文不知义
  • 为了达到代码自解释的目标,任何自定义的编程元素在命名时,使用尽量完整的单词组合来表达含义
  • 如果模块,接口,类,方法使用了设计模式,在命名时需要体现出设计模式
  • 接口类中的方法和属性不要加任何修饰符号(不要加public), 保持代码的简洁
  • 尽量不要在接口中定义变量,如果一定要定义变量,一定是与接口方法有关的,并且是整个应用的基础变量
    • 接口方法签名: void commit()
    • 接口基础常量: String COMPANY="Oxford"
  • 接口和实现类:
    • 对于ServiceDAO类,基于SOA的理念,暴露出来的服务一定是接口,内部的实现类用Impl的后缀与接口的区别
    • 如果是形容能力的接口名称,去对应的形容词为接口(-able的形式)
  • 枚举类带上Enum后缀,枚举成员名称需全部大写
    • 枚举类是特殊的类,域成员均为常量,且构造方法被默认强制是私有的
  • 各层命名规范:
    • Service或者DAO层方法命名规范:
      • 获取单个对象的方法用get做前缀
      • 获取多个对象的方法用list做前缀,复数形式结尾
      • 获取统计值的方法用count做前缀
      • 插入方法使用save或者insert做前缀
      • 删除的方法使用remove或者delete做前缀
      • 修改的方法使用update做前缀
    • 领域模型命名规范:
      • 数据对象: XxxDO,Xxx为数据表名
      • 数据传输对象: XxxDTO,Xxx为业务领域相关的名称
      • 展示对象: XxxVO,xxx一般为网页名称
      • POJO为DO,DTO,BO,VO的统称,禁止命名成XxxPOJO

常量定义

  • 不允许任何未经预先定义的常量出现在代码中
  • 在long或者Long赋值时,数值后使用大写的L, 不能是小写的l. 因为小写容易和数字1混淆,造成误解
  • 不要使用一个常量类维护所有常量,要按常量的功能进行归类,分开维护
    • 大而全的常量类杂乱无章,使用查找功能才能定位到修改的常量,不利于理解和维护
  • 常量的复用层次有五层:
    • 跨应用共享常量: 放置在二方库中,通常是client.jar中的constant目录下
    • 应用类共享常量 放置在一方库中,通常是子模块中的constant目录下
    • 子工程内共享常量 在当前子工程的constant目录下
    • 包内共享常量 在当前包的constant目录下
    • 类内共享常量 直接在类内部private static final定义
  • 如果变量值仅在一个固定范围内变化,使用enum类型定义
    • 如果存在名称之外的延伸属性应使用enum类型,比如季节,表示一年中第几个季节:
public enum SeasonEnum {
	SPRING(1),SUMMER(2),AUTUMN(3),WINTER(4);
	private int seq;
	SeasonEnum(int seq) {
		this.seq=seq;
	}
} 

代码格式

  • 大括号的使用约定:
    • 如果大括号内为空,则简洁地写成 { } 即可,不需要换行
    • 如果是非空代码块:
      • 左大括号前不换行
      • 左大括号后换行
      • 右大括号前换行
      • 右大括号后如果还有else则不换行
      • 表示终止的右大括号后必须换行
  • 小括号的使用约定:
    • 左小括号和字符之间不要出现空格
    • 右小括号和字符之间也不要出现空格
    • 左大括号之前需要空格
  • if,for,while,switch,do等保留字与括号之间都必须加空格
  • 任何二目,三目运算符左右两边都需要加一个空格
    • 运算符包括:
      • 赋值运算符 :=
      • 逻辑运算符 :&&
      • 加减乘除符号
  • 采用4个空格进行缩进
  • 注释的双斜线与注释内容之间有且仅有一个空格
  • 方法参数在定义和传入时,多个参数逗号后面必须加空格
  • 不需要增加若干空格来使某一行的字符与上一行对应位置的字符对齐
  • 不同逻辑,不同语义,不同业务代码之间只需要插入一个空行分割来提升可读性即可

OPP规约

  • 避免通过一个类的对象引用访问类的静态变量和静态方法,这会增加编译器的解析成本,直接使用类名访问即可
  • 所有的覆写方法,必须加 @Override
  • 相同参数类型,相同业务含义,才可以使用Java的可变参数,避免可变参数使用Object类型
    • 可变参数必须放置在参数列表的最后, 建议尽量不要用可变参数编程
  • 外部正在调用的或者二方库依赖的接口,不允许修改方法签名(方法名和参数列表),避免对接口的调用方产生影响 .接口过时必须加上 @Deprecated 注解,并清晰地说明采用的新接口和新服务是什么
  • 不能使用过时的类或方法:
    • 接口的提供方既然明确是过时接口,那么有义务提供新接口
    • 作为调用方,有义务考证过时方法的新实现是什么
  • Objectequals方法容易抛出空指针异常,应使用常量或者确定有值的对象来调用equals
    • "test".equals(Object)
    • 推荐使用java.util.objects
  • 所有相同类型的包装类对象之间的值的比较,全部使用equals方法比较
    • 对于 Integer var = ? 在-128至127范围内赋值时 ,Integer对象是在IntegerCache.cache中产生,会复用已有对象,这个区间内的Integer值可以直接使用 == 进行判断
    • 但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,所以推荐使用equals方法进行比较
  • 基本类型和包装类型的使用标准:
    • 所有的POJO类属性必须使用包装类数据类型
    • RPC方法的返回值和参数必须使用包装数据类型
    • 所有的局部变量使用基本数据类型
  • 定义DO,DTO,VO等POJO类时,不要设定任何属性默认值
  • 序列化类新增属性时,不能修改serialVersionUID字段,这样会导致反序列化失败;如果完全不兼容升级,避免反序列化混乱,可以修改serialVersionUID值.在serialVersionUID不一致时会抛出序列化运行时异常
  • 构造方法中禁止加入任何业务逻辑,如果有初始化逻辑,要放在init
  • POJO类必须写toString方法.如果继承了一个POJO类,需要在前面添加super.toString
    • 这样在方法执行抛出异常时,可以直接调用POJO的toString()方法打印属性值,便于排查问题
  • 禁止在POJO类中,同时存在对应属性Xxx的isXxx()getXxx() 方法
    • 框架在调用属性Xxx的获取方法时,不能确定哪个方法一定是被优先调用到的
  • 使用索引访问用String的split方法得到的数组时,需要做最后一个分隔符后有无内容的检查, 否则会有IndexOutofBoundsException异常
  • 当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起,便于阅读
  • 类内方法定义的顺序依次为:
    • 公有方法或者保护方法
      • 公有方法是类调用者或者维护最频繁使用的方法,最好首先展示
      • 保护方法尽管是子类需要的方法,但也可能是模板设计模式中的核心方法
    • 私有方法
      • 私有方法外部一般不需要关心,是一个黑盒实现
    • getter或者setter方法
      • 所有Service和DAO的getter或者setter方法都放在类的最后
  • setter方法中,参数名称要和类成员变量名称一致 ,this.成员名=参数名.
  • 在getter或者setter方法中,不要增加业务逻辑
  • 循环体内,字符串的类连接方式,使用StringBuilderappend方法进行扩展
    • 否则会导致每次循环都会new一个新的StringBuilder对象
    • 然后再进行append操作
    • 最后通过toString方法返回String对象,造成资源浪费
  • final可以声明类,成员变量,方法,以及本地变量. 使用final的情况:
    • 不允许被继承的类
      • String
    • 不允许修改的引用的域对象
    • 不允许被重写的方法
      • POJO中的setter方法
    • 不允许运行过程中重新赋值的局部变量
    • 避免上下文重复使用一个变量,使用final描述可以强制重新定义,方便更好地进行重构
  • 不要使用Objectclone方法拷贝对象:
    • 对象的clone方法默认是浅拷贝
    • 若想实现深度拷贝需要重写clone方法实现域对象的深度遍历拷贝需要重写clone方法实现域对象的深度遍历拷贝
  • 类成员与方法访问控制规约:
    • 如果不允许外部直接通过new来创建对象,那么构造方法必须是private
    • 工具类不允许有public或者default构造方法
    • 类非static成员变量并且与子成员共享,必须是protected
    • 类非static成员变量并且仅在本类中使用,必须是private
    • 类static成员变量如果仅在本类中使用,必须是private
    • 若是static成员变量,考虑是否为final
    • 类成员方法只供类内部调用时,必须是private
    • 类成员方法只对继承类公开时,限制使用protected

集合处理

  • hashCode和equals的处理:
    • 只要重写equals, 就必须重写hashCode
    • Set中存储的是不重复的对象,依据hashCodeequals进行判断,所以Set存储的对象必须重写这两个方法
    • 如果自定义对象作为Map的键,必须重写hashCodeequals
      • String重写了hashCodeequals方法所以可以使用String对象作为key来使用
  • ArrayList的subList结果不可以强转成ArrayList,否则会抛出ClassCastException异常:
    • subList返回的是ArrayList的内部类SubList, 并不是ArrayList, 而是ArrayList的一个视图.对于SubList子列表的所有操作最终会反映到原列表上
  • 在subList场景中,要注意对原集合元素的增加或者删除,都会导致子列表的遍历,增加和删除产生ConcurrentModificationException异常
  • 使用集合转数组的方法,必须使用集合的 toArrary(T[] array), 传入的是类型完全一样的数组,数组的大小就是list.size()
    • 使用toArray带参方法,入参分配的数组空间不够大时,toArray方法内部将重新分配内存空间,并返回新数组的地址;
    • 如果数组元素个数大于实际所需,下标为[list.size()] 的元素的数组元素将被置为null,其余数组元素保持原值
    • 因此最好将方法入参数组大小定义为与集合元素个数一致
List<String> list = new ArrayList<>();
list.add("guan");
list.add("bao");
String[] array = new String[list.size];
array = list.toArray(array);
  • 使用工具类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对象加锁
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
	String item = iterator.next();
	if (condition) {
		iterator.remove();
	}
}
  • JDK 7以后的版本中 ,Comparator实现要满足三个条件,否则Arrays.sort, Collections.sort会出现IllegalArgumentException异常:
    • x, y的比较结果和y, x的比较结果相反
    • x > y, y > z, 则 x > z
    • x = y, 则x, z比较结果和y, z比较结果相同
  • JDK 7以后的版本中,给集合的泛型定义时,使用全省略,即直接使用 <> 来指定前边已经指定的类型
  • 集合初始化时,指定集合初始值大小
    • HashMap使用HashMap(int initialCapacity) 初始化
    • initalCapacity = (需要存储的元素个数 / 负载因子) + 1. 注意负载因子(即loader factor)默认为0.75,如果暂时无法确定初始值的大小,设为为默认值16
  • 使用entrySet遍历Map类集合kv, 而不是使用keySet方式进行遍历
    • 如果使用keySet方式遍历,其实是遍历了两次:
      • 一次转换为Iterator对象
      • 一次从hashMap中取出key所对应的value
    • entrySet只是遍历一次就把keyvalue都放到了entry中,效率更高
    • 如果是JDK 8以后的版本,使用Map.foreach方法
    • 示例:
      • values()返回的是V值集合,是一个list集合对象
      • keySet()返回的是K值集合,是一个Set集合对象
      • entrySet()返回的是K-V值组合集合
  • 要注意Map类集合中的K-V能不能存储null值的情况:
集合类 Key Value Super 说明
Hashtable 不允许为null 不允许为null Dictionary 线程安全
ConcurrentHashMap 不允许为null 不允许为null AbstractMap 锁分段技术
TreeMap 不允许为null 允许为null AbstractMap 线程不安全
HashMap 允许为null 允许为null AbstractMap 线程不安全

由于HashMap的干扰,误以为ConcurrentHashMap可以置入null值,其实这样会抛出NPE异常

  • 合理利用集合的有序型 - sort和集合的稳定性 - order, 避免集合的无序性 - unsort和不稳定性 - unorder带来的负面影响
    • 有序性是指遍历的结果按照某种比较规则依次排列的
    • 稳定性是指集合每次遍历的元素次序是一定的
    • ArrayList, HashMap, TreeSet
  • 利用Set元素唯一的特性,可以快速对一个集合进行去重操作
    • 避免使用List的contains方法进行遍历,对比,去重操作

并发处理

  • 获取单例对象需要保证线程安全,其中的方法也要保证线程安全
    • 资源驱动类, 工具类, 单例工厂类都需要注意
  • 创建线程或者线程池时要指定有意义的线程名称,方便出错时回溯
  • 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
    • 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题
    • 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者过度切换的问题
  • 线程池不允许使用Executors创建,要通过ThreadPoolExecutors创建,这样可以让人更加明确线程池的运行规则,规避资源消耗的风险
    • Executors返回线程池对象存在以下问题:
      • FixedThreadPool和SingleThreadPool:
        • 允许请求队列长度为Integer.MAX_VALUE,可能会堆积大量请求,导致OOM
      • CachedThreadPool和ScheduledThreadPool:
        • 允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量线程,导致OOM
  • SimpleDateFormat是线程不安全类,不要定义为static变量.如果定义为static,必须加锁,或者使用DateUtils工具类
    • 注意线程安全,使用DateUtils,可以进行如下处理:
    private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
    	@Override
    	protected DateFormat initialValue() {
    		return new SimpleDateFormat("yyyy-MM-dd");
    	}
    }
    
    • 在JDK 8中,可以使用:
      • Instant 代替 Date
      • LocalDateTime 代替 Calendar
      • DateTimeFormatter 代替 SimpleDateFormat
  • 高并发时,同步调用应该考量锁的性能损耗.
    • 能用无锁数据结构,就不要用锁
    • 能用锁区块,就不要锁整个方法体
    • 能用对象锁,就不要用类锁
      • 尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用RPC方法
  • 对多个资源, 数据库表, 对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁
    • 如果线程一需要对A, B, C依次全部加锁后才可以进行更新操作
    • 那么线程二的加锁顺序也必须是A, B, C.否则可能会出现死锁
  • 并发修改同一记录时,避免更新丢失,需要加锁:
    • 在应用层加锁
    • 在缓存加锁
    • 在数据库中加锁
    • 使用version作为更新依据
      • 如果每次访问概率小于20%, 推荐使用乐观锁
      • 否则的话,使用悲观锁
      • 乐观锁的重试次数不得小于3
  • 多线程并行处理定时任务时:
    • Timer运行多个TimerTask时只要其中之一没有捕获抛出的异常,任务便会自动终止运行
    • 使用ScheduleExecutorService则没有这个问题
  • 使用CountDownLatch进行异步转同步操作:
    • 每个线程退出前必须调用countDown方法
    • 线程执行代码注意catch异常,确保counDown方法被执行到
    • 避免主线程无法执行至await方法,直到超时才返回结果
      • 子线程抛出的异常堆栈,不能在主线程try-catch得到异常
  • 避免Random实例被多线程使用,共享该实例是线程安全的,但是会因为竞争同一个seed导致性能下降
    • Random实例:
      • java.util.Random的实例
      • Math.random() 的方式
    • 在JDK 7后,可以直接使用ThreadLoalRandom
  • 在并发的场景下,通过双重检查锁double-check locking实现延迟初始化来优化问题隐患:
    • 将目标属性声明为volatile
  • volatile用于解决多线程内存不可见问题:
    • 对于一写多读,可以解决变量同步问题
    • 对于多写,无法解决线程安全问题
    • 对于count++操作,使用如下的类实现:
    AtomicInteger count = new AtomicInteger();
    count.addAndGet(1);
    
    • 在JDK 8后,推荐使用LongAdder对象,比AtomicLong性能更好,因为可以减少乐观锁的重试次数
  • HashMap在容量不够进行resize操作时会由于高并发可能出现死锁,导致CPU增加:
    • 使用其它的数据结构
    • 加锁
  • ThreadLocal无法解决共享对象的更新问题,建议要使用static进行修饰:
    • 这个变量是针对一个线程内所有操作共享的
    • 因此设置为静态变量,所有的此类实例共享此静态变量
    • 即这个变量在类第一次被使用时装载,只分配一块内存空间,只要这个线程内定义的所有此类的对象都可以操作这个变量

控制语句

  • 在一个switch块内:
    • 每个case要通过break或者return来终止
    • 或者注释说明程序将继续执行到哪一个case为止
    • 必须包含一个default语句并且放在最后,即使是空代码
  • if, else, for, while, do语句中必须使用大括号,即使只有一行代码,避免采用单行编码模式
  • 在高并发的场景中,避免使用 “等于” 判断作为中断或者退出的条件
    • 因为如果并发控制没有处理好,容易产生等值判断被 “击穿” 的情况 .要使用大于或者小于区间判断条件来代替
    • 示例: 判断剩余数量等于0时,当数量等于0的过程中,由于并发处理错误导致数量瞬间变成了负数,这样的话,处理无法终止
  • 表达异常的分支时,不要使用if - else方式,改写为
if (condition) {
	...
	return obj;
}
// 然后写else的业务处理逻辑

对于超过3层的if - else的逻辑判断代码可以使用卫语句,策略模式,状态模式等实现

  • 除常用的方法**:getXxx, isXxx**等,不要在条件判断中执行复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性
    • 很多if语句内的逻辑相当复杂,需要分析表达式的最终结果,才能明确什么样的条件执行什么样的语句
  • 循环体中的语句要考量性能,以下操作尽量移动至循环体外处理:
    • 定义对象,变量
    • 获取数据库连接
    • 进行不必要的try - catch操作(考虑这个try - catch操作是否可以移动至循环体外)
  • 避免使用取反逻辑运算符
    • 取反逻辑运算符不利于快速理解
    • 取反逻辑写法必然存在对应的正向逻辑写法
  • 接口入参保护: 这种场景常见的是用作批量操作的接口
  • 参数校验:
    • 需要进行参数校验的情形:
      • 调用频次低的方法
      • 执行时间开销很大的方法
        • 此情形中,参数校验的时间几乎可以忽略不计
        • 但是如果因为参数错误导致中间执行被退回,或者错误,就得不偿失
      • 需要极高稳定性和可用性的方法
      • 对外提供开放接口,无论是 RPC, API, HTTP接口
      • 敏感权限入口
    • 不需要进行参数校验的情形:
      • 极有可能被循环调用的方法. 但是在方法说明里必须注明外部参数的检查要求
      • 底层调用频度比较高的方法
      • 被声明成private只会被自己代码所调用的方法.如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数

注释规约

  • 类, 类属性, 类方法的注释必须使用Javadoc规范,使用/** xxx */格式,不允许使用// xxx方式
  • 所有抽象方法, 包括接口中的方法, 都必须使用Javadoc注释,除了返回值, 参数, 异常说明外,还必须指出该方法做了什么事情,实现什么功能. 对子类的实现要求以及调用的注意事项需要一并说明
  • 所有的类都必须添加创建者和创建日期
  • 方法内部注释:
    • 单行注释: 在被注释语句上方另起一行,使用 // 注释
    • 多行注释: 使用 /* */ 注释,注意与代码对齐
  • 所有枚举类型字段必须要有注释,说明每个数据项的用途
  • 当水平足够高时,应当使用英文注释. 否则就用中文把问题说清楚,只要将专有名词关键字保持英文原文即可
  • 代码修改的同时,注释也要进行相应的修改,尤其是参数, 返回值, 异常, 核心逻辑等. 要保持代码与注释更新同步
  • 谨慎注释代码:
    • 注释的代码要进行详细的说明,而不是简单的注释
    • 如果无用,则应该删除
  • 注释的要求:
    • 能够准确反映设计思想和代码逻辑
    • 能够描述业务含义,能够迅速了解到代码背后的信息
  • 好的命名,代码结构是自解释的,注释保证精简准确,表达到位
  • 特殊的注释标记,需要注明标记人与标记时间.注意及时处理这些标记,通过标记扫描,经常清理此类标记.线上故障有时候就源于这些标记处的代码
    • 待办事宜TODO : (标记人, 标记时间, [预处理时间])
      • 表示要实现,但目前尚未实现的功能.这实际上是一个Javadoc的标签.只能应用于类, 接口, 方法
    • 错误,不能工作FIXME : (标记人, 标记时间, [预处理时间])
      • 在注释中用FIXME标记某段代码是错误的,而且不能工作,需要及时纠正情况

其它注意

  • 在使用正则表达式时, 利用好预编译功能,可以有效加快正则匹配速度
  • 不要在方法体内定义
  • velocity调用POJO类的属性时,直接使用属性名取值即可,模板引擎会自动按规范调用POJO的getXxx(), 如果是boolean基本类型变量 ,boolean命名不要加is前缀, 会自动调用isXxx方法.如果是Boolean包装类对象,优先调用getXxx() 方法
  • 后台输送给页面变量必须加上 $ ! {var},注意中间的感叹号
    • 如果var等于null或者不存在,那么${var}会直接显示在桌面上
  • 注意Math.random() 这个方法返回是double类型,取值范围0 <= x <1(能够取到零值,注意除零)
    • 如果获取整数类型的随机数,不需要将x放大10的若干倍然后取整,直接使用Random对象的nextInt或者nextLong方法
  • 获取当前秒数System.currentTimeMillis(), 不是使用new Date().getTime()
    • 如果想获取更加精确的纳秒级时间值,使用System.nanoTime() 的方式
    • 在JDK 8以后,针对统计时间等常景,需要使用Instant
  • 不要在视图模版中加入任何复杂逻辑,根据MVC理论,视图的职责是展示,不要有模型和控制器的代码逻辑
  • 任何数据结构的构造和初始化,都应指定大小,避免数据结构无限增长吃光内存
  • 及时清理不再使用的代码段或配置信息
    • 对于垃圾代码或过时配置,坚决清理干净,避免程序过度臃肿,代码冗余
    • 对于暂时被注释掉,后续可能恢复使用的代码片段,在注释代码的上方,统一规定使用三个斜杠///来说明注视掉代码的理由
发布了127 篇原创文章 · 获赞 109 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/JewaveOxford/article/details/103485946