网站代码优化-【干货】循环体内的犯错经历及解决办法(一)
1. 犯错的经历 1.1 故事背景
最近有一个类似背景的需求:
我通过一系列的操作得到了一批学生的考试成绩数据,现在需要过滤分数大于95的学生列表。
擅长写bug,已经将三五除以二完成了代码的编写:
@Test
public void shouldCompile() {
for (int i = 0; i < studentDomains.size(); i++) {
if (studentDomains.get(i).getScore() < 95.0) {
studentDomains.remove(studentDomains.get(i));
}
}
System.out.println(studentDomains);
}
在测试数据中的四名学生中,有两名分数在95分以上的学生被成功筛选出来。测试成功了,他们打卡了。
[StudentDomain{id=1, name='李四', subject='科学', score=95.0, classNum='一班'}, StudentDomain{id=1, name='王六', subject='科学', score=100.0, classNum='一班'}]
1.2 好像下不了班了!
X年经验的直觉告诉我,事情并没有那么简单。
但是自检没有问题,是不是写法有问题?然后我用另一种方式写它(增强的for循环):
@Test
public void commonError() {
for (StudentDomain student : studentDomains) {
if (student.getScore() < 95.0) {
studentDomains.remove(student);
}
}
System.out.println(studentDomains);
}
好家伙网站代码优化,这个一定要试试,直接报错:.
1.3 普通的for循环真的好吗?
为了判断普通的for循环是否有问题,我在原代码中加入了执行次数的打印:
@Test
public void shouldCompile() {
System.out.println("studentDomains.size():" + studentDomains.size());
int index = 0;
for (int i = 0; i < studentDomains.size(); i++) {
index ++;
if (studentDomains.get(i).getScore() < 95.0) {
studentDomains.remove(studentDomains.get(i));
}
}
System.out.println(studentDomains);
System.out.println("执行次数:" + index);
}
这个加法不可思议,我的 .size() 明明是 4,怎么循环体只执行了 2 次。
更巧合的是,执行的两个循环的数据恰好满足我的过滤条件,所以会让我误以为【要求已经完成】。
2.问题分析
一一分析,我们先来看看为什么普通的for循环执行的频率比我们想象的要少。
2.1 减少普通for循环的数量
其实稍微有点开发经验的人应该都知道这个原因:在循环删除元素后,List的索引会自动变化,List.size()得到的List长度也会实时更新,所以会导致遗漏被删除元素后一个索引处的元素。
例如:如果你在循环到第一个元素的时候删除了它,那么第二个循环应该访问的是第二个元素,但是这次它实际上访问的是原来List的第三个元素,因为第一个元素被删除了,而原来的第3个 成为当前的 2nd 元素,这导致了元素的省略。
2.2 增强for循环抛出错误
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
只要不为空,程序的执行路径就会走到else路径,最后调用()方法:
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null;
}
在 () 方法中,见第 2 行 [给变量的值加 1]。
通过编译代码可以看到,在实际执行增强的for循环时,使用的核心方法是()和next()。
并且 next() 方法调用 on():
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
见 throw new() 然后就可以关闭案例了:
因为上面的()方法修改了值,所以这里肯定会抛出异常。
3.正确的方式
既然知道了为什么普通的for循环和增强的for循环都不能用了,那我们就从这两个地方开始吧。
3.1 优化普通for循环
我们知道使用普通 for 循环有问题的原因是,当我们仍在使用原始坐标进行操作时,数组坐标发生了变化。
@Test
public void forModifyIndex() {
for (int i = 0; i < studentDomains.size(); i++) {
StudentDomain item = studentDomains.get(i);
if (item.getScore() < 95.0) {
studentDomains.remove(i);
// 关键是这里:移除元素同时变更坐标
i = i - 1;
}
}
System.out.println(studentDomains);
}
使用倒序法不需要改变坐标,因为:如果去掉后一个元素,不会影响前一个元素的坐标,也不会导致一个元素被跳过。
@Test
public void forOptimization() {
List studentDomains = genData();
for (int i = studentDomains.size() - 1; i >= 0; i--) {
StudentDomain item = studentDomains.get(i);
if (item.getScore() < 95.0) {
studentDomains.remove(i);
}
}
System.out.println(studentDomains);
}
3.2 使用过()
@Test
public void iteratorRemove() {
Iterator iterator = studentDomains.iterator();
while (iterator.hasNext()) {
StudentDomain student = iterator.next();
if (student.getScore() < 95.0) {
iterator.remove();
}
}
System.out.println(studentDomains);
}
您一定想知道为什么迭代器的 () 方法有效。同样,让我们看一下源代码:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
我们可以看到:每次执行()方法时,都会赋值给这个值,这样两个变量就相等了。
3.3()
懂童鞋的人应该都能想到这个方法,这里就不赘述了。
@Test
public void streamFilter() {
List studentDomains = genData();
studentDomains = studentDomains.stream().filter(student -> student.getScore() >= 95.0).collect(Collectors.toList());
System.out.println(studentDomains);
}
3.4 .() [推荐]
在JDK1.8中,及其子类新增加了()方法网站代码优化,用于按照一定的规则过滤集合中的元素。
@Test
public void removeIf() {
List studentDomains = genData();
studentDomains.removeIf(student -> student.getScore() < 95.0);
System.out.println(studentDomains);
}
查看()方法的源码,你会发现底层的()方法也用到了:
default boolean removeIf(Predicate super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
4.总结
如果你仔细仔细地阅读这篇文章,最大的感悟应该是:源码还是靠谱的!
4.1 几句话
其实在我刚从事Java开发的时候,这个问题就一直困扰着我。当时我只是想解决问题,所以采用了一种很傻的方式:
新建一个List,遍历旧List,将满足条件的元素放入新元素中。在这种情况下,当时的任务终于完成了。
现在想想。几年前,如果你花点时间想想为什么不能直接(),再多问几个原因,你可能会比现在好多了。
当然,只要你意识到这一点,任何时候都不晚,让我们一起努力吧!
4.2 文中代码示例
*请认真填写需求信息,我们会在24小时内与您取得联系。