两年来遵守的代码风格

养成一个合格的编码风格,有益于自己和阅读你代码的人理解:

**** 命名风格 ****

  1. 不允许任何类名,包名,方法名,变量名以下划线_或者美元符号$开头或者结尾
    反例:name, name, name, name, $name, name $

  2. 代码中的命名不允许出现拼音,拼音与英语混合或者中文命名(专有名词除外:alibaba,taobao,suzhou等)
    反例:dazhe, guanggao, 明天,getJihuoByDate()

  3. 类名使用大驼峰(UpperCamelCase)风格命名,但是PO/VO/DTO等例外
    举例:XmlUtil, QrCode, TcpIpDeal
    反例:XMLUtil, HTTPUtil, TCPIPDeal, QRCode

  4. 方法名,参数,成员变量,局部变量使用小驼峰(lowerCamelCase)风格命名
    举例:localValue, getUsernameById(Long id)

  5. 常量命名全部要大写,单词之间以单个下划线_连接,尽量见名知意,不要嫌弃名字太长
    举例:MAX_SERVER_COUNT, CACHE_EXPIRED_TIME
    反例:MAX_COUNT, EXPIRED_TIME

  6. 抽象类命名必须以AbstractBase开头,异常类必须以Exception结尾,测试类必须以被测试类名为开头,以Test(s)结尾

  7. 数组命名:类型与方括号[]之间无空格相连定义
    举例:String[] args
    反例:String args[]

  8. 需要序列化的类中boolean类型的变量名请勿使用is为前缀,容易导致部分框架的序列化问题以及代码getter/setter逻辑问题

  9. 包名统一使用小写,点分隔符之间有且只有一个英语单词;包名使用单数形式,但是类名有复数含义,则类名可以使用复数

  10. 子类和父类中间的成员变量名不能使用相同的变量名,避免造成误读,降低代码可读性

  11. 杜绝使用不规范的单词缩写,避免单词歧义或者词不达意

  12. 接口实现类,一定要用接口类名+Impl来定义

**** 常量 ****

  1. 关于常量,需要去判断是否真正为常量,并不仅仅是用static final定义来判断
    static final int NUMBER = 5;
    static final ImmutableList NAMES = ImmutableList.of(“Ed”, “Ann”);
    static final Joiner COMMA_JOINER = Joiner.on(’,’); // because Joiner is immutable
    static final SomeMutableType[] EMPTY_ARRAY = {};
    enum SomeEnum { ENUM_CONSTANT }
    反例
    //缺少final
    static String nonFinal = “non-final”;
    //缺少static
    final String nonStatic = “non-static”;
    //set是可变的,即使是用static final修饰也不会是常量
    static final Set mutableCollection = new HashSet();
    //与上一个类似
    static final ImmutableSet mutableElements = ImmutableSet.of(mutable);
    static final Logger logger = Logger.getLogger(MyClass.getName());
    static final String[] nonEmptyArray = {“these”, “can”, “change”};
  2. 常量
    每个常量都是一个静态final字段,但不是所有静态final字段都是常量。在决定一个字段是否是一个常量时, 考虑它是否真的感觉像是一个常量。例如,如果任何一个该实例的观测状态是可变的,则它几乎肯定不会是一个常量。 只是永远不打算改变对象一般是不够的,它要真的一直不变才能将它示为常量
/**
1.不允许任何魔法值(即未预先定义的常量)直接出现在代码中

2.long和Long初始赋值时,数值后应该使用大写L,避免小写l与数字1混淆,造成误解

3.不要使用一个常量类维护所有常量,最好按照常量功能进行分类,分开维护
**/
// Constants
static final int NUMBER = 5;
static final ImmutableList<String> NAMES = ImmutableList.of("Ed", "Ann");
static final Joiner COMMA_JOINER = Joiner.on(',');  // because Joiner is immutable
static final SomeMutableType[] EMPTY_ARRAY = {};
enum SomeEnum { ENUM_CONSTANT }

// Not constants
static String nonFinal = "non-final";
final String nonStatic = "non-static";
static final Set<String> mutableCollection = new HashSet<String>();
static final ImmutableSet<SomeMutableType> mutableElements = ImmutableSet.of(mutable);
static final Logger logger = Logger.getLogger(MyClass.getName());
static final String[] nonEmptyArray = {"these", "can", "change"};

