Java开发手册
Java开发手册
ibingbing一、编程规约
(一)命名风格
1.【强制】代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
1 | 反例:_name / __name / $name / name_ / name$ / name__ |
2.【强制】所有编程相关的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。
1 | 说明:正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,纯拼音命名方式更要避免采用。 |
3.【强制】类名使用 UpperCamelCase 风格,但以下情形例外:DO / BO / DTO / VO / AO / PO / UID 等。
1 | 正例:ForceCode / UserDO / HtmlDTO / XmlService / TcpUdpDeal / TaPromotion |
4.【强制】方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格。
1 | 正例: localValue / getHttpMessage() / inputUserId |
5.【强制】常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
1 | 正例:MAX_STOCK_COUNT / CACHE_EXPIRED_TIME |
6.【强制】抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾;测试类命名以它要测试的类的名称开始,以 Test 结尾。
7.【强制】类型与中括号紧挨相连来表示数组。
1 | 正例:定义整形数组 int[] arrayDemo; |
8.【强制】POJO 类中的任何布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误。
1 | 反例:定义为基本数据类型 Boolean isDeleted 的属性,它的方法也是 isDeleted(),框架在反向解析的时候,“误以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。 |
9.【强制】杜绝完全不规范的缩写,避免望文不知义。
1 | 反例:AbstractClass“缩写”命名成 AbsClass;condition“缩写”命名成 condi,此类随意缩写严重降低了代码的可阅读性。 |
10.【推荐】接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性。尽量不要在接口里定义变量,如果一定要定义变量,确定与接口方法相关,并且是整个应用的基础常量。
1 | 正例:接口方法签名 void commit(); |
11.【参考】各层命名规约
1 | A) Controller 层: |
(二)常量定义
1.【强制】不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。
1 | 反例: |
2.【强制】在 long 或者 Long 赋值时,数值后使用大写的 L,不能是小写的 l,小写容易跟数字
混淆,造成误解。
1 | 说明:Long a = 2l; 写的是数字的 21,还是 Long 型的 2。 |
3.【推荐】不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护。
1 | 说明:大而全的常量类,杂乱无章,使用查找功能才能定位到修改的常量,不利于理解,也不利于维护。 |
(三)代码格式
1.【强制】提交代码前,一定要对代码进行格式化。
1 | 说明:在开发工具IntelliJ IDEA中 |
(四)OOP 规约
1.【强制】避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。
2.【强制】所有的覆写方法,必须加@ Override 注解。
1 | 说明:getObject()与 get0bject()的问题。一个是字母的 O,一个是数字的 0,加 @Override 可以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,其实现类会马上编译报错。 |
3.【强制】不能使用过时的类或方法。
1 | 说明:java.net.URLDecoder 中的方法 decode(String encodeStr) 这个方法已经过时,应该使用双参数decode(String source, String encode)。接口提供方既然明确是过时接口,那么有义务同时提供新的接口;作为调用方来说,有义务去考证过时方法的新实现是什么。 |
4.【强制】 Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调 equals 。
1 | 正例:"test".equals(object); |
5.【强制】所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
1 | 说明:对于 Integer var = ? 在-128 至 127 之间的赋值,Integer 对象是在 IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。 |
6.【强制】任何货币金额,均以最小货币单位且整型类型来进行存储。
1 | 说明:金额使用 Long 类型来存储,单位是分 |
(五)日期时间
1.【强制】日期格式化时,传入 pattern 中表示年份统一使用小写的 y。
1 | 说明:日期格式化时,yyyy 表示当天所在的年,而大写的 YYYY 代表是 week in which year(JDK7之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的 YYYY就是下一年。 |
2.【强制】在日期格式中分清楚大写的 M 和小写的 m,大写的 H 和小写的 h 分别指代的意义。
1 | 说明:日期格式中的这两对字母表意如下: |
3.【强制】获取当前毫秒数:System.currentTimeMillis(); 而不是 new Date().getTime()。
1 | 说明:如果想获取更加精确的纳秒级时间值,使用 System.nanoTime 的方式。在 JDK8 中,针对统计时间等场景,推荐使用 Instant 类。 |
4.【强制】不允许在程序任何地方中使用:1)java.sql.Date 2)java.sql.Time 3)java.sql.Timestamp。
1 | 说明:第 1 个不记录时间,getHours()抛出异常;第 2 个不记录日期,getYear()抛出异常;第 3 个在构造方法 super((time/1000)*1000),fastTime 和 nanos 分开存储秒和纳秒信息。 |
5.【强制】不要在程序中写死一年为 365 天,避免在公历闰年时出现日期转换错误或程序逻辑错误。
1 | 正例: |
6.【推荐】使用枚举值来指代月份。如果使用数字,注意 Date,Calendar 等日期相关类的月份 month 取值在 0-11 之间。
1 | 说明:参考 JDK 原生注释,Month value is 0-based. e.g., 0 for January. |
(六)集合处理
1.【强制】判断所有集合内部的元素是否为空,使用 isEmpty()方法,而不是 size()==0 的方式。
1 | 说明:前者的时间复杂度为 O(1),而且可读性更好。 |
2.【强制】在使用 java.util.stream.Collectors 类的 toMap()方法转为 Map 集合时,一定要使用含有参数类型为 BinaryOperator,参数名为 mergeFunction 的方法,否则当出现相同 key 值时会抛出 IllegalStateException 异常。
1 | 说明:参数 mergeFunction 的作用是当出现 key 重复时,自定义对 value 的处理策略。 |
3.【强制】在使用 java.util.stream.Collectors 类的 toMap()方法转为 Map 集合时,一定要注意当 value 为 null 时会抛 NPE 异常。
1 | 说明:在 java.util.HashMap 的 merge 方法里会进行如下的判断: |
4.【强制】使用 Map 的方法 keySet() / values() / entrySet() 返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常。
5.【强制】使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组。
1 | 反例:直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[] 类,若强转其它类型数组将出现 ClassCastException 错误。 |
6.【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
1 | 正例: |
7.【推荐】集合泛型定义时,在 JDK7 及以上,使用 diamond 语法或全省略。
1 | 说明:菱形泛型,即 diamond,直接使用<>来指代前边已经指定的类型。 |
8.【推荐】集合初始化时,指定集合初始值大小。
1 | 说明:HashMap 使用 HashMap(int initialCapacity) 初始化,如果暂时无法确定集合大小,那么指定默认值(16)即可。 |
9.【推荐】使用 entrySet 遍历 Map 类集合 KV ,而不是 keySet 方式进行遍历。
1 | 说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8,使用Map.forEach 方法。 |
10.【推荐】高度注意 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 | 线程不安全 |
1 | 反例:由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,而事实上,存储 null 值时会抛出 NPE 异常。 |
11.【参考】利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的
contains()进行遍历去重或者判断包含操作。
(七)并发处理
1.【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
1 | 说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 |
2.【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
1 | 说明:Executors 返回的线程池对象的弊端如下: |
3.【强制】 SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static ,必须加锁,或者使用 DateUtils 工具类。
1 | 正例:注意线程安全,使用 DateUtils。亦推荐如下处理: |
4.【强制】必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 try-finally 块进行回收。
1 | 正例: |
5.【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
1 | 说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。 |
6.【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。
1 | 说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。 |
7.【强制】在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。
1 | 说明一:如果在 lock 方法与 try 代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。 |
8.【强制】在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。
1 | 说明:Lock 对象的 unlock 方法在执行时,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),如果当前线程不持有锁,则抛出 IllegalMonitorStateException 异常。 |
9.【强制】并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。
1 | 说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3 次。 |
(八)控制语句
1.【强制】在一个 switch 块内,每个 case 要么通过 continue/break/return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default语句并且放在最后,即使它什么代码也没有。
1 | 说明:注意 break 是退出 switch 语句块,而 return 是退出方法体。 |
2.【强制】当 switch 括号内的变量类型为 String 并且此变量为外部参数时,必须先进行null判断。
1 | 反例:如下的代码输出是什么?(报:java.lang.NullPointerException) |
3.【强制】在 if / else / for / while / do 语句中必须使用大括号。
1 | 说明:即使只有一行代码,禁止不采用大括号的编码方式:if (condition) statements; |
4.【强制】三目运算符 condition? 表达式 1 : 表达式 2 中,高度注意表达式 1 和 2 在类型对齐时,可能抛出因自动拆箱导致的 NPE 异常。
1 | 说明:以下两种场景会触发类型对齐的拆箱操作: |
5.【强制】在高并发场景中,避免使用”等于”判断作为中断或退出的条件。
1 | 说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替。 |
6.【推荐】表达异常的分支时,少用 if-else 方式 ,这种方式可以改写成:
1 | if (condition) { |
7.【推荐】除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
1 | 说明:很多 if 语句内的逻辑表达式相当复杂,与、或、取反混合运算,甚至各种方法纵深调用,理解成本非常高。如果赋值一个非常好理解的布尔变量名字,则是件令人爽心悦目的事情。 |
8.【推荐】循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的 try-catch 操作 ( 这个 try-catch 是否可以移至循环体外 )。
9.【推荐】避免采用取反逻辑运算符。
1 | 说明:取反逻辑不利于快速理解,并且取反逻辑写法必然存在对应的正向逻辑写法。 |
10.【推荐】接口入参保护,这种场景常见的是用作批量操作的接口。
1 | 反例:某业务系统,提供一个用户批量查询的接口,API 文档上有说最多查多少个,但接口实现上没做任何保护,导致调用方传了一个 1000 的用户 id 数组过来后,查询信息后,内存爆了。 |
(九)注释规约
1.【强制】类、类属性、类方法的注释必须使用 Javadoc 规范,使用/**内容*/
格式,不得使用// xxx
方式。
1 | 说明:在 IDE 编辑窗口中,Javadoc 方式会提示相关注释,生成 Javadoc 可以正确输出相应释;在 IDE 中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。 |
2.【强制】所有的抽象方法 ( 包括接口中的方法 ) 必须要用 Javadoc 注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。
1 | 说明:对子类的实现要求,或者调用注意事项,请一并说明。 |
3.【强制】所有的类都必须添加创建者和创建日期。
4.【强制】方法内部单行注释,在被注释语句上方另起一行,使用//
注释。方法内部多行注释使
用/* */
注释,注意与代码对齐。
5.【推荐】与其“半吊子”英文来注释,不如用中文注释把问题说清楚。专有名词与关键字保持英文原文即可。
1 | 反例:“TCP 连接超时”解释成“传输控制协议连接超时”,理解反而费脑筋。 |
6.【推荐】代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。
1 | 说明:代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义。 |
7.【推荐】在类中删除未使用的任何字段和方法;在方法中删除未使用的任何参数声明与内部变量。
8.【参考】特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描,经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。
1 | 1) 待办事宜(TODO):(标记人,标记时间,[预计处理时间]) |
(十)其它
1.【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。
1 | 说明:不要在方法体内定义:Pattern pattern = Pattern.compile(“规则”); |
2.【推荐】任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存。
3.【推荐】及时清理不再使用的代码段或配置信息。
1 | 说明:对于垃圾代码或过时配置,坚决清理干净,避免程序过度臃肿,代码冗余。 |
二、异常日志
1.【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
2.【强制】事务场景中,抛出异常被 catch 后,如果需要回滚,一定要注意手动回滚事务。
3.【强制】finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。
1 | 说明:如果 JDK7 及以上,可以使用 try-with-resources 方式。 |
4.【强制】不要在 finally 块中使用 return。
1 | 说明:try 块中的 return 语句执行成功后,并不马上返回,而是继续执行 finally 块中的语句,如果此处存在 return 语句,则在此直接返回,无情丢弃掉 try 块中的返回点。 |
5.【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
1 | 说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。 |
6.【推荐】防止 NPE ,是程序员的基本修养,注意 NPE 产生的场景:
1 | 1) 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。 |
7.【参考】对于公司外的 http/api 开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间 RPC 调用优先考虑使用 Result 方式,封装 isSuccess()方法、“错误码”、“错误简短信息”;而应用内部推荐异常抛出。
1 | 说明:关于 RPC 方法返回方式使用 Result 方式的理由: |
三、MySQL 数据库
(一)建表规约
1.【强制】数据库字符集采用utf8mb4。表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字下划线形式。字段如果是单字节,需要在末尾加上下划线;如果是多字节,则不需要。
1 | -- ---------------------------- |
2.【强制】表达是与否概念的字段,必须使用 xxx_ 的方式命名,数据类型是tinyint(1 表示是,0 表示否)。
1 | 说明:表达逻辑删除的字段名 enable_,1 表示可用,0 表示不可用。 |
3.【强制】表名不使用复数名词
1 | 说明:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。 |
4.【强制】禁用保留字,如 desc、range、match、delayed 等,请参考 MySQL 官方保留字。
5.【强制】唯一索引名为uk_
字段名;普通索引名则为idx_
字段名。
1 | 说明:uk_ 即 unique key;idx_ 即 index 的简称。 |
6.【强制】小数类型为 decimal,禁止使用 float 和 double。金额转为为分进行存储,存储类型为bigint(20).
1 | 说明:在存储的时候,float 和 double 都存在精度损失的问题,很可能在比较值的时候,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储。 |
7.【强制】如果存储的字符串长度几乎相等,使用 char 定长字符串类型。
8.【强制】 varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度
大于此值,定义字段类型为 text ,独立出来一张表,用主键来对应,避免影响其它字段索引效率。
(二)SQL 语句
1.【强制】在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。
1 | 说明:1)增加查询分析器解析成本。2)增减字段容易与 resultMap 配置不一致。3)无用字段增加网络消耗,尤其是 text 类型的字段。 |
2.【强制】不要使用count(列名)
或count(常量)
来替代count(*)
,count(*)
是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。
1 | 说明:count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行。 |
3.【强制】count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col1,col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0。
4.【强制】当某一列的值全是 NULL 时,count(col)的返回结果为 0,但 sum(col)的返回结果为NULL,因此使用 sum()时需注意 NPE 问题。
1 | 正例:可以使用如下方式来避免 sum 的 NPE 问题:SELECT IFNULL(SUM(column), 0) FROM table; |
5.【强制】使用 ISNULL() 来判断是否为 NULL 值。
1 | 说明:NULL 与任何值的直接比较都为 NULL。 |
6.【强制】不得使用外键与级联,一切外键概念必须在应用层解决。
1 | 说明:(概念解释)学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。 |
7.【强制】禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。
8.【强制】数据订正(特别是删除或修改记录操作)时,要先 select ,避免出现误删除,确认无
误才能执行更新语句。
9.【强制】对于数据库中表记录的查询和变更,只要涉及多个表,都需要在列名前加表的别名(或
表名)进行限定。
1 | 说明:对多表进行查询记录、更新记录、删除记录时,如果对操作列没有限定表的别名(或表名),并且操作列在多个表中存在时,就会抛异常。 |
10.【推荐】 in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在 1000 个之内。