背景
阿⾥云上有个阿⾥巴巴编码规范认证,我估算⼀下时间成本很低,多个认证也没什么坏处,就花了1分钱报了个名。这个认证报名后就可以下载链接下的编码规范,然后参加个考试应该就OK了。
共48页的规范实际上每读⼀遍都是要花⼀些时间的,因为每读⼀遍就会发现上⾯有些东西我不信。我需要去证明。过去证明过的因为JDK版本升级迭代有可能需要继续证明。下⾯是其中的⼀些证明过程。 案例1规范原⽂
【强制】不要在foreach循环⾥进⾏元素的remove/add操作。remove元素请使⽤Iterator⽅式,如果并发操作需要对Iterator对象加锁。正例:
List Iterator for(String item : list) { if(\"1\".equals(item)) { list.remove(item); }} 说明:以上代码的执⾏结果肯定会出乎⼤家的意料,那么试⼀下把\"1\"换成\"2\",会是同样的结果吗?证明 1.先按照反例例⽂运⾏测试(test1) list⾥两个元素,remove掉⼀个,剩下1个。这应该是符合⼤多数⼈预期的。2.按照说明把\"1\"换成\"2\"运⾏测试(test2) 这⾥没有按照预期remove掉\"2\",⽽是抛出了并发修改异常。点击到异常的地⽅3.根据异常提⽰。找到抛出异常代码的地⽅查看是哪个⽅法抛出异常: 4.对源码做⼀个解析: 抛出并发修改异常的条件是modCount!=expectedModCount。 5.根据这个条件,我做⼀个推测:在⼀个操作⾥把这两者的值改的不⼀样了,因为这⾥调⽤了remove修改⽅法。我⾃然就推测是remove⽅法做的修改,来看remove⽅法的源码: 6.果然,源码中将modCount++,但是expectedModCount并没有修改。证明了推测。运⾏完remove后需要判断for(String item : list) ,这时候调⽤了迭代器的next⽅法。这样我理解了上⾯test2⾥为什么会抛出异常。那么再来思考下test1为什么不抛出异常呢?7.我们来debug⼀下test1的情况1 运⾏完remove⽅法后,可看到这时候modCount!=expectedModCount,但是这时候只执⾏了hasNext(),判断了cursor != size,这时候不会执⾏next⽅法,所以不会产⽣异常。⽽下⾯再⽤到list时迭代器是新的迭代器,会把modCount=expectedModCount; 结论 如果list在for循环⾥调⽤remove⽅法是会抛出并发修改异常的,但是如果只修改了第1个就返回的情况是个例外,因为这时候不会调⽤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⽐较结果相同。 反例:下例中没有处理相等的情况,交换两个对象判断结果并不互反,不符合第⼀个条件,在实际使⽤中可能会出现异常。new Comparotor public int compare(Student o1, Student o2) { return o1.getId()>o2.getId()?1:-1; }}证明 1.我们先来看看反例在实际使⽤中会抛出什么异常。 2.测试发现不论是Collections.sort还是Arrays.sort都抛出错误说必须实现Comparable接⼝⽽不是Comparator接⼝。⽽Comparable接⼝是不需要满⾜规范⾥所说的⾃反性、传递性和对称性的。 那为什么规范⾥会这么说呢? 3.我查了Collections类的源码,确实有⼏个⽅法⽤到了Comparator类。包括反转、⼆分查找、最⼤值、最⼩值和⼏个sort等。后⾯的原来sort是可以后⾯接Comparator参数的。 4. 然⽽我⽤源码的两个参数形式传⼊后,运⾏了⼏个例⼦并没有抛出⾮法参数异常。于是我⼜在源码中找线索。 Collections.sort底层⽤了Arrays.sort,Arrays.sort底层⽤了TimSort,TimSort有两处会抛出这个异常,看源码是在⼆分查找merge结果的时候。 5.使⽤这个sort果然是发⽣了⾮法参数异常。 6.具体为什么会发⽣异常,我打了断点,使⽤debug跟了⼀下TimSort源码,⼤体是有部分排序使⽤了if(x 分析 1.从上⾯总结来看线程安全的Map的key和value都不能为null。线程不安全的可以为null。⼤家都知道map的key要进⾏hash。对null进⾏hash不会空指针吗?带着这个疑问,打开HashMap的源码看到hash⽅法有对null做判断,如果null则hash值为0。所以不会NPE 2.TreeMap的put⽅法没有对key做任何的判断,然后会调⽤compare⽅法,这⾥会抛出NPE 3.那么对于key如果不做特殊处理,肯定是要抛出NPE的,应该没有什么疑问了。为什么有的value为空也会NPE呢? 从上⾯源码可知道ConcurrentHashMap就是这么处理的,算是强制。 4.⽽Hashtable也是强制。 5.为什么线程安全的容器要设计成key和value不能为null呢?在⽹上找到了类设计者Lea的原话,主要表达的意思是因为map需要实现containsKey和containsValue⽅法。这个⽅法对于null的情况实际上是⽤get(XX)来实现的,如果为null就不好区分到底是因为不存在还是值就是null。 6.⽽线程不安全的就是按单线程处理,下⾯是TreeMap⾥containsValue的处理,如果为输⼊为null,并且有个对象值为null就是true了。总结 四种常⽤map中线程安全的Map的key和value都不能为null。线程不安全的value都可以为null。TreeMap的key不能为null。案例4规范原⽂ 【强制】多线程并⾏处理定时任务时,Timer 运⾏多个 TimeTask 时,只要其中之⼀没有捕获 抛出的异常,其它任务便会⾃动终⽌运⾏,如果在处理定时任务时使⽤ScheduledExecutorService 则没有这个问题。 证明 1.先让Timer 运⾏多个 TimeTask,让其中之⼀没有捕获 抛出的异常 这段代码的意思是在10秒内运⾏两个定时任务,其中⼀个定时任何每10ms做前后打印。另外⼀个抛出异常,结果抛出异常后两个都停⽌了。2.从Timer源代码可知,本质上多个任务通过⼀个队列来维护。处理的时候整个过程整体try catch。那么⼀个出异常整个过程都停⽌了。3.再验证使⽤ScheduledExecutorService的情况, 可看到抛出异常的线程运⾏了⼀次之后就停⽌了,另外⼀个线程⼀直继续运⾏。 4. 从源码可知如果⼀个⼯作线程出现了问题会直接从⼯作队列⾥移除,不影响其他的。 总结 ScheduledExecutorService相⽐Timer能避免多个任务之间的出现问题时的副作⽤。案例5规范原⽂ 【强制】使⽤⼯具类Arrays.asList()把数组转换成集合时,不能使⽤其修改集合相关的⽅ 法,它的 add/remove/clear ⽅法会抛出 UnsupportedOperationException 异常。说明:asList 的返回对象是⼀个 Arrays 内部类,并没有实现集合的修改⽅法。Arrays.asList 体现的是适 配器模式,只是转换接⼝,后台的数据仍是数组。String[] str = new String[] { \"yang\ List list = Arrays.asList(str);第⼀种情况:list.add(\"yangguanbao\"); 运⾏时异常。第⼆种情况:str[0] = \"changed\"; 也会随之修改,反之亦然。证明 1.Arrays.asList()把数组转换成集合后添加元素,测试运⾏,果然抛出异常 跟踪源码可知道虽然asList⽣成的是ArrayList,但它并不是java.util.ArrayList,⽽是Arrays⾥⾃定义的。这个类不⽀持这些更新操作。 总结 ⼩⼼集合类中返回⼀个⼦集或者转换类型的操作,可能返回的是内部定义的类,不是我们平时⽤的类,这些类中对⼀些操作做了限制。思考题 下⾯测试类体现了规范的哪⼀条? 因篇幅问题不能全部显示,请点此查看更多更全内容