后知后觉,那些码了百万行代码后才知道的低级错误!!!
菜鸟的成长是艰辛的,是不堪的,是独自打拼的。但是请你和我一样不要放弃,先看下博主自己喜欢的这个小故事:
《卖油翁》
宋.欧阳修
陈康肃公尧咨善射,当世无双 ,公亦以此自矜。
尝射于家圃,有卖油翁释担而立,睨之,久而不去。
见其发矢十中八九,但微颔之。
康肃问曰:”汝亦知射乎?吾射不亦精乎?”
翁曰:”无他, 但手熟尔。”
康肃忿然曰:”尔安敢轻吾射!”
翁曰:”以我酌油知之。”
乃取一葫芦置于地,以钱覆其口,徐以杓酌油沥之,自钱孔入,而钱不湿。
因曰:”我亦无他,唯手熟尔。”康肃笑而遣之。
此与庄生所谓解牛斫轮者何异?
这是一篇浅显的文言文,初中水平,相信大家都能看懂吧,具体意思不做详细说明,我想让你记住的是“无他,但手熟尔”。
世界上哪有那么多的天赋秉异的编码高手,每个人光鲜的背后都是努力的汗水,大学时教我java基础的老师曾在课堂上说过:“你亲自敲过的代码不会背叛你”。愿你我共勉吧。
当你亲自参与项目,并实际编码行数超过100W行时(不用追求精确啦,只是为了说明量大的泛指),你会渐渐明白并且尝试去规避一些低级错误,尽管这些错误在刚入门时觉得理所当然。
- 1.多次频繁操作字符串时,不要使用String类进行字符串的拼接
循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。
说明:下例中,反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,
然后进行 append 操作,最后通过 toString 方法返回 String 对象,造成内存资源浪费。
反例:
String str = "start";
for (int i = 0; i < 100; i++) {
str = str + "hello";
}
- 2.使用集合转数组的方法,必须使用集合的 toArray(T[] array)
使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一样的数组,大小就是 list.size()。
说明:使用 toArray 带参方法,入参分配的数组空间不够大时,toArray 方法内部将重新分配
内存空间,并返回新数组地址;如果数组元素个数大于实际所需,下标为[ list.size() ]
的数组元素将被置为 null,其它数组元素保持原值,因此最好将方法入参数组大小定义与集
合元素个数一致。
正例:
List<String> list = new ArrayList<String>(2);
list.add("guan");
list.add("bao");
String[] array = new String[list.size()];
array = list.toArray(array);
反例:
直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它
类型数组将出现 ClassCastException 错误。
- 3.不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。
正例:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (删除元素的条件) {
iterator.remove();
}
}
反例:
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?
- 4.使用 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 值组合集合。
- 5.对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁
说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。
- 6.在高并发场景中,避免使用”等于”判断作为中断或退出的条件。
说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替。
反例:判断剩余奖品数量等于 0 时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数,这样的话,活动无法终止
- 7.代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改
说明:代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义。
- 8.注意 Math.random() 这个方法返回是 double 类型,注意取值的范围 0≤x<1(能够取到零值,注意除零异常),如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后取整,直接使用 Random 对象的 nextInt 或者 nextLong 方法。
- 9.获取当前毫秒数 System.currentTimeMillis(); 而不是newDate().getTime();
说明:如果想获取更加精确的纳秒级时间值,使用 System.nanoTime()的方式。在 JDK8 中,针对统计时间等场景,推荐使用 Instant 类。
- 10.catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。
说明:对大段代码进行 try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。
正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。
- 11.不要在 finally 块中使用 return。
说明:finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句。
- 12.避免出现重复的代码(Don’t Repeat Yourself),即 DRY 原则。
说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。
正例:一个类中有多个 public 方法,都需要进行数行相同的参数校验操作,这个时候请抽取:
private boolean checkParam(DTO dto) {...}
- 13.不要使用 count(列名)或 count(常量)来替代 count(),count()是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关
说明:count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行。
- 14.使用 ISNULL()来判断是否为 NULL 值。
说明:NULL 与任何值的直接比较都为 NULL。
1) NULL<>NULL 的返回结果是 NULL,而不是 false。
2) NULL=NULL 的返回结果是 NULL,而不是 true。
3) NULL<>1 的返回结果是 NULL,而不是 true。
- 15.在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。
说明:
1)增加查询分析器解析成本。
2)增减字段容易与 resultMap 配置不一致。
3)无用字段增加网络消耗,尤其是 text 类型的字段。