阿里巴巴编码规范认证:深入解析foreach循环中的remove/add操作及最佳实践
背景

阿里云上有阿里巴巴编码标准认证。我估计时间成本很低,而且多认证也没什么坏处,所以就花了1毛钱报名了。注册了这个认证之后,可以在链接下下载编码规范,然后参加考试应该就可以了。
48页的说明书其实每次读起来都需要花费一些时间,因为每次读的时候我都会发现上面有一些我不相信的东西。我需要证明这一点。过去已经被证明的东西,可能会因为JDK版本的升级和迭代而需要继续被证明。下面是一些证明过程。
案例一
标准文本
[强制] 不要在 foreach 循环中执行元素删除/添加操作。请使用Iterator方法删除元素。如果并发操作需要锁定Iterator对象。
正面例子:
列表列表 = new ArrayList();
列表.add(“1”);
列表.add(“2”);
Iteratoriteraot = list.interator();
while(iterator.hasNext()){
字符串项 =iterator.next();
if (删除元素的条件) {
迭代器.remove();
反例:
for(字符串项:列表){
if("1".equals(item)){
列表.删除(项目);
注:上面代码的执行结果肯定会出乎大家的意料。那么尝试用“2”替换“1”,会是相同的结果吗?
证明
1.首先根据计数器示例文本运行测试(test1)
列表中有2个元素,去掉1个,就剩下1个。这应该是大多数人所期待的。
2. 按照说明操作,将“1”替换为“2”来运行测试(test2)
这里,“2”并没有按照预期被移除,而是抛出了并发修改异常。点击异常的地方
3. 按照异常提示进行操作。找到抛出异常的地方,看看是哪个方法抛出了异常:
4.对源码进行分析:
抛出并发修改异常的条件是modCount!=expectedModCount。
5.根据这个条件,我做出一个猜想:一次操作将两者的值改为不同,因为这里调用了remove修改方法。自然,我猜测是remove方法所做的修改。我们看一下remove方法的源码:
6. 果然源码中添加了modCount++,但是expectedModCount没有修改。猜想被证明了。运行remove后,需要判断for(String item: list)。这时候就会调用迭代器的next方法。这样,我就明白为什么上面的test2会抛出异常了。那么我们思考一下为什么test1不抛出异常呢?
7.我们来调试test1的情况1
运行remove方法后,可以看到此时modCount!=expectedModCount,但是此时只执行了hasNext(),并判断了cursor != size。此时不会执行next方法,因此不会产生异常。下面再次使用列表时,迭代器为新的迭代器,且modCount=expectedModCount;
综上所述
如果列表在for循环中调用remove方法,将会抛出并发修改异常。但是,如果只修改第一项然后返回,则属于异常,因为此时不会调用next方法来判断modCount和expectedModCount是否相等。
使用代码规范推荐的迭代器,底层的remove方法会一起修改modCount和expectedModCount,所以单线程不会出现并发问题。作为类的成员变量,在多线程情况下是否会被修改是不确定的。
思考问题
执行以下代码的结果是什么?
案例2
标准文本
【强制】在JDK7及以上版本中,Comparotor实现类必须满足以下三个条件,否则Arrays.sort和Collections.sort将抛出IllegalArgumentException。
注:三个条件如下
1)x和y的比较结果与y和x的比较结果相反。
2)x>y,y>z,则x>z。
3)x=y,则x和z的比较结果与y和z的比较结果相同。
反例:下面的例子没有处理相等的情况。交换两个对象的判断结果不互斥,不满足第一个条件。实际使用中可能会出现异常情况。
新比较器(){
@覆盖
publicint比较(学生o1,学生o2){
returno1.getId()>o2.getId()?1:-1;
证明
1、我们先看一下反例在实际使用中会抛出哪些异常。
2、测试发现Collections.sort和Arrays.sort都会抛出错误,说必须实现Comparable接口,而不是Comparator接口。 Comparable接口不需要满足规范中提到的自反性、传递性和对称性。
那么规范为什么这么说呢?
3.我查看了Collections类的源码,确实有几个方法用到了Comparator类。包括反转、二分查找、最大值、最小值等多种排序。原始排序后面可以跟有 Comparator 参数。
4、但是,我在源码中传入两个参数后,跑了几个例子,并没有抛出非法参数异常。于是我又在源码中寻找线索。
Collections.sort底层使用Arrays.sort,Arrays.sort底层使用TimSort。 TimSort 会在两个地方抛出这个异常。看源码的时候,就是二分查找合并结果的时候。
5、果然,使用该排序时出现了非法参数异常。
6、具体为什么会出现异常,我设置断点,使用debug跟踪TimSort源码。一般来说,有些排序使用if(x
总结
实现Comparator的compare方法必须满足自反性、对称性和传递性。
案例3
标准文本
分析
1、从上面的总结来看,线程安全的Map的key和value不能为null。如果线程不安全,则可以为 null。大家都知道映射键需要进行哈希处理。散列 null 会导致空指针吗?
带着这个疑问,我打开了HashMap的源码,看到hash方法判断为null。如果为null,则哈希值为0。所以不会出现NPE
2、TreeMap的put方法不对key做任何判断,然后调用compare方法,会抛出NPE。
3.那么如果不对key进行特殊处理,肯定会抛出NPE。应该没有疑问。为什么有些值为空时会出现NPE?
从上面的源码我们可以知道ConcurrentHashMap就是这么处理的,这算是强制的。
4.并且Hashtable也是强制的。
5、为什么要设计线程安全的容器,让key和value不能为null?我在网上找到了班级设计师Lea的原话。主要意思是map需要实现containsKey和containsValue方法。这个方法实际上是使用get(XX)来实现null的情况。如果为null,则很难区分是因为不存在还是值为null。
6.什么是线程不安全,就是在单线程中处理。以下是TreeMap中containsValue的处理。如果输入为 null 并且有一个对象值为 null,则为 true。
总结
常用的四种Map中线程安全的Map的key和value不能为null。线程不安全的值可以为 null。 TreeMap 的 key 不能为 null。
案例4
标准文本
【强制】多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中一个任务没有捕获抛出的异常,其他任务就会自动终止。如果在处理定时任务时使用ScheduledExecutorService,就不会出现这样的问题。
证明
1.首先让Timer运行多个TimeTask,使得其中一个不捕获抛出的异常。
这段代码的意思是在10秒内运行两个定时任务,其中定时任务每10ms前后打印一次。另一个抛出异常,抛出异常后都停止了。
2、从Timer源码可以看出,本质上是通过队列来维护多个任务的。处理的时候,整个过程都是try catch作为一个整体。然后发生异常,整个过程停止。
3、再次验证ScheduledExecutorService的使用。可以看到抛出异常的线程运行一次后就停止了,而另一个线程继续运行。
4、从源码中可以看出,如果某个工作线程出现问题,会直接从工作队列中移除,不影响其他。
总结
与Timer相比,ScheduledExecutorService可以避免多个任务之间出现问题时产生的副作用。
案例5
标准文本
【强制】使用工具类Arrays.asList()将数组转换为集合时,不能使用其与修改集合相关的方法。它的 add/remove/clear 方法将抛出 UnsupportedOperationException 异常。注意:asList的返回对象是一个Arrays内部类,没有实现集合修改方法。 Arrays.asList体现了适配器模式,仅转换接口,后台数据仍然是数组。
String[] str = new String[] { "yang", "hao" };
列表列表 = Arrays.asList(str);第一种情况:list.add("yangguanbao");运行时异常。第二种情况:str[0] = "changed";也将进行相应修改,反之亦然。
证明
1.Arrays.asList() 将数组转换为集合,然后添加元素。测试已运行并按预期抛出异常。
跟踪源码可知,asList虽然生成了一个ArrayList,但它并不是java.util.ArrayList,而是在Arrays中自定义的。此类不支持这些更新操作。
总结
请注意集合类中返回子集或转换类型的操作。它们可能返回内部定义的类,而不是我们通常使用的类。这些类对某些操作施加限制。
思考问题
以下测试等级体现了哪些规范?