编程规约
一 命名风格
1. 【强制】代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
反例:_name / __name / $name / name_ / name$ / name__
说明:
正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,纯拼音命名方式更要避免采用。
renminbi / alibaba / taobao / youku / hangzhou 等国际通用的名称,可视同英文。
DaZhePromotion [打折] / getPingfenByName() [评分] / int 某变量 = 3
3.
【强制】类名使用 UpperCamelCase 风格,但以下情形例外:DO / BO / DTO / VO / AO / PO / UID 等。
JavaServerlessPlatform / UserDO / XmlService / TcpUdpDeal / TaPromotion
javaserverlessplatform / UserDo / XMLService / TCPUDPDeal / TAPromotion
正例:
localValue / getHttpMessage() / inputUserId
常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
MAX_STOCK_COUNT / CACHE_EXPIRED_TIME
MAX_COUNT / EXPIRED_TIME
定义整形数组 int[] arrayDemo;
在 main 参数中,使用 String args[]来定义。
在本文
MySQL
规约中的建表约定第一条,表达是与否的值采用
is_xxx
的命名方式,所以,需要在 <resultMap>设置从
is_xxx
到
xxx
的映射关系。
定义为基本数据类型 Boolean isDeleted 的属性,它的方法也是 isDeleted(),RPC 框架在反向解 析的时候,“误以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。
应用工具类包名为 com.alibaba.ai.util、类名为 MessageUtils(此规则参考 spring 的框架结构)
子类、父类成员变量名相同,即使是 public 类型的变量也是能够通过编译,而局部变量在同一方法内的不同代码块中同名也是合法的,但是要避免使用。对于非 setter/getter 的参数名称也要避免与成员变量名称相同。
public class ConfusingName {
public int age;
// 非 setter/getter 的参数名称,不允许与本类成员变量同名
public void getData(String alibaba) {
if(true) {
final int money = 531;
// ...
}
for (int i = 0; i < 10; i++) {
// 在同一方法体中,不允许与其它代码块中的 taobao 命名相同
final int money = 615;
// ...
}
}
}
class Son extends ConfusingName {
// 不允许与父类的成员变量名称相同
public int age;
}
11.【强制】杜绝完全不规范的缩写,避免望文不知义。
AbstractClass“缩写”命名成 AbsClass;condition“缩写”命名成 condi,此类随意缩写严重降低了代码的可阅读性。
在 JDK 中,表达原子更新的类名为:AtomicReferenceFieldUpdater。
int a 的随意命名方式。
startTime / workQueue / nameList / TERMINATED_THREAD_COUNT
startedAt / QueueOfWork / listName / COUNT_TERMINATED_THREAD
将设计模式体现在名字中,有利于阅读者快速理解架构设计理念。
public class OrderFactory;
接口方法签名 void commit(); 接口基础常量 String COMPANY = “alibaba”;
接口方法定义
public abstract
void f();
【强制】
对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用 Impl 的后缀与接口区别。
CacheServiceImpl 实现 CacheService 接口。
【推荐】
如果是形容能力的接口名称,取对应的形容词为接口名(通常是–able 的形容词)。
AbstractTranslator 实现 Translatable 接口。
枚举其实就是特殊的类,域成员均为常量,且构造方法被默认强制是私有。
枚举名字为 ProcessStatusEnum 的成员名称:SUCCESS / UNKNOWN_REASON。
1) 获取单个对象的方法用 get 做前缀。
2) 获取多个对象的方法用 list 做前缀,复数形式结尾如:listObjects。
3) 获取统计值的方法用 count 做前缀。
4) 插入的方法用 save/insert 做前缀。
5) 删除的方法用 remove/delete 做前缀。
6) 修改的方法用 update 做前缀。
B) 领域模型命名规约
1) 数据对象:xxxDO,xxx 即为数据表名。
2) 数据传输对象:xxxDTO,xxx 为业务领域相关的名称。
3) 展示对象:xxxVO,xxx 一般为网页名称。
4) POJO 是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO。
二 常量定义
1. 【强制】不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。
反例:String key = “Id#taobao_” + tradeId;
cache.put(key, value);
// 缓存 get 时,由于在代码复制时,漏掉下划线,导致缓存击穿而出现问题
2. 【强制】在 long 或者 Long 赋值时,数值后使用大写的 L,不能是小写的 l,小写容易跟数字 1 混淆,造成误解。
Long a = 2l; 写的是数字的 21,还是 Long 型的 2。
大而全的常量类,杂乱无章,使用查找功能才能定位到修改的常量,不利于理解和维护。
缓存相关常量放在类 CacheConsts 下;系统配置相关常量放在类 ConfigConsts 下。
2) 应用内共享常量:放置在一方库中,通常是子模块中的 constant 目录下。
反例:易懂变量也要统一定义成应用内共享常量,两位工程师在两个类中分别定义了“YES”的变量:
类 A 中:public static final String YES = “yes”;
类 B 中:public static final String YES = “y”;
A.YES.equals(B.YES),预期是 true,但实际返回为 false,导致线上问题。
3) 子工程内部共享常量:即在当前子工程的 constant 目录下。
4) 包内共享常量:即在当前包下单独的 constant 目录下。
5) 类内共享常量:直接在类内部 private static final 定义。
如果存在名称之外的延伸属性应使用 enum 类型,下面正例中的数字就是延伸信息,表示一年中的第几个季节。
public enum SeasonEnum {
SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4);
private int seq;
SeasonEnum(int seq) {
this.seq = seq;
}
public int getSeq() {
return seq;
}
}
三 代码格式
if (空格 a == b 空格)
运算符包括赋值运算符=、逻辑运算符&&、加减乘除符号等。
如果使用 tab 缩进,必须设置 1 个 tab 为 4 个空格。IDEA 设置 tab 为 4 个空格时,请勿勾选
Use tab character
;而在 eclipse 中,必须勾选
insert spaces for tabs
。
(涉及 1-5 点)
public static void main(String[] args) {
// 缩进 4 个空格
String say = "hello";
// 运算符的左右必须有一个空格
int flag = 0;
// 关键词 if 与括号之间必须有一个空格,括号内的 f 与左括号,0 与右括号不需要空格
if (flag == 0) {
System.out.println(say);
}
// 左大括号前加空格且不换行;左大括号后换行
if (flag == 1) {
System.out.println("world");
// 右大括号前换行,右大括号后有 else,不用换行
} else {
System.out.println("ok");
// 在右大括号后直接结束,则必须换行
}
}
6. 【强制】注释的双斜线与注释内容之间有且仅有一个空格。
=
new
String
();
first
=
1000000000000L
;
second
=
(
int
)
first
+
2
;
2)运算符与下文一起换行。
3)方法调用的点符号与下文一起换行。
4)方法调用中的多个参数需要换行时,在逗号后进行。
5)在括号前不要换行,见反例。
StringBuilder sb = new StringBuilder();
// 超过 120 个字符的情况下,换行缩进 4 个空格,点号和方法名称一起换行
sb.append("Jack").append("Ma")...
.append("alibaba")...
.append("alibaba")...
.append("alibaba");
反例:
StringBuilder sb = new StringBuilder();
// 超过 120 个字符的情况下,不要在括号前换行
sb.append("Jack").append("Ma")...append
("alibaba");
// 参数很多的方法调用可能超过 120 个字符,不要在逗号前换行
method(args1, args2, args3, ...
, argsX);
9. 【强制】方法参数在定义和传入时,多个参数逗号后边必须加空格。
正例:下例中实参的 args1,后边必须要有一个空格。
method(args1, args2, args3);
除注释之外的方法签名、左右大括号、方法内代码、空行、回车及任何不可见字符的总行数不超过80 行。
代码逻辑分清红花和绿叶,个性和共性,绿叶逻辑单独出来成为额外方法,使主干代码更加清晰; 共性逻辑抽取成为共性方法,便于复用和维护。
int one = 1;
long two = 2L;
float three = 3F;
StringBuilder sb = new StringBuilder();
增加 sb 这个变量,如果需要对齐,则给 one、two、three 都要增加几个空格,在变量比较多的情
任何情形,没有必要插入
多个空行
进行隔开。
四 OOP规约
1. 【强制】避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。
可变参数必须放置在参数列表的最后。(提倡同学们尽量不用可变参数编程)
public List<User> listUsers(String type, Long… ids) {…}
java.net.URLDecoder 中的方法 decode(String encodeStr) 这个方法已经过时,应该使用双参数decode(String source, String encode)。接口提供方既然明确是过时接口,那么有义务同时提供新的接口;作为调用方来说,有义务去考证过时方法的新实现是什么。
“test”.equals(object);
object.equals(“test”);
推荐使用 java.util.Objects#equals(JDK7 引入的工具类)。
对于 Integer var = ? 在
-128 至 127
范围内的赋值,Integer 对象是在 IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。
浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。二进
制无法精确表示大部分的十进制小数。
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
if (a == b) {
// 预期进入此代码快,执行其它业务逻辑
// 但事实上 a==b 的结果为 false
}
Float x = Float.valueOf(a);
Float y = Float.valueOf(b);
if (x.equals(y)) {
// 预期进入此代码快,执行其它业务逻辑
// 但事实上 equals 的结果为 false
}
正例:
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");
}
(2) 使用 BigDecimal 来定义值,再进行浮点数的运算操作。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
if (x.equals(y)) {
System.out.println("true");
}
9. 【强制】定义数据对象 DO 类时,属性类型要与数据库字段类型相匹配。
数据库字段的 bigint 必须与类属性的 Long 类型相对应。
某个案例的数据库表 id 字段定义类型 bigint unsigned,实际类对象属性为 Integer,随着 id 越来越大,超过 Integer 的表示范围而溢出成为负数。
BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。如:BigDecimal g = new BigDecimal(0.1f); 实际的存储值为:0.10000000149
优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法,此方法内部其实执行了 Double 的 toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。
BigDecimal recommend1 = new BigDecimal("0.1");
BigDecimal recommend2 = BigDecimal.valueOf(0.1);
11.关于基本数据类型与包装数据类型的使用标准如下:
【强制】
所有的 POJO 类属性必须使用包装数据类型。
【强制】
RPC 方法的返回值和参数必须使用包装数据类型。
【推荐】
所有的局部变量使用基本数据类型。
POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证。
数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。
比如显示成交总额涨跌情况,即正负 x%,x 为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。
POJO 类的 createTime 默认值为 new Date(),但是这个属性在数据提取时并没有置入具体值,在
注意 serialVersionUID 不一致会抛出序列化运行时异常。
在方法执行抛出异常时,可以直接调用 POJO 的 toString()方法打印其属性值,便于排查问题。
框架在调用属性 xxx 的提取方法时,并不能确定哪个方法一定是被优先调用到。
公有方法是类的调用者和维护者最关心的方法,首屏展示最好;保护方法虽然只是子类关心,也可
能是“模板设计模式”下的核心方法;而私有方法外部一般不需要特别关心,是一个黑盒实现;因为承载
的信息价值较低,所有 Service 和 DAO 的getter/setter 方法放在类体最后。
对象 clone 方法默认是浅拷贝,若想实现深拷贝需覆写 clone 方法实现域对象的深度遍历式拷贝。
任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块解耦。思考:如果
是一个 private 的方法,想删除就删除,可是一个 public 的 service 成员方法或成员变量,删除一下,不
得手心冒点汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大,无限制的到处跑,那么你
会担心的。
String 已覆写 hashCode 和 equals 方法,所以我们可以愉快地使用 String 对象作为 key 来使用。
subList 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList 而是 ArrayList 的一个视图,对
如果查询无结果,返回
Collections.emptyList()
空集合对象,调用方一旦进行了添加元素的操作,就会触发 UnsupportedOperationException
异常。
直接使用
toArray
无参方法存在问题,此方法返回值只能是
Object[]
类,若强转其它类型数组将出现 ClassCastException
错误。
List<String> list = new ArrayList<>(2);
list.add("guan");
list.add("bao");
String[] array = list.toArray(new String[0]);
使用
toArray
带参方法,数组空间大小的
length
:
)
等于
0
,动态创建与
size
相同的数组,性能最好。
)
大于
0
但小于
size
,重新创建大小等于
size
的数组,增加
GC
负担。
)
等于
size
,在高并发情况下,数组创建完成之后,
size
正在变大的情况下,负面影响与上相同。
)
大于
size
,空间浪费,且在
size
处插入
null
值,存在
NPE
隐患。
在 ArrayList#addAll 方法的第一行代码即 Object[] a = c.toArray(); 其中 c 为输入集合参数,如果为 null,则直接抛出异常。
asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。
String[] str = new String[] { "yang", "hao" };
List list = Arrays.asList(str);
第一种情况:list.add(“yangguanbao”); 运行时异常。
也会随之修改,反之亦然。
扩展说一下 PECS(Producer Extends Consumer Super)原则:第一、频繁往外读取内容的,适合用<? extends T>。第二、经常往里插入的,适合用<? super T>
三个条件如下
菱形泛型,即 diamond,直接使用<>来指代前边已经指定的类型。
// diamond 方式,即<>
HashMap<String, String> userCache = new HashMap<>(16);
// 全省略方式
ArrayList<User> users = new ArrayList(10);
HashMap 使用 HashMap(int initialCapacity) 初始化。
initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。
HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容量 7 次被迫扩大,resize 需要重建 hash 表,严重影响性能。
keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的 value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8, 使用 Map.forEach 方法。
values()返回的是 V 值集合,是一个 list 集合对象;keySet()返回的是 K 值集合,是一个 Set 集合对象;entrySet()返回的是 K-V 值组合集合。
集合类 | Key | Value | Super | 说明 |
Hashtable | 不允许为 null | 不允许为 null | Dictionary | 线程安全 |
ConcurrentHashMap | 不允许为 null | 不允许为 null | AbstractMap | 锁分段技术(JDK8:CAS) |
TreeMap | 不允许为 null | 允许为 null | AbstractMap | 线程不安全 |
HashMap | 允许为 null | 允许为 null | AbstractMap | 线程不安全 |
由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,而事实上,存储 null 值时会抛出 NPE 异常。
有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次序是一定的。如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet 是 order/sort。
六 并发处理
资源驱动类、工具类、单例工厂类都需要注意。
创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
自定义线程工厂,并且根据外部特征进行分组,比如机房信息。
线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
Executors 返回的线程池对象的弊端如下:
FixedThreadPool
和
SingleThreadPool
:
CachedThreadPool
:
如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar, DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。
尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。
线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。
如果在 lock 方法与 try 代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。
如果 lock 方法在 try 代码块之内,可能由于其它方法抛出异常,导致在 finally 代码块中,unlock 对未加锁的对象解锁,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),抛出 IllegalMonitorStateException 异常。
在 Lock 对象的 lock 方法实现中可能抛出 unchecked 异常,产生的后果与说明二相同。
Lock
对象的
unlock
方法在执行时,它会调用
AQS
的
tryRelease
方法(取决于具体实现类),如果当前线程不持有锁,则抛出 IllegalMonitorStateException
异常。
如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次。
乐观锁在获得锁的同时已经完成了更新操作,校验逻辑容易出现漏洞,另外,乐观锁对冲突的解决策略有较复杂的要求,处理不当容易造成系统压力或数据异常,所以资金相关的金融敏感信息不建议使用乐观锁更新。
注意,子线程抛出异常堆栈,不能在主线程 try-catch 到。
Random 实例包括 java.util.Random 的实例或者 Math.random()的方式。
在 JDK7 之后,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要编码保证每个线程持有一个实例。
如果是 count++操作,使用如下类实现:AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。
这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。
七 控制语句
注意
break
是退出
switch
语句块,而
return
是退出方法体。
即使只有一行代码,避免采用单行的编码方式:if (condition) statements;
如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替。
判断剩余奖品数量等于 0 时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数,这样的话,活动无法终止。
如果非使用 if()…else if()…else…方式表达逻辑,避免后续代码维护困难,
【强制】
请勿超过 3 层。
超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现,其中卫语句即代码逻辑先考虑失败、异常、中断、退出等直接返回的情况,以方法多个出口的方式,解决代码中判断分支嵌套的问题,这是逆向思维的体现。
很多 if 语句内的逻辑表达式相当复杂,与、或、取反混合运算,甚至各种方法纵深调用,理解成本非常高。如果赋值一个非常好理解的布尔变量名字,则是件令人爽心悦目的事情。
赋值点类似于人体的穴位,对于代码的理解至关重要,所以赋值语句需要清晰地单独成为一行。
取反逻辑不利于快速理解,并且取反逻辑写法必然存在对应的正向逻辑写法。
八 注释规约
在 IDE 编辑窗口中,Javadoc 方式会提示相关注释,生成 Javadoc 可以正确输出相应注释;在 IDE 中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。
对子类的实现要求,或者调用注意事项,请一并说明。
代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义。
代码被注释掉有两种可能性:1)后续会恢复此段代码逻辑。2)永久不用。前者如果没有备注信息,难以知晓注释动机。后者建议直接删掉(代码仓库已然保存了历史代码)。
TODO
):(标记人,标记时间,[预计处理时间])
FIXME
):(标记人,标记时间,[预计处理时间])
九 其他
不要在方法体内定义:Pattern pattern = Pattern.compile(“规则”);
注意如果是 Boolean 包装类对象,优先调用 getXxx()的方法。
如果 var 等于 null 或者不存在,那么${var}会直接显示在页面上。
如果想获取更加精确的纳秒级时间值,使用 System.nanoTime()的方式。在 JDK8 中,针对统计时间等场景,推荐使用 Instant 类。
日期格式化时,
yyyy
表示当天所在的年,而大写的
YYYY
代表是
week in which year(JDK7
之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的 YYYY
就是下一年。另外需要注意:
- 表示月份是大写的 M
- 表示分钟则是小写的 m
- 24 小时制的是大写的 H
- 12 小时制的则是小写的 h
根据 MVC 理论,视图的职责是展示,不要抢模型和控制器的活。
对于垃圾代码或过时配置,坚决清理干净,避免程序过度臃肿,代码冗余。
对于暂时被注释掉,后续可能恢复使用的代码片断,在注释代码上方,统一规定使用三个斜杠(///) 来说明注释掉代码的理由。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/9535.html