手记

阿里巴巴2017Java编程规范个人总结(一)

【编程规约】
命名规约
  • 所有命名不能以美元符和下划线开始和结尾
  • 禁止拼音加英文
  • 类用UserDao..方法,变量,参数用userDao
  • 常量名全部大写,下划线隔开
  • 抽象类使用Abstract或者Base开头,异常类使用Exception结尾
  • boolean属性命名不能使用is开头
  • 如果使用到了设计模式,建议在类名中体现出具体模式。LoginProxy
  • 接口类中方法不加权限修饰符
  • 接口和实现类 实现类以Impl结尾
  • MVC各层命名规约
    • Service/Dao层
      • 获取单个对象 get 前缀
      • 获取多个对象 list 前缀
      • 获取统计值的方法 count 前缀
      • 插入 save 前缀
      • 删除 remove
      • 修改 update
    • 领域模型
      • 数据对象 DO 是数据表名
      • 数据传输对象 DTO 是业务领域相关的名称
      • 展示对象 VO 是网页名称 使用了模板框架
      • POJO是 DO DTO BO VO 统称
常量定义
  • 不允许出现魔法值,未经定义的常量突然出现
  • Long类型使用大写L结尾
  • 常量类存放常量要分门别类放置
  • 常量的复用层次的安排
    • 跨应用共享常量 :二方库中 通常是client.jar中的constant目录下
    • 应用内共享常量 :一方库的modules中的constant
    • 子工程内共享常量 :当前子工程的constant目录下
    • 包内共享常量:当前包下单独的constant目录下
    • 类内共享常量:直接定义 private static final
格式规约
  • 小括号,左右括号前后之间没有空格
  • if/for/while/switch/do 后加空格: 例如 :if ()
  • 任何运算符前后都必须加空格
  • 缩进采用4个空格,而不是tab字符,IDE要调整一下
  • 单行字符不超过120个,超出需换行 第二行比第一行缩进4个空格,第三行以后就和第二行平齐就可以了
    • 运算符与下文一起换行
    • 方法调用的点符号与下文一起换行
    • 调用方法 多个参数,超长,需逗号后进行换行
    • 括号之前不要换行
  • 方法参数在定义和传入时,多个参数逗号后边必须加空格
  • 编码统一采用UTF-8 IDE中换行符采用unix格式
  • 方法体内执行语句组,变量的定义语句组,不同的业务逻辑之间或者不同的语义之间插入一个空行,相同业务逻辑和语义之间不需要插入空行
OOP规约
  • 避免使用对象来引用类的静态变量或方法
  • 所有的覆写方法,必须加注解 @Override
  • 相同参数类型,相同业务含义,才可以使用Java的可变参数避免使用Object
  • 对外暴露的接口签名,原则上不允许修改方法签名,避免对接口调用方法产生影响,接口过时必须加@Deprecated注解,并说明采用的新接口或新服务是什么
  • 不能使用过时的类或方法
  • 使用 "t".equals(test)方式,推荐使用java.util.Object#equals (jdk7引入的工具类)
  • 所有的相同类型的包装类对象之间的值比较,全部使用equals方法,
    • 注意: Integer对象 使用==也可以,但是会产生很多内存浪费
  • 所有的POJO类属性必须使用包装数据类型
    • RPC方法的返回值和参数必须使用包装数据类型
    • 所有的局部变量 推荐使用基本数据类型
  • 定义 DO/DTO/VO等POJO类时,不要设定任何属性的默认值
  • 序列化类新增属性时,不要修改serialVersionUID字段,避免反序列化失败,如果完全不兼容升级,避免反序列化混乱,可以修改serialVersionUID的值
  • 构造方法不能有任何的业务逻辑,只是用来初始化对象的
  • POJO类每一个都必须重写 toString方法,方便之后debug
  • 使用索引访问用String 的split 方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有下标超出的异常
  • 当一个类有多个构造方法,或者多个重名方法,这些方法应该按顺序放置在一起
  • 类内方法的定义顺序依次是 共有方法或保护方法 -> 私有方法 -> setter/getter方法
  • setter/getter方法中也不要加入业务逻辑
  • 循环体中的字符串的连接方式,使用StringBuffer的append方法进行扩展
  • final可提高程序响应效率,声明成final的情况:
    • 不需要重新赋值的变量,包括类属性,局部变量
    • 对象参数前加final,表示不允许修改引用的指向
    • 类方法确定不允许被重写
  • 慎用Object的clone方法来拷贝对象,最好重写该方法,实现属性的拷贝
  • 类成员与方法访问控制从严:
    • 如果不允许外部直接通过new来创建对象,那么构造方法显式声明并private
    • 工具类不允许有public或default构造方法
    • 类非static成员变量并且与子类共享,必须是protexted
    • 类非static成员变量并且仅在本类中使用,必须是private
    • 若是static成员变量,必须考虑是否final
    • 类成员方法只供类内部调用,必须是private
    • 类成员方法只对继承类公开,那么限制为protected
    • 注意 访问范围的严格控制利于对模块解耦,如果删除一个private方法,删除就删除,但是如果是public方法就得手心冒汗的删除了
