JAVA编程规约之我不知系列

JAVA编程规约之我不知系列 命名风格

  1. 接口类中的方法和属性不要家人和修饰符号(public也不要加),保持代码的简洁性,并加上有效的Javadoc注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是与接口方法相关,并且是整个应用的基础常量。
  2. get、list、count、save/insert、remove、update作为前缀。
  3. 数据对象DO、数据传输对象DTO、展示对象VO。
常量定义
  1. 不允许魔法值(即未经定义的常量)直接出现在代码中。
  2. long和Long初始赋值时候,使用大写的L。
  3. 缓存常量CacheConsts,系统配置常量ConfigConsts。
代码格式
  1. 如果是大括号内为空,则简洁地写成{}即可。
  2. if/for/while/switch/do等保留字与括号之间都必须加空格。
  3. 任何二目、三目运算符的左右两边都需要加上一个空格。(=、&&、+…)
  4. 采用4个空格缩进,禁止使用tab字符。IDEA设置tab未4个空格时,请勿勾选Use tab character;而在eclipse中,必须勾选insert space for tabs。
  5. 注释的双斜线与注释内容之间有且仅有一个空格。
  6. 单行字符数限制不超过120个,超出需要换行,换行时遵循如下原则:
    1). 第二行相对第一行缩进4个空格,从第三行开始,不再继续缩进。
    2). 运算符与下文一起换行。
    3). 方法调用的点符号与下文一起换行。
    4). 方法调用时,多个参数,需要换行时,在逗号后进行。
    5). 在括号前不要换行。
  7. 方法体内的执行语句组、变量的定义语句组、不同的业务逻辑之间或者不同的语义之间插入一个空行。相同业务逻辑和语义之间不需要插入空行。
OOP规约
  1. 避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。
  2. 所有的复写方法,必须加@override注解。
  3. 相同参数类型,相同业务含义,才可以使用java的可变参数,避免使用Object。(提倡同学们精良不用可变参数编程)
  4. 所有的相同类型的包装类对象之间值的比较,全部使用equals方法比较。
  5. 关于基本数据类型与包装数据类型的使用标准如下:
    1). 所有的POJO类属性必须使用包装数据类型。
    2). RPC方法的返回值和参数必须使用包装数据类型。
    3). 所有的局部限量使用基本数据类型。
  6. 定义DO/DTO/VO等POJO类时,不要设定任何属性的默认值
  7. 序列化类新增属性时,请不要修改serialVersionUID字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改serialVersionUID值。
  8. 构造方法里禁止加入任何业务逻辑,如果有初始化逻辑,请放在init方法中。
  9. POJO类必须写toString方法。如果继承了另一个POJO类,注意加一下super.toString使用IDE的中工具直接生成。
  10. 使用索引访问用String的split方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛IndexOutOfBoundsException的风险。
String str = "a,b,c,,"; String[] ary = str.split(","); // 预期大于3,结果是3 System.out.println(arg.length);

  1. 类中方法定义的顺序是:公有方法或保护方法>私有方法>getter/setter方法。
  2. getter/setter方法中,不要增加业务逻辑,增加排查问题的难度。
  3. 循环体内,字符串的连接方式,使用StringBuilder的append方法进行扩展。
  4. final可以声明类、成员变量、方法、以及本地变量,下列情况使用final关键字:
    1). 不允许被继承的类。
    2). 不允许修改应用的域对象。
    3). 不允许被重写的方法。
    4). 不允许运行过程中重新复制的局部变量。
    5). 避免上下文重复使用一个变量,使用final描述可以强制重新定义一个变量,方便更好地进行重构。
  5. 慎用Object的clone方法来拷贝对象。对象的clone方法默认是浅拷贝,若想实现深拷贝需要重写clone方法实现属性对象的拷贝。
  6. 类成员与方法访问控制从严。
    1). 如果不允许外部直接通过new来创建对象,那么构造方法必须是private。
    2). 工具类不允许有public或default构造方法。
    3). 类非static成员变量并且与子类共享,必须是protected。
    4). 类非static成员变量并且仅在本类使用,必须是private。
    5). 类static成员变量如果仅在本类使用,必须是private。
    6). 若是static成员变量,必须考虑是否为final。
    7). 类成员方法只供类内部调用,必须是private。
    8). 类成员方法只对继承类公开,那么限制为protected。
集合处理
  1. 关于hashCode和equals的处理,遵循如下规则:
    1). 只要重写equals,就必须重写hashCode。
    2). 因为Set存储的是不重复的对象,依据hashCode和equals进行判断,所以Set存储的对象必须重写这两个方法。
    3). 如果自定义对象作为Map的键,那么必须重写hashCode和equals。
  2. ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCaseException异常。
  3. 在subList场景中,==高度注意==对原集合元素个数的修改,会导致子列表的遍历、增加、删除均会产生ConcurrentModificationExecption异常。
  4. 使用集合转数组的方法,必须使用集合的toArray(T[] array),传入的是类型完全一样的数组,大小就是list.size()。
  5. 使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UnsupportedOperationException异常。
String[] str = new String[]{"you", "wu"}; List list = Arrays.asList(str); list.add("wowwj"); // 运行是异常 str[0] = "gsdf"; // 那么list.get(0)也会随之修改

  1. 泛型通配符
