6

遍历中删除

List或Queue等数据结构中,如何一边遍历一遍删除?

1. 常犯错误

ArrayList

可能没遇到坑过的人会用增强for循环这么写:

public static void main(String[] args) {
1 List<Integer> list = new ArrayList<>();
2    list.add(1);
3    list.add(2);
4    list.add(3);
5
6    for (Integer value : list) {
7        if (value.equals(2)) {
8            list.remove(value);
9        }
10    }
11    System.out.println(list);
}

但是一运行,结果却抛 java.util.ConcurrentModificationException 异常
即并发修改异常

image.png

简单看看异常是怎么产生的:

首先,从抛出异常的位置入手,发现原因是因为: modCount 的值不等于 expectedModCount

image.png

modCount值是什么呢?

The number of times this list has been structurally modified. Structural modifications are those that change the size of the list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.

从源码注释中可以看出来,表示的是 修改该表结构的次数, 包括修改列表大小等。

所以原因很简单了: 刚开始循环时 modCount 与期待值相等

image.png

但是在循环中进行了删除操作后,modeCount 进行了自增。
image.png

最后由于 modCount 的值不等于 expectedModCount,抛出异常。

HashMap:

HashMap中也同理, 如果我们简单使用forEach,然后remove。同样报
java.util.ConcurrentModificationException

1    HashMap<Integer,String> hashMap = new HashMap<>();
2    hashMap.put(1, "张三");
3    hashMap.put(2, "李四");
4    hashMap.put(3, "王五");
5    hashMap.forEach((key, value) -> {
6        logger.info("当前值: key" + key +  " value " + value) ;
7        if ((hashMap.get(key) ==  "李四")) {
8            hashMap.remove(key);
9        }
10    });
11    logger.info(hashMap.toString());

image.png

原因也一样: 在 modCount 与期待值不一样, 发生了并发修改异常

image.png

Queue

我们再来看看队列

1    Queue<Integer> queue = new LinkedList<>();
2    queue.offer(1);
3    queue.offer(2);
4    queue.offer(3);
5
6    logger.info("删除前" + queue.toString());
7    for(Integer value : queue) {
8        logger.info("当前值" + value);
9        if (value.equals(2)) {
11           queue.remove(value);
12      }
13    }
14    logger.info("删除后" + queue.toString());

如果我们用 LinkList 结构作为队列,可以成功删除。

但是我们按理来说循环3次, 实际上只循环2次。如下图。 所以可能会导致漏判断。(有空在进行研究)

image.png

如果我们把 LinkList 换成 一些功能性比较好的队列,就不会有这种情况。 比如 带有ReentrantLock 锁的LinkedBlockingQueue。

 Queue<Integer> queue = new LinkedBlockingQueue<>();

就能正常循环3次, 并正常删除。
image.png

迭代器遍历

那么应该使用哪些遍历呢?

比较推荐使用Iterator迭代器进行遍历:
使用它的 iterator.remove()方法不会产生并发修改错误。

1 List<Integer> list = new ArrayList<>();
2        list.add(1);
3        list.add(2);
4        list.add(3);
5        Iterator<Integer> iterator = list.iterator();
6        while (iterator.hasNext()) {
7            Integer integer = iterator.next();
8            if (integer == 2) {
9                logger.info("删除" + integer);
10                iterator.remove();
11            }
12        }
13        System.out.println(list);

原因可以从源码中找到: 每次删除一个元素,都会将 modCount 的值重新赋值给expectedModCount,这样2个变量就相等了,不会触发异常。

image.png

同时好处还有一个,无论是什么结构的数据,一般都可以用迭代器访问。

从JDK1.8开始,可以使用removeIf()方法来代替 Iterator的remove()方法实现一边遍历一边删除,IDEA中也会提示:
其原理也是使用迭代器的remove。
image.png

mysql5.6版本添加unique失败

项目使用jpa自动生成数据库, 添加unique字段的时候,发现报错:

image.png

Specified key was too long; max key length is 767 bytes

反复测试后,发现,Long对象的 unique 可以添加成功,String 对象的不行,然后我把 String 对象加上length限制

@Column(unique = true, nullable = false, length = 20)
private String acctName;

结果就成功了。

原因:

数据库查看:

image.png

原来是因为指定字段长度过长。未指定长度的字符串,默认为255 varchar,utf8mb4字符集每个 varchar 为4bytes,即为总长255x4=1020bytes,大于了767bytes。

因此,unique 字段的最大长度为767/4 = 191 varchar。(注:utf8mb4字符集)

MySQL 5.6 版(及之前的版本)中的 767 字节是 InnoDB 表的规定前缀限制。在 MySQL 5.7 版(及更高版本)中,此限制已增加到3072 字节。

可以选择升级版本,或者设置长度
image.png


weiweiyi
761 声望111 粉丝