集合处理
  • 关于HashCode 和equals的处理
    • 只要重写equals,就必须重写HashCode
    • 因为Set存储的是不重复的对象,依据HashCode和equals进行判断,所以Set存储的方法必须重写这两个方法
    • 如果自定义对象作为Map的键,那么必须重写HashCode和equals
  • ArrayList的subList结果不可强转成ArrayList,否则会抛出类型强转的错误
    • subList返回的是ArrayList的内部类,是ArrayList的一个视图,对于subList的所有操作最终都会反映到原列表上
  • 在 subList 场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、删除均产生 ConcurrentModificationException 异常。
  • 使用集合转数组的方法,必须使用集合的 toArray(T[] array) ,传入的是类型完全一样的数组,大小就是 list . size()
    • String[] array = new String[list.size()];
    • array = list.toArray(array);
  • 把数组转换成集合:使用工具类 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) 也会随之修改。
  • 泛型通配符<? extends T >来接收返回的数据,此写法的泛型集合不能使用 add 方法。
    • 说明:苹果装箱后返回一个<? extends Fruits >对象,此对象就不能往里加任何水果,包括苹果。
  • 不要在 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);
         }
   }
  • 在 JDK 7 版本以上, Comparator 要满足自反性,传递性,对称性,不然 Arrays . sort ,Collections . sort 会报 IllegalArgumentException 异常。
    • 1 ) 自反性: x , y 的比较结果和 y , x 的比较结果相反。
    • 2 ) 传递性: x > y , y > z ,则 x > z 。
    • 3 ) 对称性: x = y ,则 x , z 比较结果和 y , z 比较结果相同。
  • 集合初始化时,尽量指定集合初始值大小。
  • 使用 entrySet 遍历 Map 类集合 KV ,而不是 keySet 方式进行遍历
    • keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出key 所对应的 value 。
    • 而 entrySet 只是遍历了一次就把 key 和 value 都放到了 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 线程不安全
  • 合理利用好集合的有序性 (sort) 和稳定性 (order) ,避免集合的无序性 (unsort) 和不稳定性 (unorder) 带来的负面影响。
    • 稳定性指集合每次遍历的元素次序是一定的。
    • 有序性是指遍历的结果是按某种比较规则依次排列的。
    • 如: ArrayList 是 order / unsort;HashMap 是 unorder / unsort;TreeSet 是order / sort 。
  • 利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的contains 方法进行遍历、对比、去重操作。
并发处理
  • 获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
    • 资源驱动类、工具类、单例工厂类都需要注意。
  • 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
  • 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
  • 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
    • 1) FixedThreadPool 和 SingleThreadPool :
    • 允许的请求队列长度为 Integer.MAX_VALUE ,可能会堆积大量的请求,从而导致 OOM 。
    • 2) CachedThreadPool 和 ScheduledThreadPool :
    • 允许的创建线程数量为 Integer.MAX_VALUE ,可能会创建大量的线程,从而导致 OOM 。
  • SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为static ,必须加锁,或者使用 DateUtils 工具类。
    • 如果是 JDK 8 的应用,可以使用 Instant 代替 Date , LocalDateTime 代替 Calendar ,DateTimeFormatter 代替 Simpledateformatter ,
    • 官方给出的解释: simple beautiful strongimmutable thread - safe 。
  • 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁 ; 能锁区块,就不要锁整个方法体 ; 能用对象锁,就不要用类锁。
  • 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。
    • 线程一需要对表 A 、 B 、 C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A 、 B 、 C ,否则可能出现死锁。
  • 并发修改同一记录时,避免更新丢失,要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。
    • 如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次。
  • 多线程并行处理定时任务时, Timer 运行多个 TimeTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用 ScheduledExecutorService 则没有这个问题。
  • 使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown方法,线程执行代码注意 catch 异常,确保 countDown 方法可以执行,避免主线程无法执行至 countDown 方法,直到超时才返回结果。
    • 注意,子线程抛出异常堆栈,不能在主线程 try - catch 到。
  • 避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一seed 导致的性能下降。
    • Random 实例包括 java . util . Random 的实例或者 Math . random() 实例。
  • 通过双重检查锁 (double - checked locking)( 在并发场景 ) 实现延迟初始化的优化问题隐患
    • ( 可参考 The " Double - Checked Locking is Broken " Declaration) ,推荐问题解决方案中较为简单一种 ( 适用于 JDK 5 及以上版本 ) ,将目标属性声明为 volatile 型 。
  • volatile 解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。如果是 count ++操作,使用如下类实现:
    • AtomicInteger count = new AtomicInteger(); count . addAndGet( 1 );
    • 如果是 JDK 8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好 ( 减少乐观锁的重试次数 ) 。
  • HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升,在开发过程中注意规避此风险。
  • ThreadLocal 无法解决共享对象的更新问题, ThreadLocal 对象建议使用 static修饰。这个变量是针对一个线程内所有操作共有的,所以设置为静态变量,
    • 所有此类实例共享此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象 ( 只要是这个线程内定义的 ) 都可以操控这个变量。
