最近在翻看阿里巴巴Java开发手册,对照自己的代码规范,发现了代码中存在的不少缺陷。比如手册强制规定所有的POJO类属性必须使用包装类型。因为使用包装类型在使用起来会比较麻烦,有时候需要多加一些判断,我很好奇是否这些规定都落地实施了,于是到阿里云下载了几个SDK查看了一下,这个规范确实是都做到了,但也发现了有些强制要求的规范没有实施,比如不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。
反例:
String key = "Id#taobao_" + tradeId; cache.put(key, value);
手册里面强制规定不允许此类代码的出现,但是我发现阿里云的SDK中也还是出现这种字符串未定义就直接拼接的情况。下面先看一些平时需要多加注意的地方,后面再讲一下foreach导致的问题。
强制所有的相同类型的包装对象之间值的比较,全部使用equals方法的比较。在-128至127范围内赋值,Integer对象是在IntegerCache.cache产生,会复用已有对象,在这个区间内的Integer值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用equals方法进行判断。
最好不要一个常量类维护所有常量,而要按常量功能进行归类,分开维护。比如在constants包下面,缓存相关的常量放在CacheConsts下;系统配置的相关常量放在类ConfigConsts等等。
使用工具类Arrays.asList()把数组转化成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UnsupportedOperationException异常。原因是asList返回对象是一个Arrays内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转化接口,后台的数据仍是数组。改变一个另一个也会改变。比如str[0] = "jc",那么list.get(0)也会改变。
推荐使用entrySet集合遍历Map类集合KV,而不是KeySet方式进行遍历。KeySet其实是遍历了两次,一次是转为Iterator对象,另一次是从hashMap中取出key所对应的value。而entrySey只是遍历了一次就把key和value都放到了entry中,效率更高。如果是JDK8,使用Map.foreach方法。value()是返回V值集合,是一个list集合对象;keySet()返回的是K值集合,是一个Set集合对象;entrySet()返回的是K-V值组合集合。
正则表达式相关。使用正则表达式的预编译编译功能,可以有效加快正则匹配速度。Pattern要定义为staic final静态变量。以避免多次预编译。
private static final Pattern pattern = Pattern.compile(regexRule); private void func(...) { Matcher m = pattern.matcher(content); if (m.matches()) { ... } }
终于开始讲正题了,手册中规定不要在foreach循环里进行元素的remove/add操作,remove元素请使用Iterator方式,如果是并发操作,需要对Iterator对象加锁。先来看下面这两段代码
public static void main(String[] args) { Listlist = new ArrayList<>(); list.add("1"); list.add("2"); // 正确的删除方式 // Iterator iterator = list.iterator(); // while (iterator.hasNext()){ // String item = iterator.next(); // if (item.equals("2")){ // System.out.println(item); // iterator.remove(); // } // } // 错误的删除方式 for (String item : list) { if (item.equals("1")){ System.out.println(item); list.remove(item); } } }
其实你运行这两段代码,都不会出错,第一种使用手册推荐的方式,没什么问题。第二段代码的循环其实只执行了一次,但是没有报错;如果将其中的判断的数字1改成2,程序就会抛出异常了。
下面我们来仔细分析一些第二段代码,在终端中切换到该类的目录下,输入命令javac xxx.java,就会在同一个包下生成一个名字相同的xxx.class,javac是一种编译器,将代码编写成class文件的工具,在AndroidStudio上打开xxx.class,可以看到第二段代码直接变成
ArrayList var1 = new ArrayList(); var1.add("1"); var1.add("2"); Iterator var2 = var1.iterator(); while(var2.hasNext()) { String var3 = (String)var2.next(); if (var3.equals("1")) { System.out.println(var3); var1.remove(var3); } }
可以看到foreach遍历集合,实际上内部使用的是Iterator。代码先判断是否hasNext(),然后再去调用next()方法,这两个函数是引起问题的关键。remove()调用的还是list的remove()方法。下面来一起来看一下remove中的fastRemove()方法。
private void fastRemove(int var1) { ++this.modCount; int var2 = this.size - var1 - 1; if (var2 > 0) { System.arraycopy(this.elementData, var1 + 1, this.elementData, var1, var2); } this.elementData[--this.size] = null; }
我们可以看到第二行处有个modCount,这里我们先记住fail-fast机制是Java集合中的一种错误检测机制。通过记录modCount参数来实现。
然后我们也可以看到add方法中也有
public void add(E var1) { this.checkForComodification(); try { int var2 = this.cursor; ArrayList.this.add(var2, var1); this.cursor = var2 + 1; this.lastRet = -1; this.expectedModCount = ArrayList.this.modCount; } catch (IndexOutOfBoundsException var3) { throw new ConcurrentModificationException(); } } private void checkForComodification() { if (ArrayList.this.modCount != this.modCount) { throw new ConcurrentModificationException(); } }
下面再来看一下next,hasNext方法,在内部类Itr类当中
private class Itr implements Iterator{ int cursor; int lastRet = -1; int expectedModCount; Itr() { this.expectedModCount = ArrayList.this.modCount; } public boolean hasNext() { return this.cursor != ArrayList.this.size; } public E next() { this.checkForComodification(); int var1 = this.cursor; if (var1 >= ArrayList.this.size) { throw new NoSuchElementException(); } else { Object[] var2 = ArrayList.this.elementData; if (var1 >= var2.length) { throw new ConcurrentModificationException(); } else { this.cursor = var1 + 1; return var2[this.lastRet = var1]; } } } final void checkForComodification() { if (ArrayList.this.modCount != this.expectedModCount) { throw new ConcurrentModificationException(); } }
在next方法第二行处可以看到checkForComodification方法中的modCount != expectedModCount,就会抛出ConcurrentModificationException异常,一开始modCount和expectedModCount是相等的,就是list内部的元素个数。在hasNext方法中,cursor != list.size()的时候,hasNext返回true。next方法中checkForComodification()就是函数抛出异常的原因。
下面再仔细分析一下把第二段代码为什么不会报错(当判断的字符串为1的时候),该代码执行完第一次循环以后:
modCount = 3,因为执行了一次remove(),调用了里面的fastRemove()方法。
expectedModCount = 2,因为一开始的list.size()大小为2。
cursor = 1,执行了一次next()。
size = 1,移除了字符串1。
所以程序在执行hasNext方法的时候返回(cursor != size) false,相当于总共就循环了一次,程序也不会报错。
当把判断的字符串改成2的时候,执行完第二层循环后:
modCount = 3,因为执行了一次remove(),调用了里面的fastRemove()方法。
expectedModCount = 2,因为一开始的list.size()大小为2。
cursor = 2,因为执行了两次next()
size = 1
此时hasNext方法返回(cursor != size) true,接着就会继续执行next方法,然后就会检查modCount != expectedModCount,如果不相等就抛出ConcurrentModificationException异常,此时两个值不相等,抛出了异常,相当于执行第三次循环的时候,在next方法中抛出了异常。
到此为止就很清楚了,foreach循环实际上使用的还是Iteartor迭代器,但是移除的时候通过list的remove方法去便很有可能抛出ConcurrentModificationException异常,如果使用迭代器的remove()方法就不会有什么问题,当然多线程情况下也是有可能出问题,所以要添加并发锁。
fail-fast 机制,即快速失败机制,是java集合(Collection)中的一种错误检测机制。通过记录modCount参数来实现,当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出ConcurrentModificationException异常。fail-fast机制并不保证在不同步的修改下一定会抛出异常,它只是尽最大努力去抛出,所以这种机制一般仅用于检测bug。
在调试的过程中,也碰到一下其他小问题。我是在Android Studio上调试的,一开始调试的时候,查看源码的时候查看ArrayList的源码是Android API 27 Platform上的。但是调试的时候显示源码不匹配,但是程序确实是运行下去了,该报bug的也报bug。
而且如果把上面的第二段代码中的判断改成2,程序运行报错,抛出异常,但是在API 27的时候显示的源码却怎么也推理不出抛异常的结果,它的hasNext方法如下,这样是不会抛出异常的,但是事实是程序的确抛出异常了。
后面问题总于找到了,我虽然在Android Studio上写代码,但是我新建普通Java类,写了一个main方法来验证程序的运行过程,所以实际上程序并未运行在任何安卓平台上,所以ArrayLIst类虽然是Android API 27 上的,但是最终运行的源码是rt.jar中的ArrayList。在这里选择1.8 rt.jar 最后就能正确调试了。
同时我也在安卓手机(7.1.1)上试了一下第二段代码,发现把第二段代码中的判断修改为1的时候,程序抛出异常,而当改成2的时候,程序却可以正常运行,抛出异常的原因都是类似的,有兴趣的可以自己去研究android源码看看。
高质量编程视频shangyepingtai.xin