一位攻城狮的自我修养,在于良好的编程规范

Posted 攻城狮Chova

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一位攻城狮的自我修养,在于良好的编程规范相关的知识,希望对你有一定的参考价值。

命名风格

  • 类名使用UpperCamelCase风格,但下列情形除外:

    • DO: Data Object. 与数据库表结构一一对应,通过DAO层向上传输数据源对象
    • BO: Business Object,业务对象. 由Service层输出的封装业务逻辑的对象
    • DTO: Data Transfer Object,数据传输对象. Service或Manager向外传输的对象
    • VO: View Object,显示对象. 通常是Web向模板渲染引擎层传输的对象
    • AO: Application Object,应用对象. 在Web层与Service层之间抽象复用的对象模型
    • PO: POJO缩写,Plain Ordinary Java Object. 专指只有setter/getter/toString的简单类,包括DO,DTO,BO,VO等, 禁止使用xxxPOJO来命名
    • UID
  • 方法名,参数名,成员变量,局部变量都统一使用lowerCamelcase风格
  • 常量命名全部大写,单词间用下划线隔开, 力求语义表达完整清楚,不要嫌名字长
  • 抽象类命名使用Abstract或者Base开头
  • 异常类命名使用Exception结尾
  • 测试类命名要以要测试的类的名称命名,以Test结尾
  • 类型与中括号紧挨来表示数组
  • POJO类中布尔类型的变量都不要加is前缀,在部分框架中会引起序列化错误
  • 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词.包名统一使用单数形式.但是类名如果有复数含义,可以使用复数形式
  • 杜绝不规范的缩写,避免望文不知义
  • 为了达到代码自解释的目标,任何自定义的编程元素在命名时,使用尽量完整的单词组合来表达含义
  • 在常量与变量命名的同时,表示类型的名词放在词尾,以提升辨识度
  • 如果模块,接口,类,方法使用了设计模式,在命名时需要体现出设计模式
  • 接口类中的方法和属性不要加任何修饰符号(不要加public), 保持代码的简洁
  • 尽量不要在接口中定义变量,如果一定要定义变量,一定是与接口方法有关的,并且是整个应用的基础变量

    • 接口方法签名: void commit()
    • 接口基础常量: String COMPANY="Oxford"
  • 接口和实现类:

    • 对于Service和DAO类,基于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个空格进行缩进
  • 注释的双斜线与注释内容之间有且仅有一个空格
  • 方法参数在定义和传入时,多个参数逗号后面必须加空格
  • 单个方法的总行数不要超过80行:

    • 除注释之外的方法签名,左右大括号,方法内代码,空行,回车及任何不可见字符的总行数不超过80行
    • 代码逻辑分清红花和绿叶,个性和共性:

      • 绿叶逻辑单独出来成为额外的方法,使主干代码更加清晰
      • 共性逻辑抽取成共性方法,便于复用和维护
  • 不需要增加若干空格来使某一行的字符与上一行对应位置的字符对齐
  • 不同逻辑,不同语义,不同业务代码之间只需要插入一个空行分割来提升可读性即可

    OPP规约

  • 避免通过一个类的对象引用访问类的静态变量和静态方法,这会增加编译器的解析成本,直接使用类名访问即可
  • 所有的覆写方法,必须加 @Override
  • 相同参数类型,相同业务含义,才可以使用Java的可变参数,避免可变参数使用Object类型

    • 可变参数必须放置在参数列表的最后, 建议尽量不要用可变参数编程
  • 外部正在调用的或者二方库依赖的接口,不允许修改方法签名(方法名和参数列表),避免对接口的调用方产生影响 .接口过时必须加上 ==@Deprecated== 注解,并清晰地说明采用的新接口和新服务是什么
  • 不能使用过时的类或方法:

    • 接口的提供方既然明确是过时接口,那么有义务提供新接口
    • 作为调用方,有义务考证过时方法的新实现是什么
  • Object的equals方法容易抛出空指针异常,应使用常量或者确定有值的对象来调用equals

    • "test".equals(Object)
    • 推荐使用java.util.objects
  • 所有相同类型的包装类对象之间的值的比较,全部使用equals方法比较

    • 对于 Integer var = ? 在-128至127范围内赋值时 ,Integer对象是在IntegerCache.cache中产生,会复用已有对象,这个区间内的Integer值可以直接使用 == 进行判断
    • 但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,所以推荐使用equals方法进行比较
  • 任何货币金额,均以最小货币单位且整型类型来进行存储
  • 浮点数之间的等值判断:

    • 基本类型不能用 == 来比较
    • 包装类型不能使用equals来判断

      • 浮点数采用尾数+阶码的编码方式,类似与科学计数法有效数字+指数的表示方式. 二进制无法精确表示大部分十进制小数
    • 为了避免出现问题,所有的浮点数都使用BigDecimal定义

      /*
       * float类型的浮点数:
       *         指定一个误差范围,
       *         两个浮点数的差值在此范围之内,
       *         则认为是相等的.
       */
       float a = 1.0f - 0.9f;
       float b = 0.9f - 0.8f;
       float diff = 1e - 6f;
       if (Math.abs(a-b) < diff) {
       System.out.println("true");
       }
      
      /* 
       *  使用BigDecimal来定义值,再进行浮点数的运算操作
       */
       BigDecimal a = new BigDecimal("1.0");
       BigDecimal b = new BigDecimal("0.9");
       BigDecimal c = new BigDecimal("0.8");
       BigDecimal x = a.substract(b); 
       BigDecimal y = b.substract(c);
       if (x.equals(y)) {
       System.out.println("true");
       } 
  • 定义数据对象DO类时,属性类型要与数据库字段类型相匹配

    • 数据库字段的bigint必须与类属性Long类型相对应
  • 禁止使用构造方法BigDecimal(double) 的方式将double值转化为BigDecimal对象:

    • BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中会导致业务逻辑异常

      • 推荐使用入参为String的构造方法
      • 或者使用BigDecimal的valueOf方法: 此方法内部执行了Double的toString,实际能表达的精度对尾数进行了截断

        BigDecimal a = new BigDecimal("0.1");
        BigDecimal b = BigDecimal.valueOf(0.1);
  • 基本类型和包装类型的使用标准:

    • 所有的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方法中,不要增加业务逻辑
  • 循环体内,字符串的类连接方式,使用StringBuilder的append方法进行扩展

    • 否则会导致每次循环都会new一个新的StringBuilder对象
    • 然后再进行append操作
    • 最后通过toString方法返回String对象,造成资源浪费
  • final可以声明类,成员变量,方法,以及本地变量. 使用final的情况:

    • 不允许被继承的类

      • String
    • 不允许修改的引用的域对象
    • 不允许被重写的方法

      • POJO中的setter方法
    • 不允许运行过程中重新赋值的局部变量
    • 避免上下文重复使用一个变量,使用final描述可以强制重新定义,方便更好地进行重构
  • 不要使用Object的clone方法拷贝对象:

    • 对象的clone方法默认是浅拷贝
    • 若想实现深度拷贝需要重写clone方法实现域对象的深度遍历拷贝需要重写clone方法实现域对象的深度遍历拷贝
  • 类成员与方法访问控制规约:

    • 如果不允许外部直接通过new来创建对象,那么构造方法必须是private
    • 工具类不允许有public或者default构造方法
    • 类非static成员变量并且与子成员共享,必须是protected
    • 类非static成员变量并且仅在本类中使用,必须是private
    • 类static成员变量如果仅在本类中使用,必须是private
    • 若是static成员变量,考虑是否为final
    • 类成员方法只供类内部调用时,必须是private
    • 类成员方法只对继承类公开时,限制使用protected

      日期时间

  • 日期格式化时,传入pattern中表示年份统一使用小写的yyyy

    • 日期格式化时:

      • yyyy表示当天所在的年
      • YYYY表示当天所在的周属于的年份,一周从周日开始,至周六结束.如果本周跨年,返回的YYYY就是下一年
  • 在日期格式中分清楚大写的M和小写的m,大写的H和小写的h的含义:

    • 表示月份的是大写的M
    • 表示分钟的是小写的m
    • 24小时的是大写的H
    • 12小时的是小写的h
  • 获取当前的毫秒数 :System.currentTimeMillis()

    • 如果想要获取更加精确的纳秒级的时间值,使用System.nanoTime()
    • 针对统计时间的场景,推荐使用Instant
  • 不要使用java.sql中的相关时间方法
  • 不要在程序中写死一年的为365,避免在公历闰年时出现日期转换错误或程序逻辑错误

    • 使用LocalDate方法

      // 获取今年的天数
      int daysOfThisYear = LocalDate.now().lengthOfYear();
      
      // 获取指定某年的天数
      LocalDate.of(2011, 1, 1).lengthOfYear();
  • 使用Calendar中的枚举值来指代月份

    • 如果使用数字,要注意Date,Calendar等日期相关类的月份month的值在0 - 11之间

      集合处理

  • hashCode和equals的处理:

    • 只要重写equals, 就必须重写hashCode
    • Set中存储的是不重复的对象,依据hashCode和equals进行判断,所以Set存储的对象必须重写这两个方法
    • 如果自定义对象作为Map的键,必须重写hashCode和equals

      • String重写了hashCode和equals方法所以可以使用String对象作为key来使用
  • ArrayList的subList结果不可以强转成ArrayList,否则会抛出ClassCastException异常:

    • subList返回的是ArrayList的内部类SubList, 并不是ArrayList, 而是ArrayList的一个视图.对于SubList子列表的所有操作最终会反映到原列表上
  • 在subList场景中,要注意对原集合元素的增加或者删除,都会导致子列表的遍历,增加和删除产生ConcurrentModificationException异常
  • 使用Map的方法:

    • keySet()
    • values()
    • entrySet()
    • 返回集合对象时,不可以进行添加元素的操作,否则会抛出UnsupportedOperationException异常
  • Collections类返回的对象不可以进行添加或者删除操作:

    • 如果查询无结果,则返回Collection.emptyList()空集合对象. 调用方一旦进行了添加元素操作,就会触发UnsupportedOperationException异常
  • 使用集合转数组的方法,必须使用集合的 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);
  • 在使用Collection接口任何实现类的addAll() 方法时,都要对输入集合参数进行NPE判断
  • 使用工具类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只是遍历一次就把key和value都放到了entry中,效率更高
    • 如果是JDK 8以后的版本,使用Map.foreach方法
    • 示例:

      • values()返回的是V值集合,是一个list集合对象
      • keySet()返回的是K值集合,是一个Set集合对象
      • entrySet()返回的是K-V值组合集合
  • 要注意Map类集合中的K-V能不能存储null值的情况:
