実際のビジネスプロジェクト開発では、与えられたリストから条件を満たさない要素を削除するという操作は誰でも経験があるのではないでしょうか?
多くの学生はそれを達成するためのさまざまな方法をすぐに思いつきますが、あなたが考えるこれらの実装方法は人体や動物に無害ですか?一見普通に見える操作の多くは実は罠であり、多くの初心者は注意しないと罠に陥る可能性があります。
誤って踏んだ場合:
コードは実行時に直接例外をスローし、エラーを報告します。これは不幸中の幸いです。少なくとも、それは可能です。時間内に発見され解決される
コードはエラーなく実行されますが、さまざまな奇妙な問題がビジネス ロジックに不可解に現れます。この問題に注意を払わないと、これはさらに悲劇的です。その後のビジネスに隠れた危険を引き起こす可能性があります。
それでは、どのような実装方法があるのでしょうか?どの実装方法に問題がある可能性がありますか?ここで一緒に話し合いましょう。ここで議論されているのは、フェンネル豆で「フェンネル」という単語をどのように書くかという問題ではなく、非常に深刻で実際的であり、無視されがちな技術的な問題であることに注意してください。
仮想の需要シナリオ:
ユーザー リスト allUsers がある場合、下位部門が dev である担当者をリストから削除し、残りの担当者情報を返す必要があります
多くの初心者が最初に考えるのは、for ループを 1 つずつ確認して、条件を満たすものを削除することです。とても簡単です...
1 分でコードを書き終えました:
public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) { for (UserDetail user : allUsers) { // 判断部门如果属于dev,则直接剔除 if ("dev".equals(user.getDepartment())) { allUsers.remove(user); } } // 返回剩余的用户数据 return allUsers; }
それから自信を持って実行ボタンをクリックしました:
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)
え?何してるの?なぜ例外がスローされるのでしょうか?
注意しないと罠にはまってしまうので、なぜ例外がスローされるのかを分析してみましょう。
原因分析:
JAVA の foreach 構文の実際の処理は、反復子 Iterator に基づいています。
ループの開始時に、最初に反復インスタンスが作成され、この反復インスタンスの ExpectedModCount にコレクションの modCount が割り当てられます。イテレータが hashNext() / next() を使用して次の要素をトラバースするたびに、modCount 変数と ExpectModCount 値が等しいかどうかをチェックします。等しい場合はトラバースが返され、そうでない場合は ConcurrentModificationException がスローされます。トラバースを終了します。
ループ内で要素を追加または削除する場合は、コレクションの add() メソッドと Remove() メソッドを直接呼び出して、modCount を増減させますが、これらのメソッドは、反復インスタンスの結果: 反復インスタンスの ExpectedModCount と modCount の値が等しくない場合、ConcurrentModificationException 例外がスローされます。
え? foreach メソッドが機能しないので、元の添字ループ メソッドを使用して実行すると、エラーは発生しませんよね。まだとても簡単です...
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; }
コードは一度で完成します。実行して、処理された出力を確認してください:
{id=2, name='李思' 、部門='開発'}
{id=3、名前='王五'、部門='製品'}
{id=4、名前='鉄竹'、部門='午後'}
案の定、エラーは報告されず、結果も出力されました。完璧です~
お待ちください?本当にこれでいいのでしょうか?
コードのロジックは、"dev".equals(Department) かどうかを判断することですが、出力結果に、削除する必要がある、Department=dev のようなデータがまだ残っているのはなぜですか?
これが実際のビジネス プロジェクトの場合、開発段階でエラーが報告されず、その結果が慎重に検証されてから生産ラインに流れないと、ビジネス ロジックに異常が発生する可能性があります。
この現象の具体的な理由を見てみましょう。
原因分析:
リスト内の要素と添字の間には、実際には強い結合関係はなく、単に位置の順序が対応しているだけであることがわかっています。リストが変更されると、各要素に対応する添字が以下のように変更される場合があります。
次に、リストから要素を削除した後、削除された要素以降のすべての要素がリストから変更されます。リストの添字は前方に移動されますが、for ループのポインタ i は常に後方に累積されます。次の要素を処理するときに、一部の要素が欠落して処理されない可能性があります。
例えば下図のように、i=0の場合はA要素の削除が必要と判断して直接削除しますが、再循環する場合はi=1となり、この時、リスト内の要素の位置が前方に移動するため、B 要素は次のようになります。添字 0 の元の位置が直接欠落しました:
これで、次のようになります。上記のコードが実行後に見逃される理由を知ってください。La~
上記の 2 つの落とし穴を見た後、正しく適切な操作方法は何でしょうか?
え?それは正しい? foreach メソッドでもイテレータが使用されると言いましたが、これは実際にはトラップ操作ですか?ここでイテレータ パターンが正しい方法であると言われているのはなぜですか?
虽然都是基于迭代器,但是使用逻辑是不一样的,看下代码:
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; }
以上がJAVAの単純なforループで例外を回避するにはどうすればよいですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。