new Comparator() { @Override public int compare(Student o1, Student o2) { return o1.getId() > o2.getId() ? 1 : -1; // 没有处理相等的情况,实际使用中可能会出现异常 } }

  1. 集合初始化时,指定集合初始值大小。
  2. 使用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值值组合集合。
  3. 高度注意Map类集合K/V能不能存储null值的情况,如下表格:
集合类 Key Value Super 说明
Hashtable 不允许为null 不允许为null Dictionary 线程安全
ConcurrentHashMap 不允许为null 不允许为null AbstractMap 锁分段技术(JDK8:CAS)
TreeMap 不允许为null 允许为null AbstractMap 线程不安全
HashMap 允许为null 允许为null AbstractMap 线程不安全
【JAVA编程规约之我不知系列】12. 合理利用集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响。
集合类 有序性 稳定性
ArrayList unsort order
HashMap unsort unorder
TreeSet sort unorder
并发处理
  1. 获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
    说明:资源驱动类、工具类、单例工厂类都需要注意。
  2. 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
public class TimerTaskThread extends Thread { public TimerTaskThread() { super.setName("TimerTaskThread"); ... } }

  1. 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
  2. 线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
    *说明: Executors返回的线程池对象的弊端如下:
    1). FixedThreadPool和SingleThreadPool:
    允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
    2). CachedThreadPool和ScheduledThreadPool:
    允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。*
  3. SimpleDateFormat是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用DateUtils工具类。
// 注意线程安全,使用DateUtils。亦推荐如下处理: private static final ThreadLocal df = new ThreadLocal() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } }

说明:如果是JDB8的应用,可以使用Instant代替Date,LocalDateTime代替Calendar,DateTimeFormatter代替SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。
6. 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
说明:尽可能使加锁的代码块工作量尽可能小,避免在锁代码块中调用RPC方法。
7. 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。
说明:线程一需要对表A、B、C依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是A、B、C,否则可能出现死锁
8. 并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用version作为更新依据。
说明:如果每次访问冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3次。
9. 多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用ScheduledExecutorService则没有这个问题。
10. 使用CountDownLatch进行异步转同步操作,每个线程退出前必须调用countDown方法,线程执行代码注意catch异常,确保countDown方法被执行到,避免主线程无法执行至await方法,直到超时才返回结果。
说明:注意,子线程抛出异常堆栈,不能再主线程try-catch到。
11. 避免Random实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一个seed导致的性能下降。
说明:Random实例包括java.util.Random的实例或者Math.random()的方式。
12. 在并发场景下,通过双重检查锁(double-checked locking)实现延迟初始化的优化问题隐患,推荐解决方案中较为简单一种(适用于JDK5及以上版本),将目标属性声明为colatile型。
class Singleton { private Helper helper = null; public Helper getHelper() { if (helper == null) synchronized(this) { if (helper == null) { helper = new Helper(); } } return helper; } // other methods and fields... }

  1. volatile解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。如果是count++操作,使用如下类实现:AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是JDK8,推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)。
  2. HashMap在容量不够进行resize时由于高并发可能出现死链,导致CPU飙升,在开发过程中可以使用其它数据结构或加锁来规避此风险。
  3. ThreadLocal无法解决共享对象的更新问题,ThreadLocal对象建议使用static修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。
控制语句
  1. 在一个switch块中,都必须包含一个default语句并放在最后,即使它什么代码也没有。
  2. 表达异常的分支时,少用if-else方式,这种方式可以改写成:
if (condition) { ... return obj; } // 接着写else的业务逻辑代码;

  1. 除常用方法外,不要在条件判断中执行其他复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
final boolean existed = (file.open(fileName, "w") != null) && (...) || (...); if (existed) { ... }

  1. 循环体重的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的try-catch操作。
  2. 下列情形,需要进行参数校验:
    1). 调用频次低的方法
    2). 执行时间开销很大的方法
    3). 需要极高稳定性和可用性的方法
    4). 对外提供的开放接口
    5). 敏感权限接口
  3. 下列情形,不需要进行参数校验:
    1). 极有可能被循环调用的方法
    2). 底层调用频度比较高的方法
    3). 被声明成private只会被自己代码所调用的方法
其它
  1. 在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。
    说明:不要在方法体内定义:Pattern pattern = Pattern.compile(规则);
  2. 后台输送给页面的变量必须加$!{var}——中间的感叹号。
    说明:如果var=null或者不存在,那么${var}会直接显示在页面上。
  3. 注意 Math.random() 这个方法返回是double类型,注意取值的范围0≤x<1(能够取到零值,注意除零异常),如果想获取整数类型的随机数,不要将x放大10的若干倍然后取整,直接使用Random对象的nextInt或者nextLong方法。
  4. 获取当前毫秒数System.currentTimeMillis(); 而不是new Date().getTime(); 说明:如果想获取更加精确的纳秒级时间值,使用System.nanoTime()的方式。在JDK8中,针对统计时间等场景,推荐使用Instant类。
  5. 任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存。
  6. 及时清理不再使用的代码段或配置信息。
    *说明:对于垃圾代码或过时配置,坚决清理干净,避免程序过度臃肿,代码冗余。
    正例:对于暂时被注释掉,后续可能恢复使用的代码片断,在注释代码上方,统一规定使用三个斜杠(///)来说明注释掉代码的理由。*

    推荐阅读