Java中代码Bug记录--泛型失效、数组删除、HashMap死循环
最近在工作的过程中,遇到了不少奇怪自己或者同事的Bug,都是一些出乎意料的,不太容易发现的,记录一下来帮助可能也遇到了这些Bug的人
1. 编译时泛型校验失效
Map<String, String> nameToType = new HashMap<>();
nameToType.put( "testName", 123 ); // java: 不兼容的类型: int无法转换为java.lang.String
上面的代码,我们很容易看出来,无法通过编译,因为Map的value需要的是一个String,但我们传的是一个int。但我只要稍微改一下:
package generic;
import java.util.HashMap;
import java.util.Map;
public class RecContext<T>{
private Map<String, String> nameToType = new HashMap<>();
public Map<String, String> getNameToType(){
return nameToType;
}
public void setNameToType( Map<String, String> nameToType ){
this.nameToType = nameToType;
}
public static void main( String[] args ){
RecContext recContextRaw = new RecContext();
recContextRaw.getNameToType().put( "testName", 123 );
}
}
同样是一个value要求为String的Map, 放到一个对象里面就可以通过编译了
不过这不是一个普通的对象,这个Class本身带泛型,但我们在使用的时候,没有指定这个泛型,也就是IDEA中常常报的错,说你在Raw Use这个类型
也正是因为Raw Use了这个类,所以导致它的泛型属性也被类型擦除了,具体可以看StackOverflow-What is a raw type and why shouldn't we use it?
这篇文章里面是这么说的
In simpler terms, when a raw type is used, the constructors, instance methods and non-static fields are also erased
简单地讲,当使用了原始类型,构造器、实例方法和非静态的字段都会被擦除
我们有一个老的项目里面有不少这样的Raw use,也正好有另外一个同事把一个其他的类型插入了这个Map,于是就报了一个类型转换错误,同事们都很震惊,认为这个Map不是有泛型吗,怎么可能插入别的类型,一番排查,才发现是这个问题
解决方法
- 不要Raw use类,会使编译校验失效,也有很多其他的理由,可以参考上面那篇文章,我不使用的原因是因为IDEA每次都会发出告警。还有如果发现这个类一直在Raw use也没啥问题,说明这个类本身就不需要泛型,可以考虑是一下是不是需要重构一下
- 我的告警配置的是黄色的背景,看起来很惹眼
- IDEA的告警会是Raw use,不可能的条件判断,可以更换写法的代码(完全不影响效果),甚至可能是bug(之前碰到过,修复的时候发现IDEA已经提示了,但是可能别人的告警不是很明显,没看到)
- 如果是真的Raw use了类,那检查类型是否错误的责任就落到我们自己的头上,确保不要传进错误的类型,确保取出来的类型不要转换错误
2. 数组删除中的“刻舟求剑”
线上代码中有这样一个逻辑,想从帖子列表中筛选出一部分帖子然后从原列表中删除,其代码逻辑如下:
import java.util.ArrayList;
import java.util.List;
public class ListDelTest{
public static void main( String[] args ){
List<Integer> jobIds = new ArrayList<>();
for( int i = 0; i < 10; i++ ){
jobIds.add( i );
}
List<Integer> delIndex = new ArrayList<>();
for( int i = 0; i < jobIds.size(); i++ ){
// 线上是其他的筛选逻辑,在这我们用偶数代替
if( jobIds.get( i ) % 2 == 0 ){
delIndex.add( i );
}
}
for( int i = 0; i < jobIds.size(); i++ ){
if( delIndex.contains( i ) ){
jobIds.remove( jobIds.get( i ) );
}
}
System.out.println( jobIds );
// [1, 2, 4, 5, 7, 8]
}
}
可以看到输出结果中,并不符合我们的预期,我们希望的是把所有的偶数删除,结果中不但有偶数,而且一些奇数也不见了
这个其实很容易理解,因为我们记得位置是0,2,4,6,8
原始数据是0,1,2,3,4,5,6,7,8,9
当我们删除了0时,数据变成了1,2,3,4,5,6,7,8,9
这时候我们再去删除index是2的值,结果就把3这个值给删除了
解决方法
- 使用stream filter collect,这种其实不是原地删除,而是新建了一个List, 不过我们把这个List覆盖原来的引用,效果一样
- 常见的边遍历边删除的方法,使用Iterator,可以避免
ConcurrentModificationException
异常
int i = 0;
Iterator<Integer> iterator = jobIds.iterator();
while( iterator.hasNext() ){
iterator.next();
if( delIndex.contains( i ) ){
iterator.remove();
}
i++;
}
System.out.println( jobIds );
// [1, 3, 5, 7, 9]
- 倒着删除,这样不会影响我们已经记录过的index,我记得当时我在华为OD面试的时候一个面试官问的我的一个问题,我没答出来,他告诉我的答案
for( int i = jobIds.size() - 1; i > -1; i-- ){
if( delIndex.contains( i ) ){
jobIds.remove( jobIds.get( i ) );
}
}
3. Java8 HashMap死循环
线上同事上线了一个新的过滤器,我们的过滤器是并发执行的,比如,帖子敏感词过滤,会将帖子分成10份,用10个线程分别执行,执行完了就把结果放到一个公共的map中
很明显,这个map是有线程安全问题的,因为会有多个线程同时去put,然而,因为没考虑到,同事使用了普通的HashMap
线上的现象就是,每过一段时间,某个机器的CPU使用率就到了90%以上,需要重启
按照我们的理解,就算是有并发问题,怎么会使CPU使用变高呢
我们都背过,在Java1.7中的HashMap会因为并发插入产生死循,1.8使用尾插法代替头插法解决了死循环
可我们用的是Java1.8,看起来好像还是有死循环的问题
具体原因我就不仔细分析了,是在链表转换树或者对树进行操作的时候会出现线程安全的问题
解决方法
- 多线程还是需要使用ConcurrentHashMap
参考
[1] StackOverflow-What is a raw type and why shouldn't we use it?
本文来自博客园,作者:songtianer,转载请注明原文链接:https://www.cnblogs.com/songjiyang/p/17582799.html