3. 代码格式

  1. 大括号{}
    如果大括号内容为空则简洁地写成{}即可,不需要换行;例外:
    左大括号不另起一行
    右大括号另起一行
    右大括号后有else/catch/finally等代码块时不换行,否则必须换行
// 多代码块的情况下,即使内容为空,也应该换行
if (a == b) {
  // do something
} else if (a == c) {
} else {
}
try {
  // do something
} catch(Exception e) {
} finally {
}
  1. 小括号():左小括号与字符之间没有空格,同理右小括号与字符之间也没有空格
    反例:if ( a == b )

  2. if/else/for/while/do/swtich等保留字与括号之间必需有一个空格
    反例:for(int i = 0; i <= 10; i++){}, if(a == b){}

  3. 运算符:任何双目、三目运算符的左右两边都必须有一个空格(赋值运算符,算数运算符,逻辑运算符,按位运算符)

举例:
int a = 1;
int b += a;
int c = a - b;
int d = (c > 0) ? 1 : 0;

if ((d == a) && (a + c == 0)) {
  System.out.println(true);
} else {
}
  1. 缩进:缩进必须使用4个空格,严禁使用Tab控制符,更不要混用

  2. 单行注释:双斜线//与注释内容之间有且仅有一个空格;单行注释不要写在代码尾部

  3. 单行代码字符不超过120(100)个,超出则必须在合适的位置换行,并且遵循下列规则:
    第二行相对第一行缩进8(4)个空格,第三方及以后与第二行保持垂直对齐
    运算符与下文一起换行(即运算符要在行首,不要放在行尾)
    方法调用的点符号.与下文一起换行
    方法调用中的多个参数需要换行时,在逗号后面换行(即不要把逗号放在行首)
    括号前不要换行

// 反例
StringBuilder sb = new StringBuilder();
sb.append('a').append('b')...append
  ("no line break here");
  1. 方法参数在定义与传入时,多个参数逗号后必须有一个空格
 void excute(String arg1, String arg2, String arg3);
method(1, 2, 3);
  1. 单个方法的实现行数不要超过80行(注释,方法签名,左右大括号,方法内空行,回车及任何不可见字符除外)

4. OOP规约

  1. 避免使用类的实例访问类的静态变量或者今天方法(无谓增加编译器的解析成本,使用类名访问即可)

  2. 所有的重写方法都必须添加@Override注解
    原因:
    1)避免某些字符相似导致的没有重写的错误;
    2)添加了注解之后,如果父类/接口签名修改,实现类马上报编译错误

  3. 可变参数:相同的参数类型,相同的业务含义,才能使用Java的可变参数;请不要使用Object类型的可变参数
    // 举例