控制语句
  • 在一个 switch 块内,每个 case 要么通过 break / return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止 ;
    • 在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有。
  • 在 if / else / for / while / do 语句中必须使用大括号,即使只有一行代码
  • 推荐尽量少用 else , if - else 的方式可以改写成:
    • 逻辑上超过 3 层的 if-else 代码可以使用卫语句,或者状态模式来实现
  • 除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
    • boolean existed = (file.open(fileName, "w") != null) && (...) || (...);if (existed) {}
  • 循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的 try - catch 操作 ( 这个 try - catch 是否可以移至循环体外) 。
  • 接口入参保护,这种场景常见的是用于做批量操作的接口。
  • 方法中需要进行参数校验的场景:
    • 1 ) 调用频次低的方法。
    • 2 ) 执行时间开销很大的方法,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退,或者错误,那得不偿失。
    • 3 ) 需要极高稳定性和可用性的方法。
    • 4 ) 对外提供的开放接口,不管是 RPC / API / HTTP 接口。
    • 5) 敏感权限入口。
  • 方法中不需要参数校验的场景:
    • 1 ) 极有可能被循环调用的方法,不建议对参数进行校验。但在方法说明里必须注明外部参数检查。
    • 2 ) 底层的方法调用频度都比较高,一般不校验。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。
      • 一般 DAO 层与 Service层都在同一个应用中,部署在同一台服务器中,所以 DAO 的参数校验,可以省略。
    • 3 ) 被声明成 private 只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数。
注释规约
  • 类、类属性、类方法的注释必须使用 Javadoc 规范,使用/* 内容 /格式,不得使用 // xxx 方式。
  • 所有的抽象方法 ( 包括接口中的方法 ) 必须要用 Javadoc 注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。
  • 所有的类都必须添加创建者信息。
  • 方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使用/ /注释,注意与代码对齐。
  • 所有的枚举类型字段必须要有注释,说明每个数据项的用途。
  • 与其“半吊子”英文来注释,不如用中文注释把问题说清楚。专有名词与关键字保持英文原文即可。
  • 代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。
    • 代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义。
  • 注释掉的代码尽量要配合说明,而不是简单的注释掉。
    • 代码被注释掉有两种可能性:
      • 1 ) 后续会恢复此段代码逻辑。
      • 2 ) 永久不用。
    • 情况(1)如果没有备注信息,难以知晓注释动机。(2)建议直接删掉 ( 代码仓库保存了历史代码 ) 。
  • 对于注释的要求:
    • 第一、能够准确反应设计思想和代码逻辑 ;
    • 第二、能够描述业务含义,使别的程序员能够迅速了解到代码背后的信息。
    • 完全没有注释的大段代码对于阅读者形同天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路 ;
    • 注释也是给继任者看的,使其能够快速接替自己的工作。
  • 好的命名、代码结构是自解释的,注释力求精简准确、表达到位。避免出现注释的一个极端:过多过滥的注释,代码的逻辑一旦修改,修改注释是相当大的负担。
  • 特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描,经常清理此类标记。 线上故障有时候就是来源于这些标记处的代码。
    • 1 ) 待办事宜 (TODO) : ( 标记人,标记时间, [ 预计处理时间 ])
      • 表示需要实现,但目前还未实现的功能。这实际上是一个 Javadoc 的标签,目前的 Javadoc还没有实现,但已经被广泛使用。只能应用于类,接口和方法 ( 因为它是一个 Javadoc 标签 ) 。
    • 2 ) 错误,不能工作 (FIXME) : ( 标记人,标记时间, [ 预计处理时间 ])
      • 在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正的情况。
其他
  • 在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。
    • 不要在方法体内定义: Pattern pattern = Pattern . compile( 规则 );
  • 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 类。
  • 尽量不要在 vm 中加入变量声明、逻辑运算符,更不要在 vm 模板中加入任何复杂的逻辑。
  • 任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存。
  • 对于“明确停止使用的代码和配置”,如方法、变量、类、配置文件、动态配置属性等要坚决从程序中清理出去,避免造成过多垃圾。
2人推荐
随时随地看视频
慕课网APP