集合类KeyValueSuper说明
Hashtable不允许为null不允许为nullDictionary线程安全
ConcurrentHashMap不允许为null不允许为nullAbstractMap锁分段技术
TreeMap不允许为null允许为nullAbstractMap线程不安全
HashMap允许为null允许为nullAbstractMap线程不安全

由于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
  • 必须回收自定义的ThreadLocal变量:

    • 尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,会影响后续业务逻辑和造成内存泄漏的问题
    • 尽量在代理中使用try - finally块进行回收

      ObjectThreadLocal.set(userInfo);
      try {
      ...
      } finally {
      ObjectThreadLocal.remove();
      } 
  • 高并发时,同步调用应该考量锁的性能损耗.

    • 能用无锁数据结构,就不要用锁
    • 能用锁区块,就不要锁整个方法体
    • 能用对象锁,就不要用类锁

      • 尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用RPC方法
  • 对多个资源, 数据库表, 对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁

    • 如果线程一需要对A, B, C依次全部加锁后才可以进行更新操作
    • 那么线程二的加锁顺序也必须是A, B, C.否则可能会出现死锁
  • 在使用阻塞等待获取锁的方式中:

    • 必须在try代码块之外

      • 如果lock方法在try代码块之内,可能由于其它方法抛出异常 ,导致在finally代码块中 ,unlock对未加锁的对象解锁,会调用AQS的tryRelease方法,抛出IlleagalMonitorStateException异常
    • 必须在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁

      • 如果在lock方法与try代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法获取锁
    • 在Lock对象的lock方法实现中可能抛出unchecked异常,导致unlock对未加锁的对象解锁,会调用AQS的tryRelease方法,抛出IlleagalMonitorStateException异常

      Lock lock = new XxxLock();
      lock.lock();
      try {
      ...
      } finally {
      lock.unlock();
      }
  • 在使用尝试机制来获取锁的方式中:

    • 进入业务代码之前,必须先判断当前线程是否持有锁
    • 锁的释放规则与锁阻塞等待的方式相同

      • Lock对象的unlock方法在执行时,会调用AQS的tryRelease方法,如果当前线程不持有锁, 则抛出IllegalMonitorStateException异常

        Lock lock = new XxxLock();
        boolean isLocked = lock.tryLock();
        if (isLocked) {
        try {
          ...
        } finally {
          lock.unlock();
        }
        }
  • 并发修改同一记录时,避免更新丢失,需要加锁:

    • 在应用层加锁
    • 在缓存加锁
    • 在数据库中加锁
    • 使用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语句并且放在最后,即使是空代码
  • 当Switch括号内的变量类型为String并且此变量为外部参数时,必须进行null判断
  • 在 if, else, for, while, do语句中必须使用大括号,即使只有一行代码,避免采用单行编码模式
  • 三目运算符: condition ? 表达式1 : 表达式2 要注意表达式1和表达式2在类型对齐时,可能因自动拆箱导致NPE异常

    • 触发类型对齐的拆箱操作:

      • 表达式1或者表达式2只要有一个原始类型
      • 表达式1或者表达式2类型不一致,会强制拆箱升级成表示范围更大的那个类型
  • 在高并发的场景中,避免使用 “等于” 判断作为中断或者退出的条件

    • 因为如果并发控制没有处理好,容易产生等值判断被 “击穿” 的情况 .要使用大于或者小于区间判断条件来代替
    • 示例: 判断剩余数量等于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标记某段代码是错误的,而且不能工作,需要及时纠正情况

        前后分离

  • 前后端交互的API,需要明确协议,域名,路径,请求方法,请求内容,状态码,响应体:

    • 协议: 生产环境必须使用HTTPS
    • 路径: 每一个API需要对应一个路径,表示API具体的请求地址

      • 代表资源,只能为名词,推荐使用复数,不能为动词,因为请求方法已经表达动作含义
      • URL路径不能使用大写,单词如果需要分割,统一使用下划线
      • 路径禁止携带请求内容类型的后缀 : ".json",".xml", 通过accept头表达即可
    • 请求方法: 对具体操作的定义

      • GET: 获取
      • POST: 新增
      • PUT: 修改
      • DELETE: 删除
    • 请求内容:

      • URL带的参数必须无敏感信息或者符合安全要求
      • body里带参数时必须设置Content-Type
    • 响应体: 响应体body可以放置多种数据类型,由Content-Type头来确定
  • 前后端数据列表相关的接口返回时,如果为空,则返回空数组 [ ] 或者空集合 { }
  • 服务端发生错误时,返回给前端的响应信息必须包含HTTP状态码, errorCode, errorMessage, 用户提示信息四个部分:

    • HTTP状态码: 浏览器

      • 200 OK : 表明该请求被成功完成, 所请求的资源发送到客户端
      • 401 Unauthorized : 请求要求身份验证, 通常是需要登录而用户未登录的情况
      • 403 Forbidden : 服务器拒绝请求, 通常是机密信息或复制其余登录用户链接访问服务器的情况
      • 404 Not Found : 服务器无法取得所请求的网页. 请求的资源不存在
      • 500 Internal Server Error: 服务器内部错误
    • errorCode: 前端开发
    • errorMessage: 错误排查人员
    • 用户提示信息: 用户. 要求简短清晰,提示友好,引导用户进行下一步操作或者解释错误原因,上下文环境,推荐操作
  • errorMessage是前后端错误追踪机制的体现,可以在前端输出到 type="hidden" 的文字类控件或者用户端的日志中,这样能够快速地定位问题
  • 对于需要使用超大整数的场景,服务端一律使用String字符串返回类型,禁止使用Long类型

    • Java服务端如果直接返回Long整型数据给前端 ,JS会自动转换为Number类型:

      • Number类型: 双精度浮点数,表示原理和取值范围等同于Java中的Double
      • Long类型: 表示的最大值为2^63^ -1. 超过2^53^(9007199254740992) 的数值转化为JS的Number时,有些数值会有精度损失

        • 在Long取值范围内,任何2的指数次整数都是绝对不会存在精度损失的,所以说精度损失是一个概率问题
        • 如果浮点数尾数位与指数位空间不限,则可以精确表示任何整数.但是双精度浮点数的尾数位只有52位
    • 示例: 通常在订单号或者交易号大于等于16位,大概率会出现前后端单据不一致的情况. 比如后端的362909601374617692到前端则是362909601374617660
  • HTTP请求通过URL传递参数时,不能超过2048个字节:

    • 不同浏览器对于URL的最大长度限制略有不同,并且对超出最大长度的处理逻辑也有差异. 2048字节是取所有浏览器的最小值
  • HTTP请求通过body传递内容时,必须控制长度,超出最大长度后,后端解析会出错:

    • nginx默认限制是1MB
    • Tomcat默认限制是2MB
    • 当确实有业务需要传较大内容时,可以通过调大服务器端的限制
  • 在分页场景中,用户输入参数小于1, 则前端返回第一页参数给后端. 后端发现用户输入的参数大于总页数,直接返回最后一页
  • 服务器内部重定向必须使用forward. 外部重定向地址必须使用URL统一代理模块生成,否则会因为线上采用HTTPS协议而导致浏览器提示 "不安全", 并且还会带来URL维护不一致的问题
  • 服务器返回信息必须被标记是否可以缓存,如果缓存,客户端可能会重用之前的请求结果

    • 缓存有利于减少交互次数,减少交互的平均延迟
    • 示例: http 1.1中 ,s-maxage通知服务器进行缓存,时间单位为秒:

      • response.setHeader("Cache-Control", "s-maxage=" + cacheSeconds)
  • 服务端返回的数据,使用JSON格式而非XML :

    • HTTP支持使用不同的输出格式,例如纯文本,JSON,CSV,XML,RSS以至html
    • 在使用面向用户的服务,应该选择JSON作为通信中使用的标准数据交换格式,包括请求和响应

      • application/JSON是一种通用的MIME类型,具有实用,精简,易读的特点
  • 前后端的时间格式统一为 "yyyy-MM-dd HH:mm:ss", 统一为GMT

    其它注意

  • 在使用正则表达式时, 利用好预编译功能,可以有效加快正则匹配速度
  • 不要在方法体内定义
  • 二方库中可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用枚举类型或者包含枚举类型的POJO对象
  • 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理论,视图的职责是展示,不要有模型和控制器的代码逻辑
  • 任何数据结构的构造和初始化,都应指定大小,避免数据结构无限增长吃光内存
  • 及时清理不再使用的代码段或配置信息

    • 对于垃圾代码或过时配置,坚决清理干净,避免程序过度臃肿,代码冗余
    • 对于暂时被注释掉,后续可能恢复使用的代码片段,在注释代码的上方,统一规定使用三个斜杠/// 来说明注视掉代码的理由

以上是关于一位攻城狮的自我修养,在于良好的编程规范的主要内容,如果未能解决你的问题,请参考以下文章

前端攻城狮的进击路线

华为云物联网高级攻城狮的4年配置中心实践分享

#51CTO学院四周年#从程序猿到攻城狮的转变

云栖大会程序猿攻城狮的大聚会 他们眼中的云栖大会是啥样

阿里前辈:成为优秀工程师需要多久?

MarkDownVim双剑合璧