public List<user> listUsers(Long...ids) {...}
  1. 外部正在使用的或者二方库依赖的接口,都不允许修改方法签名,以避免调用方产生影响;若方法已过时,则在方法上添加@Deprecated注解,并清晰地说明采用的新接口或者新的服务是什么

  2. 不允许使用过时的类或者方法

  3. Object的equals方法易抛出空指针,使用时应该使用常量或者确定不为空的对象调用equals(JDK7以上推荐使用:java.util.Objects#equals工具类来比较两个对象)

举例:"test".equals(obj)
  1. 所有相同类型的封装类对象之间的值比较时,全部使用equals方法(最好使用工具类)
    封装对象使用判断时,比较的是对象地址
    延伸:对Integer对象在-128~127范围内的赋值时,对象在IntegerCache.cache中产生,会服用已有对象,可以直接使用
    判断;但是超出区间外的对象在堆中生成,不会复用

  2. 封装类型和基本数据类型:
    所有POJO的成员变量必须使用封装类型
    原因:POJO的属性没有初始值,是要提醒使用者使用时需要显式赋值;POJO对应的数据库数据表中的字段有可能为空,使用基本数据类型自动拆箱,会有NPE(NullPointerException)的风险
    RPC方法的返回值和参数列表必须使用封装类型
    原因:远程服务调用如果返回基本数据类型,在某个服务失败时会返回默认值(0/false)而不是null,会导致调用方逻辑判断有误,如果返回为null,则调用方可以显示错误或者抛出异常
    局部变量推荐使用基本数据类型

  3. POJO中的属性不要赋初始值

  4. 所有实现了java.io.Serializable接口的类,都必须添加private static final long serialVersionUID属性(请不要使用默认值-1也不要复制其他类的值,让IDE自动生成,保证值唯一;请不要使用@SuppressWarnings忽略警告)

  5. 当序列化类中添加新属性时,请不要修改serialVersionUID的值,防止反序列化失败;如果要做不兼容升级,请修改serialVersionUID的值,防止反序列化混乱

  6. 构造方法中禁止添加业务逻辑代码,初始化的逻辑代码请放在init方法中

  7. POJO必须重写toString方法,在Debug或者排查错误是,调用toString输出POJO的属性,便于排查问题

  8. 在POJO类中,禁止同时存在同一个属性的getXxx()和isXxx()方法

  9. 当一个类中存在多个构造方法,或者存在多个同名方法时,请按照一定顺序把所有相同的方法放置在一起

  10. 尽量不要在getter/setter方法中添加业务逻辑

5. 集合

  1. hashCode和equals
    只要重写equals,则必须重写hashCode
    使用自定义对象作为Map的键时,必须重写equals和hashCode

  2. Map的keySet(), values(), entrySet()方法返回的集合,不允许添加元素,否则会报UnsupportedOperationException

  3. Collections类emptyList(), singletonList()等方法返回的集合都是Immutable的,不可用添加或者删除元素

  4. ArrayList的subList()方法返回的对象是SubList,不可强转为其他类型

  5. 在subList的场景中,对原集合元素个数的修改会导致对子集合的所有操作报ConcurrentModificationException

  6. 在把集合转换成数组时,请使用 T[] toArray(T[] a)方法,不要使用无参的重载,实现如下:

  List<String> list = new ArrayList<>(2);
  list.add("1");
  list.add("2");
  String[] array = new String[list.size()];
  array = list.toArray(array);
  1. 在把数组转成集合时,注意如下:
  String[] array = new String[] { "1", "2" };
  List<String> list = Arrays.asList(array);
  // List<String> list = Arrays.asList("1", "2");
  // 这行代码会抛出UnsupportedOperationException
  list.add("3");
  // 这行代码执行后,list.get(0)的值也会随之修改
  str[0] = "0";
  //原因:asList()方法的返回值是java.util.Arrays.ArrayList,并没有重写AbstractList类实现的add方法;这个方法体现的是适配器模式,只作为转换接口使用,数据结构上仍是数组,
  //避免以上问题的写法:new java.util.ArrayList<>(Arrays.asList("1", "2"));
  1. boolean addAll(Collection<? extends E> c)方法的调用,在调用Collection接口任何实现类的addAll方法时,传入的参数都必须做NPE检查

  2. 使用泛型通配符<? extends T>的集合,不能调用add方法,而<? super T>的集合不能调用get方法;根据PECS(Producer Extends, Consumer Super)原则:频繁往外读取数据适合使用<? extends T>,频繁往里插入内容适合使用<? super T>

  3. 把无泛型限制的集合赋值给有泛型的集合时,需要进行instanceof判断,避免抛出ClassCastException

  4. 不要在foreach中添加或者删除元素(不要在对集合遍历时调用此集合的add/remove方法)

  List<String> list = new ArrayList<>(Arrays.asList("1", "2", "3"));
  // 错误写法
  for (String str : list) {
      if (Objects.equals("1", str)) {
          list.add("4");
          continue;
      }
      if (Objects.equals("2", str)) {
          list.remove(str)
          break;
      }
  }
  // 正确写法
  Iterator<String> iterator = list.iterator();
  while (iterator.hasNext()) {
      iterator.remove();
  }
  1. JDK7以上,使用泛型时,使用diamond语法(菱形语法):List list = new ArrayList<>();

  2. 对Map进行遍历时使用entrySet而不是keySet,JDK8以上则使用Map.foreach
    注意:keySet方式遍历Map其实是遍历了两次,一次是keySet生成Iterator,一次是从HashMap中取出key对应的value

6. 多线程、并发

  1. 创建单例对象是要保证线程安全:(推荐一下两种方式,其他方式各有优缺点自行了解)

枚举:最简单,最优秀的创建单例的方式

public class SingletonEnum {

private SingletonEnum() {
}

private enum Singleton {
   INSTANCE;

   private final SingletonEnum instance;

   Singleton() {
       instance = new SingletonEnum();
   }

   public SingletonEnum getInstance() {
       return instance;
   }
}

public static SingletonEnum getInstance() {
   return Singleton.INSTANCE.getInstance();
}
}

内部类方式:既能保证线程安全,又不会大量JVM资源

public class SingletonClass {

private SingletonClass() {
}

// 外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE
// 调用的时候只创建一个 所以单例
private static class InstanceHolder {
   private final static SingletonClass instance = new SingletonClass();
}

public static SingletonClass getInstance() {
   return InstanceHolder.instance;
}
}
  1. 创建线程时,需要指定有意义的线程名称,方便问题回溯

  2. 线程创建必须通过线程池提供,不允许显式地自行创建线程
    线程池的好处就是减少线程创建和销毁的开销,极大地优化系统资源不足的问题

  3. 不允许使用Executors创建线程池,需要使用ThreadPoolExecutor的方式创建,这种方式可以更加明确线程池的允许规则

  4. JDK8使用Instant代替Date,使用LocalDateTime代替Calendar,使用DateTimeFormatter代替SimpleDateFormat时间操作更方便,线程安全

  5. 必须回收自定义的ThreadLocal变量,有可能导致内存泄漏OOM

  6. 高并发场景下,认真考量线程锁,能不用锁尽量不用,能不用带锁的数据结构就尽量不用,加锁的代码块尽量小;避免在锁中调用RPC服务

  7. 在对多个资源,数据表,对象加锁时,保持一致的加锁顺序,避免死锁

  8. 与资金相关的金融敏感信息使用悲观锁

7. 控制语句

  1. switch
    每个case语句要么通过break/return来终止,要么添加注释说明要执行到哪个语句结束
    必须包含一个default语句,即使什么逻辑都没有

  2. 所有的代码块都必须包含大括号,即使代码块只有一行代码(if/else/for/while/do)
    禁止单行编码方式:if (true) doSomething();

  3. 禁止出现if-else if…else的代码
    一定需要这种逻辑判断的时候,代码不能超过三层(即只允许出现一次else if),正确写法(卫语句,策略模式,状态模式实现):

// 卫语句
public void doSomething() {
  if (isBusy()) {
      // change time
      return;
  }

  if (isFree()) {
      // do something
      return;
  }

  // study
  return;
}
  1. 不要在判断条件中执行复杂逻辑,除了一般常用的getXx()/isXx()/hasXx()/existXx()方法判断外,判断条件一般使用单个boolean值
  final boolean flag = (getXx() || hasXx()) && (a >= b || c != 0);
  if (flag) {
      // do something
  }
  1. 在高并发的场景下,不要使用==作为程序中断或者退出的判断条件,因为在并发处理错误的时候,会导致程序永远都不会退出

  2. 循环体中的语句要考量性能。以下操作尽量提取到循环体外:
    定义对象、变量
    获取数据库连接
    不必要的try-catch

  3. 循环体/递归尽量不要超过两次,禁止超过三层

  4. 避免不必要的取反逻辑

8. 注释

  1. 类,类属性,类方法的注释都必须符合Javadoc规范,使用/注释内容/格式,禁止使用单行注释方式

  2. 所有的抽象方法(包括接口方法),都必须要用Javadoc注释,清楚地说明参数,返回值,异常,以及该方法做什么事情,实现什么功能,对子类的实现有什么要求,调用时需要注意什么等

  3. 所有类必须添加创建者及创建时间

  4. 方法内的单行注释,在被注释代码上方另起一行(禁止行尾注释),使用//注释;方法内的多行注释,使用/* */注释,注意代码对齐

  5. 所有的枚举类型字段必须要有注释,说明每个数据的含义及用途

  6. 注释可以使用中文,专有名词和关键字注意保持与原文一致;注释要语句通顺,意思表达完整

  7. 修改代码时,注意修改原注释

  8. 注释代码时,请在被注释掉的代码上方,清楚地说明原因(使用///三斜线);如果代码无用,请直接删除

  9. 特殊注释: TODO, FIXME
    待办事项TODO: 在添加时,请写清楚:标注人(执行人),标注时间,预计处理的时间,未实现的功能
    错误异常FIXME: 在添加时,请写清楚:标注人(执行人),标注时间,预计处理的时间,使用fixme标记某代码是错误的,不能工作,需要及时修改

异常日志

  1. 关于RuntimeException
    类似NullPointException,IndexOutOfBoundsException等不应该使用catch来处理异常,而应该在代码中通过判断来规避这类异常
    // JDK8
    public final class Optional
    类似NumberFormatException可以使用catch捕获并处理捕获异常

  2. 捕获的异常不允许不做任何处理就丢弃,如果不想处理则抛出给调用者
    最外层调用者一定要把异常处理成用户可以理解的内容

  3. 在事务中,需要仔细判断是否需要事务回滚,调用rollback
    不要在finally代码块中使用return语句
    捕获的异常必须与抛出的异常类型完全匹配,或者捕获抛出异常的父类型

  4. 大段的try-catch代码需要对不同异常作出不同的应激反应,不允许对大量不同类型的异常统一捕获Exception对象,不利于对不同问题作出对应的处理
    对应不同异常类型,但是处理方式一致的情况,使用multi-catch方式编码

  5. 资源释放
    必须在finally代码块中释放掉所有的资源对象,流对象,关闭对象时的异常需要进行try-catch操作,禁止直接抛出

//JDK7及以上建议使用try-with-resource方式编码
  try (InputStream is = new FileInputStream("text.txt")) {
      // do something
  } catch (IOException e) {
      // handle io exception
  }
  // 此处不需要显式的通过finally块关闭流,流会在代码执行完毕后自动释放
  1. 空值null
    允许方法返回null,但是最好尽量避免;在返回null的方法要注释充分说明,什么情况下才会返回null,调用方需要添加NPE判断
    自动拆箱时,注意空指针
    数据库查询到的数据属性可能是null
    集合即使isNotEmpty,取出的数据依然可能是null
    远程调用返回对象时,一定要做NPE检查
    级联调用时注意空指针:obj.getA().getB().getC(),建议写法:
  Optional.ofNullable(obj)
      .map(Obj::getA)
      .map(A::getB)
      .map(B::getC)
      .get()
  1. 自定义异常
    不要在代码中显式的抛出new RuntimeException(),更不允许抛出Exception或者Throwable
    根据自己的业务定义自定义异常(ServerException, UsernameExistException)
    定义ErrorCode

  2. 所有对外的接口,都必须有统一的数据结构和返回规则;统一的错误编码可以更好帮助调用者定位问题或者处理逻辑

  3. 重复代码
    不允许大段复制代码(Don’t repeat yourself)即DRY原则
    把需要重复使用的代码提取成共性代码,公共方法等

  4. 日志
    输出日志建议使用SLF4J,或者框架推荐日志实现方式
    日志文件至少保存15天以上
    输出日志时不要使用字符串拼接方式,建议使用日志占位符
    // 占位符{}
    logger.info(“id-{}, message-{}”, id, message);
    对于debug/info/trace级别的日志,必须使用条件输出或者占位符的方式输出
    // 条件输出
    if (logger.isDubugEnable()) {
    logger.debug(“id-” + id + “, message-” + message);
    }
    禁止在生产环境中使用System.out, System.err输出日志;禁止使用e.printStackTrace()输出异常堆栈
    开发时可以用来打桩调试,提交代码时务必删除
    输出异常信息时,需要输出异常信息和堆栈;如果不做处理,请把异常往上一层抛出
    logger.error("异常信息toString: " + e.getMessage, e);
    生产环境禁止输出debug级别的日志,info级别的日志请有选择的输出
    避免大量的无效日志,大量的无效日志及影响系统的运行,又不能快速的定位问题;输出日志时请认真思考这些日志能用来做什么,这些日志能帮助排查问题吗
    刚上线的项目允许通过warn级别输出业务行为信息,但是要注意输出量,避免把服务器磁盘搞爆炸,并及时删除日志文件
    适当的使用warn级别日志,记录用户输入的错误参数情况,可以在用户投诉时提供帮助
    推荐使用英文输出日志信息,当用英文表达不清楚时,可以使用中文(谨慎使用);对应国外服务器或者国际服务器或者中外同服的情况下,全部使用英文输出日志,避免字符集的问题

  5. 安全规约
    任何属于用户个人的页面或者功能点都必须添加权限控制校验
    用户的敏感信息禁止直接展示,必须对展示的信息进行脱敏处理(部分/全部数据替换成***)
    注意:对外接口、服务返回的数据里不要包含敏感数据
    密码、手机号、身份证号、真实姓名等必要的数据

  6. SQL:用户输出的SQL参数必须进行严格的参数绑定和验证,防止SQL注入;禁止使用字符串拼接SQL访问数据库

  7. 所有用户输入的数据都必须进行参数绑定和有效性验证

  8. 在使用平台资源(如:短信,邮件,电话,支付等)时,必须正确实现防重放机制(如:数量限制,疲劳值限制,验证码校验),避免被滥刷或者资产损失

MYSQL

  1. 建表
    表达是否概念的字段一律使用is_xxx,数据类型为unsigned tinyint(1表示是,0表示否)
    is_deleted:是否删除
    表明或者字段名必须使用小写字母或者数字,单词之间用下划线连接,禁止以数字打头,禁止两个下划线之间只包含数字
    MYSQL在Windows下不区分大小写,但是在Linux上默认区分大小写,所有禁止在数据库中使用大写字母,避免不必要的麻烦
    表名不允许使用复数名称命名
    禁止使用数据库保留字(附:MYSQL8.0保留字库)
    经常被使用的保留字:name(s), key(s), desc等,需要特别注意
    小数类型应该为:decimal,禁止使用float/double
    如果存储的字符串长度基本相等,应使用定长的char类型代替varchar。例如:MD5加密的密码,以一定规则生成的KEY,UUID等
    varchar是可变长度字符串类型,不预先分配存储空间,最大长度不要超过5000。如果存储长度超过5000,需要使用text类型,并且建议建立关联表以主键关联,避免影响其他字段的索引效率
    允许数据表数据冗余,提高查询效率,但是必须考虑数据的一致性
    此冗余字段不能频繁修改
    此字段不能是超长的varchar或者text

  2. 单表数据超过500W行或者单表容量超过2GB时才推荐使用分表分库

  3. 对字段设置合适的存储长度,可以节约数据表空间和索引存储,更重要的是可以提升检索速度

索引

  1. 业务上具有唯一特效的字段必须建立唯一索引
    虽然唯一索引会影响插入速度,这个速度损耗可以忽略,但是会明显的提升查询速度
    根据墨菲定律,即使在应用层做了严格的参数验证,只要没有唯一索引,就一定会有脏数据产生

  2. 绝对禁止三个表以上的join操作
    需要join的字段,数据类型一定要一致
    当多表关联查询时,保证关联字段要有索引
    即使双表的join操作,也要注意索引和SQL性能
    在varchar字段上建立索引时,需要指定建立索引的长度
    模糊搜素时,禁止使用左模糊或者全模糊,如果需要请搜索索引文件,否则会导致索引失效

SQL

  1. 不要使用count(列名)或者count(常量)代替count()
    count(
    )是SQL92的标准统计行数的语法(跟数据库无关,与NULL值也无关),并且MYSQL对其进行了深度优化
    count(列名)的方式会忽略掉此列为NULL的数据行

  2. 使用SUM(列名)时,需要注意NPE问题
    使用IS NULL来判断NULL。因为NULL与其他任意值得比较结果均为NULL,而不是TRUE/FALSE
    禁止在数据库中使用外键或者级联,一起外键的概念必须在应用层解决
    禁止使用存储过程
    难以调试,难以扩展,没有移植性

  3. 数据删除或者修改时,必须先做查询操作,确认无误后,在查询到的数据基础上再做更新或者删除

  4. 规避在SQL中使用in操作,如果无法避免,则要仔细评估in后面的集合元素数量,控制在1000个以内

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章