In actual business project development, everyone should be familiar with the operation of removing elements that do not meet the conditions from a given list, right?
Many students can immediately think of many ways to achieve it, but are these implementation methods you think of harmless to humans and animals? Many seemingly normal operations are actually traps, and many novices may fall into them if they are not careful.
If you accidentally step on it:
The code will directly throw an exception and report an error when running. This is a blessing in misfortune. At least it can be discovered and solved in time
The code runs without error, but various strange problems appear inexplicably in the business logic. This is more tragic, because if this problem is not paid attention to, it may cause hidden dangers for subsequent business.
So, what are the implementation methods? Which implementation methods may have problems? Let’s discuss it together here. Please note that what is discussed here is not the issue of how to write the word "fennel" in fennel beans, but a technical issue that is very serious, practical and easy to be ignored.
Hypothetical demand scenario:
Given a user list allUsers, it is necessary to remove the personnel whose subordinate department is dev from the list, and return the remaining personnel information
The first thought of many novices is to check the for loop one by one and then eliminate those that meet the conditions~ so easy...
I finished writing the code in 1 minute:
public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) { for (UserDetail user : allUsers) { // 判断部门如果属于dev,则直接剔除 if ("dev".equals(user.getDepartment())) { allUsers.remove(user); } } // 返回剩余的用户数据 return allUsers; }
Then I clicked the execute button with confidence:
java.util.ConcurrentModificationException: null at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909) at java.util.ArrayList$Itr.next(ArrayList.java:859) at com.veezean.demo4.UserService.filterAllDevDeptUsers(UserService.java:13) at com.veezean.demo4.Main.main(Main.java:26)
Eh? what are you doing? Why is the exception thrown?
If you don’t pay attention, you will step into a trap. Let’s analyze why an exception is thrown.
Cause analysis:
The actual processing of JAVA’s foreach syntax is based on the iterator Iterator.
At the beginning of the loop, an iteration instance will first be created, and the expectedModCount of this iteration instance is assigned the modCount of the collection. Whenever the iterator uses hashNext() / next() to traverse the next element, it will check whether the modCount variable and expectedModCount values are equal. If they are equal, the traversal will be returned; otherwise, a ConcurrentModificationException will be thrown to terminate the traversal.
If you add or delete elements in a loop, you directly call the add() and remove() methods of the collection, causing the modCount to increase or decrease, but these methods will not modify the expectedModCount in the iteration instance, resulting in If the values of expectedModCount and modCount in the iteration instance are not equal, a ConcurrentModificationException exception is thrown.
Huh? Since the foreach method doesn't work, then use the original subscript loop method to do it. You won't get an error, right? It's still very easy...
public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) { for (int i = 0; i < allUsers.size(); i++) { // 判断部门如果属于dev,则直接剔除 if ("dev".equals(allUsers.get(i).getDepartment())) { allUsers.remove(i); } } // 返回剩余的用户数据 return allUsers; }
The code is completed in one go. Execute it and see the processed output:
{id=2, name='李思', department=' dev'}
{id=3, name='王五', department='product'}
{id=4, name='Tiezhu', department='pm'}
Sure enough, no error is reported, the result is also output, perfect~
Wait? Is this really OK?
The logic of our code is to determine if "dev".equals(department), but in the output result, why is there still data like department=dev that should be eliminated?
If this is a real business project, if no errors are reported during the development phase, and the results are not carefully verified, and then flow to the production line, it may cause abnormal business logic.
Let’s take a look at the specific reasons for this phenomenon.
Cause analysis:
We know that there is actually no strong binding relationship between the elements in the list and the subscripts. It is just a corresponding relationship of position order. After the elements in the list are changed, , the subscript corresponding to each element may change, as shown below:
Then, after deleting an element from the List, all elements after the deleted element in the List The subscripts are moved forward, but the pointer i of the for loop is always accumulated backwards. When processing the next one, some elements may be missed and not processed.
For example, as shown in the figure below, when i=0, it is judged that the A element needs to be deleted, and it is deleted directly; when recirculating, i=1, at this time, because the position of the element in the list moves forward, the B element becomes The original position with the subscript 0 was directly missed:
So here you can know why the above code will be missed after execution. La~
After seeing the above two pitfalls, what should be the correct and appropriate way to operate?
Eh? That's right? Didn't I just say that the foreach method also uses iterators, but is it actually a trap operation? Why is it said here that the iterator pattern is the correct way?
虽然都是基于迭代器,但是使用逻辑是不一样的,看下代码:
public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) { Iterator<UserDetail> iterator = allUsers.iterator(); while (iterator.hasNext()) { // 判断部门如果属于dev,则直接剔除 if ("dev".equals(iterator.next().getDepartment())) { // 这是重点,此处操作的是Iterator,而不是list iterator.remove(); } } // 返回剩余的用户数据 return allUsers; }
执行结果:
{id=3, name='王五', department='product'}
{id=4, name='铁柱', department='pm'}
这次竟然直接执行成功了,且结果也是正确的。为啥呢?
在前面foreach方式的时候,我们提过之所以会报错的原因,是由于直接修改了原始list数据而没有同步让Iterator感知到,所以导致Iterator操作前校验失败抛异常了。
而此处的写法中,直接调用迭代器中的remove()方法,此操作会在调用集合的remove(),add()方法后,将expectedModCount重新赋值为modCount,所以在迭代器中增加、删除元素是可以正常运行的。,所以这样就不会出问题啦。
言简意赅,直接上代码:
public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) { allUsers.removeIf(user -> "dev".equals(user.getDepartment())); return allUsers; }
作为JAVA8开始加入的Stream,使得这种场景实现起来更加的优雅与易懂:
public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) { return allUsers.stream() .filter(user -> !"dev".equals(user.getDepartment())) .collect(Collectors.toList()); }
既然前面说了不能直接循环的时候执行移除操作,那就先搞个list对象将需要移除的元素暂存起来,最后一起剔除就行啦 ~
嗯,虽然有点挫,但是不得不承认,实际情况中,很多人都在用这个方法 —— 说的就是你,你是不是也曾这么写过?
public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) { List<UserDetail> needRemoveUsers = new ArrayList<>(); for (UserDetail user : allUsers) { if ("dev".equals(user.getDepartment())) { needRemoveUsers.add(user); } } allUsers.removeAll(needRemoveUsers); return allUsers; }
或者:
public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) { List<UserDetail> resultUsers = new ArrayList<>(); for (UserDetail user : allUsers) { if (!"dev".equals(user.getDepartment())) { resultUsers.add(user); } } return resultUsers; }
The above is the detailed content of How to avoid exceptions in simple for loops in JAVA?. For more information, please follow other related articles on the PHP Chinese website!