[ERROR] java.util.ConcurrentModificationException

zirryo·2023년 3월 28일
1

❌ ERROR

목록 보기
5/7
post-thumbnail

Error

  1. Situation

    • for-each 문으로 List 를 순회하는 도중에 요소를 삽입함.
      (백준 16235 나무 재테크 구현 하던 중...)
    • 순환 중인 리스트에 삽입이나 삭제를 하는 경우 정상적인 순환이 일어나지 않을 수 있기 때문에 이를 막고자 Exception이 발생하게 됨.
    • add 뿐만 아니라 remove 도 같은 exception이 발생.
  2. Code

    public class Main {
        public static void main(String[] args) {
            List<String> listA = new ArrayList<>();
            init(listA);
            testAdd(listA);
      		testRemove(listA);
        }
        private static void init(List<String> list) {
            list.add("a");
            list.add("b");
            list.add("c");
        }
        private static void testAdd(List<String> list) {
            for(String s : list) {
                if(s.equals("a")) {
                    list.add("z"); // 순환 중인 list 에 요소 삽입
                }
            }
        }
        private static void testRemove(List<String> list) {
            for(String s : list) {
                if(s.equals("a")) {
                    list.remove(s); // 순환 중인 list의 요소 삭제
                }
            }
        }
    }





Solution

1. for-each 대신 for 문으로 순회

  • add
    // 1. for 문으로 순회 - add
    private static void testAdd(List<String> list) {
        for(int i=0; i< list.size(); i++) {
            if(list.get(i).equals("a")) {
                list.add("z");
            }
        }
    }
    public static void main(String[] args) {
        List<String> listA = new ArrayList<>();
        init(listA);
        testAdd(listA);
        System.out.println(listA);
    }

    문제 없이 메서드가 동작하며 [a, b, c, z] 가 출력됨.


  • remove
    // 1. for 문으로 순회 - remove
    private static void testAdd(List<String> list) {
        for(int i=0; i< list.size(); i++) {
            if(list.get(i).equals("a")) {
                list.remove(i);
            }
        }
    }
    public static void main(String[] args) {
        List<String> listA = new ArrayList<>();
        init(listA);
        testRemove(listA);
        System.out.println(listA);
    }

    add와 같은 방법으로 해결 가능하며, [b, c] 가 출력됨.


  • LinkedList
    // 1. for 문으로 순회 - LinkedList
    public class Main {
        public static void main(String[] args) {
            List<String> listB = new LinkedList<>();
            init(listB);
            testAdd(listB);
            testRemove(listB);
            System.out.println(listB);
        }
        private static void init(List<String> list) {
            list.add("a");
            list.add("b");
            list.add("c");
        }
        private static void testAdd(List<String> list) {
            for(int i=0; i< list.size(); i++) {
                if(list.get(i).equals("a")) {
                    list.add("z");
                }
            }
        }
        private static void testRemove(List<String> list) {
            for(int i=0; i< list.size(); i++) {
                if(list.get(i).equals("a")) {
                    list.remove(i);
                }
            }
        }
    }

    ArrayList와 같은 방법으로 해결 가능하며, [b, c, z] 가 출력됨.


2. break 로 loop 탈출

  • 더하거나 삭제할 요소가 하나라면 적용 가능한 방법
    // 2. break 로 loop 탈출
    private static void testAdd(List<String> list) {
        for(String s : list) {
            if(s.equals("a")) {
                list.add("z"); // 순환 중인 list 에 요소 삽입
                break;
            }
        }
    }
    private static void testRemove(List<String> list) {
        for(String s : list) {
            if(s.equals("a")) {
                list.remove(s); // 순환 중인 list 에 요소 삽입
                break;
            }
        }
    }
    public static void main(String[] args) {
        List<String> listA = new ArrayList<>();
        init(listA);
        testAdd(listA);
      	testRemove(listA);
        System.out.println(listA);
    }

    문제 없이 메서드가 동작하며 [b, c, z] 가 출력됨.


3. removeIf 메서드 활용

  • 조건에 맞는 항목 여러개를 삭제해야 할 때 사용가능한 방법
    // 3. removeIf 메서드 활용
    public static void main(String[] args) {
          List<String> listA = new ArrayList<>();
          List<String> listB = new LinkedList<>();
          init(listA);
          init(listB);
          listA.removeIf(s -> s.equals("a"));
          listB.removeIf(s -> s.compareTo("b") > 0);
          System.out.println(listA);
          System.out.println(listB);
      }
    private static void init(List<String> list) {
          list.add("a");
          list.add("b");
          list.add("c");
          list.add("a");
          list.add("d");
    }

    문제 없이 메서드가 동작하며 [b, c, d] / [a, b, a] 가 출력됨.





Additional

에러가 어떤 조건에서 발생하는 지 알아보기 위해 다양한 경우의 수를 테스트 해보았다.
그 과정에서 List.size() - 1번째에 해당하는 요소를 순회하고 있을 때 임의의 요소 하나를 삭제할 경우에는 exception 이 발생하지 않는다는 것을 알게되었다.

  • Condition

    • c가 List 크기 - 1번째 이므로 if 조건이 s.equals("c") 일 때는 에러가 발생하지 않는다.
    • if 조건이 s.equals("c") 가 아닌 s.equals("a"), s.equals("b"), s.equals("d") 일 때는 모두 ConcurrentModificationException 이 발생한다.
    • List 크기 - 1번째 순회 중이라면 삭제하는 요소는 임의로 선택하여도 상관 없으나 않으나, 요소 두 개 이상을 삭제한다면 에러가 발생할 수 있다.
      public static void main(String[] args) {
          List<String> listA = new ArrayList<>();
          init(listA);
          testRemove(listA);
          System.out.println(listA);
      }
      private static void init(List<String> list) {
          list.add("a");
          list.add("b");
          list.add("c");
          list.add("d");
      }
      // exception 이 발생하지 않는 경우1 - 요소 하나 삭제 (자신)
      private static void testRemove(List<String> list) {
          for(String s : list) {
              if(s.equals("c")) {
                  list.removeIf(s);
              }
          }
      }
      // exception 이 발생하지 않는 경우2 - 요소 하나 삭제 (임의의 다른 요소)
      private static void testRemove(List<String> list) {
          for(String s : list) {
              if(s.equals("c")) {
                  list.remove(0);
              }
          }
      }
      // exception 이 발생하는 경우1 - 요소 여러개 삭제
      private static void testRemove(List<String> list) {
          for(String s : list) {
              if(s.equals("c")) {
                  list.removeIf(s1 -> s1.compareTo("c") < 0);
              }
          }
      }
      // exception 이 발생하는 경우2 - 리스트 크기-1번째가 아닌 경우
      private static void testRemove(List<String> list) {
          for(String s : list) {
              if(s.equals("a")) {
                  list.remove(s);
              }
          }
      }                                                       

  • Cause

    remove() 후 list에 남아있는 요소가 있는지 확인하는 hasNext()에서 list의 size와 현재 커서의 index 값을 확인했을 때 더 이상 남은 element가 없음으로 인식하면서 해당 loop가 종료되기 때문이다.



🔗 reference

0개의 댓글