离娄之明,公输子之巧,不以规矩,不能成方圆。 —— 战国·邹·孟轲《孟子·离娄上》
A) Service/DAO 层方法命名规约
B) 领域模型命名规约
反例:定义为基本数据类型 Boolean isDeleted 的属性,它的方法也是 isDeleted(),框架在反向解析的时“误以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。
public class ConfusingName {
public int stock;
}
class Son extends ConfusingName {
// 不允许与父类的成员变量名称相同
public int stock;
}
public void get(String alibaba) {
if (condition) {
final int money = 666;
// ...
}
for (int i = 0; i < 10; i++) {
// 在同一方法体中,不允许与其它代码块中的 money 命名相同
final int money = 15978;
// ...
}
}
正例:startTime / workQueue / nameList / TERMINATED_THREAD_COUNT
反例:startedAt / QueueOfWork / listName / COUNT_TERMINATED_THREAD
大而全的常量类,非得使用查找功能才能定位到修改的常量,不利于理解和维护。
正例:缓存相关常量放在类 CacheConsts 下;系统配置相关常量放在类 ConfigConsts 下。
春夏秋冬
public enum SeasonEnum {
SPRING(1),
SUMMER(2),
AUTUMN(3),
WINTER(4);
int seq;
SeasonEnum(int seq){
this.seq = seq;
}
}
正例:"test".equals(object);
反例:object.equals("test");
说明:推荐使用 java.util.Objects#equals
(JDK7 引入的工具类)
注:浮点类型除外
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
}
反例:POJO 类的 gmtCreate 默认值为 new Date();但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。
说明:在方法执行抛出异常时,可以直接调用 POJO 的 toString()方法打印其属性值,便于排 查问题。
说明:公有方法是类的调用者和维护者最关心的方法,首屏展示最好;保护方法虽然只是子类关心,也可能是“模板设计模式”下的核心方法;而私有方法外部一般不需要特别关心,是一个黑盒实现;因为承载的信息价值较低,所有 Service 和 DAO 的 getter/setter 方法放在类体最后。
不过也可以学习 jdk 的源码,将私有/受保护方法放到第一个使用到这个方法的方法下面。
get/set 的目的是将变量封装起来,按照上面说的,你可能会觉得直接 obj.变量名获取或设置值不就好了吗,但是有一种情况只允许 get 不许 set,比如:单例模式,构造的时候赋默认值
说明:反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,然后进行 append 操作,最后通过 toString 方法返回 String 对象,造成内存资源浪费。
String a = "abc" + "bcd" + "efg" 会创建出 5 个对象
eg1. 由于 String 中的 char[]是 final 的,所以 b 创建好了之后,a 再修改不会导致 b 修改
String a = "a"
String b = "123" + a
a = "abc"
sout(b)
>> 123a
eg2. 被 final 修饰的不能修改引用的指针,但是可以修改指针指向的对象的数据
静态成员变量:当所有对象中的成员变量的数值相同时,此成员变量可以用 static 修饰
静态方法:当一个函数中没有调用静态变量时,此方法可以用 static 修饰;否则,此方法不能用 static 修饰
使用:
反例:POJO 类的 createTime 默认值为 new Date(),但是这个属性在数据提取时并没有置入具体值,在 更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。
Spring 0.9 版本中 BeanFactory 等代码的初始化逻辑全部写在构造方法中,看的时候心里全是 mmp,所以不要这样写了。
== 是比较内存地址
说明:asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。
Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。
扩展说一下 PECS(Producer Extends Consumer Super)原则:
jdk1.8 中聚合成 map 的方法
Map<String, String> attribute = modelCalculationResults.stream().collect(Collectors.toMap(ModelCalculationResult::getKey, ModelCalculationResult::getValue));
问题:由于调用的是 merge 方法,对 key、value 判空,所以 value 为空的时候会报错;若想装逼,可以使用下面的这种: Map<String, String> attribute = modelCalculationResults.stream().collect(HashMap::new, (map, modelCalculationResult) -> map.put(modelCalculationResult.getKey(), modelCalculationResult.getValue()), HashMap::putAll);
Iterator 是工作在一个独立的线程中,并且拥有一个 mutex(互斥) 锁
问题:为什么下面这种方式不会抛出异常?
答:因为移除的整好是倒数第二个元素;抛出异常的操作是在继续向下循环时调用next方法
时抛出,而移除了倒数第二个元素,hasNext方法
返回的 false,就不在向下循环了,从而不会抛出异常。
ArrayList<Object> objects = new ArrayList<>();
objects.add("x");
objects.add("x1");
objects.add("x2");
for (Object object : objects) {
System.out.println("object = " + object);
if (object.equals("x1")) {
objects.remove("x1");
}
}
说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的 value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效 率更高。如果是 JDK8,使用 Map.foreach 方法。 正例:values()返回的是 V 值集合,是一个 list 集合对象;keySet()返回的是 K 值集合,是 一个 Set 集合对象;entrySet()返回的是 K-V 值组合集合。
集合的有序、无序是指插入元素时,保持插入的顺序性,也就是先插入的元素优先放入集合的前面部分。
而排序是指插入元素后,集合中的元素是否自动排序。(例如升序排序)
HashMap、 HashSet、 HashTable 等 基于哈希存储方式的集合是无序的。其它的集合都是有序的。
而 TreeMap TreeSet 等集合是排序的。
TreeSet<Integer> integers = new TreeSet<>();
integers.add(5);
integers.add(2);
integers.add(3);
integers.add(4);
integers.forEach(x -> System.out.println("x = " + x));
>> 2 3 4 5
// 反例:
String[] departments = new String[] {"iERP", "iERP", "EIBU"}; // 抛出 IllegalStateException 异常
Map<Integer, String> map = Arrays.stream(departments)
.collect(Collectors.toMap(String::hashCode, str -> str));
// 正例:
String[] departments = new String[] {"iERP", "iERP", "EIBU"};
Map<Integer, String> map = Arrays.stream(departments)
.collect(Collectors.toMap(String::hashCode, str -> str, (v1, v2) -> v2));
Map<String, String> attribute = modelCalculationResults.stream().collect(Collectors.toMap(ModelCalculationResult::getKey, ModelCalculationResult::getValue));
问题:由于调用的是 merge 方法,对 key、value 判空,所以 value 为空的时候会报错;若想装逼,可以使用下面的这种:
Map<String, String> attribute = modelCalculationResults.stream().collect(HashMap::new, (map, modelCalculationResult) -> map.put(modelCalculationResult.getKey(), modelCalculationResult.getValue()), HashMap::putAll);
注:null 可以做 key
反例:如果查询无结果,返回 Collections.emptyList()空集合对象,调用方一旦进行了添加元素的操作,就会触发 UnsupportedOperationException 异常。
所以为空的话如果调用方可以对结果进行操作,那么还是返回 new ArrayList<>(0) 吧。
直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现 ClassCastException 错误。
public class TimerTaskThread extends Thread {
public TimerTaskThread() {
super.setName("TimerTaskThread");
...
}
说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM(Out Of Memory 内存溢出)。
2)CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
正例:注意线程安全,使用 DateUtils。亦推荐如下处理:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar, DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:线程安全且不可变(使用 final 修饰的)
双重检查锁定模式首先验证锁定条件(第一次检查),只有通过锁定条件验证才真正的进行加锁逻辑并再次验证条件(第二次检查)。 —— Wiki
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null) {
helper = new Helper();
}
}
}
return helper;
}
// other functions and members...
}
直觉上,这个算法看起来像是该问题的有效解决方案。然而,这一技术还有许多需要避免的细微问题。例如,考虑下面的事件序列:
class Foo {
private volatile Helper helper = null;
public Helper getHelper() {
Helper result = helper;
if (result == null) {
synchronized(this) {
result = helper;
if (result == null) {
helper = result = new Helper();
}
}
}
return result;
}
// other functions and members...
}
反例:判断剩余奖品数量等于 0 时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数,这样的话,活动无法终止。
如定义对象、变量、获取数据库连接,进行不必要的 try-catch 操作(这个 try-catch 是否可以移至循环体外)。
以下两种场景会触发类型对齐的拆箱操作:
反例:
Integer c = null;
Integer d = false ? 1 : c;
System.out.println(d); // NPE
因为表达式 1 是基本类型,会导致 c 进行拆箱,即调用 c.intValue();
说明:错误产生来源分为 A/B/C,A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付 超时等问题;B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题;C 表示错误来源于第三方服务,比如 CDN 服务出错,消息投递超时等问题;四位数字编号从 0001 到 9999,大类之间的步长间距预留 100。
说明:如果 JDK7 及以上,可以使用 try-with-resources 方式。
try (InputStream inputStream = result.get(0).getInputStream()) {
log.info("{}的文件流获取成功!", path);
return inputStream;
} catch (IOException e) {
log.error("获取流失败,{}", e);
}
说明:finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句。
说明:关于 RPC 方法返回方式使用 Result 方式的理由:
appName_logType_logName.log。
正例:mppserver 应用中单独监控时区转换异常,如:
mppserver_monitor_timeZoneConvert.log 说明:推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。
正例:logger.error(各类参数或者对象 toString + "_" + e.getMessage(), e);
说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请
思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
说明:注意日志输出的级别,error 级别只记录系统逻辑出错、异常或者重要的错误信息。
打印日志时仅打印出业务相关属性值或者调用其对象的 toString()方法。
优秀的单元测试
(1)单元测试是“白盒测试”,应该覆盖各个分支流程、异常条件。(所以他会促进我们使用设计模式,而不是一堆 if/else)
(2)单元测试面向的是一个单元(Unit),是由 Java 中的一个类或者几个类组成的单元。
(3)单元测试的运行速度一定要快!
(4)单元测试一定是可重复执行的!
(5)单元测试之间不能有相互依赖,应该是独立的!
(6)单元测试代码和业务代码同等重要,要一并维护!
略
说明:任何字段如果为非负数,必须是 unsigned。
正例:表达逻辑删除的字段名 is_deleted,1 表示删除,0 表示未删除。
说明:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数 形式,符合表达习惯。
说明:pk* 即 primary key;uk* 即 unique key;idx_ 即 index 的简称。
说明:float 和 double 在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不 正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储。
也可以将单位设置的小一点(分)
说明:其中 id 必为主键,类型为 unsigned bigint、单表时自增、步长为 1。gmt_create, gmt_modified 的类型均为 datetime 类型,前者现在时表示主动创建,后者过去分词表示被 动更新。
正例:alipay_task / force_project / trade_config
正例:商品类目名称使用频率高,字段长度短,名称基本一成不变,可在相关联的表中冗余存 储类目名称,避免关联查询。
说明:不要以为唯一索引影响了 insert/delete/update 的速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律(凡是可能出错的事就一定会出错),必然有脏数据产生。
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90%以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。
索引就像字典一样;A 下面对应的一些单词,B 下面对应的一些单词...,而这个 ABC 再索引中就是它的长度,比如
于 于佳 于佳鑫
如果索引长度是 1 => 于:[于,于佳,于佳鑫]
如果索引长度是 2 => 于:[于] 于佳: [于佳,于佳鑫]
如果索引长度是 3 => 于:[于] 于佳: [于佳] 于佳鑫: [于佳鑫]
以此类推,长度越长,查询速度越快(区分度越高),但是内存消耗也越高,所以要找一个平衡点。
说明:索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 file_sort 的情况,影响查询性能。
正例:
where a=? and b=? order by c; 索引:a_b_c
where a=? order by c
反例:
索引中有范围查找,那么索引有序性无法利用,如:WHERE a>10 ORDER BY b; 索引 a_b 无法排序。
where b=? order by c 由于最左原则不会使用索引
说明:MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过 特定阈值的页数进行 SQL 改写。
正例:先快速定位需要获取的 id 段,然后再关联:
SELECT a.* FROM 表 1 a, (select id from 表 1 where 条件 LIMIT 100000,20 ) b where a.id=b.id
正例:如果 where a=? and b=? ,a 列的几乎接近于唯一值,那么只需要单建 idx_a 索引即 可。 说明:存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如:where a>? and b=? 那么即使 a 的区分度更高,也必须把 b 放在索引的最前列。
说明:count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行。
由于讲的是 mybatis,日后再补
不要写一个大而全的数据更新接口。传入为 POJO 类,不管是不是自己的目标更新字段,都进行 update table set c1=value1,c2=value2,c3=value3; 这是不对的。执行 SQL 时,不要更新无改动的字段,一是易出错;二是效率低;三是增加 binlog 存储。
。。。