全网AI资源网站搭建

电脑端+手机端+微信端=数据同步管理

免费咨询热线:13114099110

网站代码优化-【干货】循环体内的犯错经历及解决办法(一)

发布时间:2022-11-12 10:01   浏览次数:次   作者:派坤优化
网站代码优化-【干货】循环体内的犯错经历及解决办法(一)

网站代码优化-【干货】循环体内的犯错经历及解决办法(一)

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 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小时内与您取得联系。