Language/JAVA

[JAVA] 반복문에서 배열요소 삭제

idleday 2023. 11. 2. 10:56

무한댓글 구현 + 삭제여부 확인하며 새로 알게된 개념 메모

 

참고) 아래 에서 이어지는 게시물입니다

 

계층형 쿼리

공지사항 게시판에 댓글 기능을 도입한다고 한다. 댓글-대댓글로 2 depth인 줄 알았으나, 대대댓글 3 depth가 될 수도 있고 아닐수도 있고 변경가능성 농후..하므로 일단 3 depth 기준으로 조회 쿼리를

idleday.tistory.com

 

 

 

목표

지난번엔 대댓글 정렬까지 구현했다.

 

이제 삭제된 댓글들 중 

삭제되지않은(살아있는) 후손대댓글이 하나라도 있는 경우 목록에서 살려둘거고,

자신도 삭제됐고 후손들도 싸그리 삭제되어있는 경우 목록에서 제외하려고 한다. 

 

살아있는 후손이 있는지는 댓글 전체목록을 돌면서 찾는 즉시 리스트에서 삭제하려고 한다.

즉, 반복문 순회 중 ArrayList 요소를 삭제하려고 한다

 

 

발생가능한 문제점 

1. ModCount & ConcurrentModificationException
순회 중에 리스트의 요소을 삭제 했을 때, 다음에 참조하는 요소의 인덱스가 list의 범위를 벗어나 발생하는 예외

  • ArrayList의 멤버변수인 ModCount 와 ArrayList의 내부클래스 Itr

    ArrayList.Iterator()는 next()를 할 때마다 list의 ModCount와 iterator인스턴스의 멤버변수 expectedModCount 값을 비교한다. 둘이 같지않다면 ConcurrentModificationException 에러를 던진다.

2. 탐색 누락
낮은 index에서 높은 index로 탐색하면서 요소를 삭제하면 탐색에서 제외되는 요소가 생길 수 있다.

 

더보기

list.remove() 를 통해 원본 리스트에서 바로 요소를 삭제해버리는 경우 다음에 탐색할 요소의 인덱스가 바뀌므로 탐색대상에서 누락될 수 있다.

//반복 도중 list요소 삭제시 -> 탐색누락되는 경우
for(int i = 0 ; i < cmntList.size() ; i++) {	//낮은 index에서 높은 index로 탐색하면서
    String seqNo = cmntList.get(i).get("cmntSeqNo").toString();
    String topNo = cmntList.get(i).get("topSeqNo").toString();
    String delYn = cmntList.get(i).get("delYn").toString();
    flag = false;

    if("Y".equals(delYn)){
        for(int j = 0 ; j < cmntList.size() ; j++) {
            String upprSeqNo = cmntList.get(j).get("upprSeqNo").toString();
            String topSeqNo = cmntList.get(j).get("topSeqNo").toString();
            String delChk = cmntList.get(j).get("delYn").toString();
            flag = false;

            if(!topNo.equals(topSeqNo)){
                continue;
            }

            if(seqNo.equals(upprSeqNo) && "N".equals(delChk)){
                System.out.println("넌 살았구나! :" + upprSeqNo);

                flag = true;

                break;
            }
        }

        if(!flag){
            System.out.println(flag);
            cmntList.remove(i);	//요소를 삭제하면, 모든 요소의 인덱스가 앞으로 땡겨질 것이다
        }
    }
}

 

 

해결책

1. Iterator.remove()

list.remove()가 아닌 iterator의 remove() 사용

  • iterator의 lastRet이 가리키는 요소를 list.remove()의 인자로 넣어서 호출하게 된다.
  • lastRet이 가리키고 있던 위치로 cursor가 뒤로 이동하여 expectedModCount에 modCount를 다시 복사하여 ConcurrentModificationException 에서 안전하다.

2. Collection.removeIf()

인자로 함수형 인터페이스 Predicate를 전달하고, 요소를 안전하게 삭제한다.

 

3. CopyOnWriteArrayList

자바에서 제공하는 수정메서드에 대해 Thread-safe를 보장하는 List이다.

  • iterator() 호출시 현재 list의 데이터 배열을 참조하여 snapshot으로 저장한다.
  • add()나 remove() 같은 동작에서 새로운 배열을 복사(shallow copy)하여 작업하기에 life-time동안 snapshot은 변경되지 않으며, ConcurrentModificationException이 발생하지 않는다.
  • synchronized가 되어 있기 때문에 Thread-safe하다.

4. List의 높은 index에서 낮은 index 방향으로 순회

  • 이 방식으로 탐색하면 요소를 삭제해도 탐색할 요소의 인덱스에 영향을 주지 않는다고 한다..

 

새로 배운것

CopyOnWriteArrayList
배열 수정작업시 깨끗하게 복사해서 Thread Safe하게 작업한다.

  • ArrayList를 Thread Safe하게 → CopyOnWriteArrayList, SynchronizedList
  • HashMap Thread Safe하게 → ConcurrentHashMap

Iterator

컬렉션 프레임워크는 컬렉션에 저장된 요소들을 읽어오는 방법을 표준화하였다. 컬렉션에 저장된 각 요소에 접근하는 기능을 가진 Iterator 인터페이스를 정의하고, Collection 인터페이스에는 Iterator (Iterator 인터페이스를 구현한 클래스의 인스턴스) 를 반환하는 iterator() 를 정의하고 있다.

 

iterator() 메서드를 호출하여 Iterator 를 반환받은 후에 반복문을 사용하여 요소들에 접근할 수 있다.

  • boolean haxNext() : 읽어 올 요소가 남아있는지 여부를 반환한다.
  • Object next() - 다음 요소를 읽어온다.
  • void remove() -  next() 로 읽어 온 요소를 삭제한다.
    next() 또는 previous()로 읽어올 때 index 를 저장해서 마지막으로 읽어들인 요소의 index 로 삭제를 수행한다.
    만약 아무 요소도 호출하지 않고 remove() 를 호출하는 경우에는 IllegalStateException 발생
    iterator 중 remove() 를 지원하지 않는 클래스의 경우 UnsupportedOperationException 발생
List list = new ArrayList();
Iterator it = list.iterator();

while(it.haxNext()) {			// list 에 값이 있으면
	System.out.println(it.next()); 	// 각 요소들이 순서대로 출력된다.
}​

 

 

비교

  • Enumeration
    컬렉션 프레임워크가 만들어지기 이전에 사용하던 것으로 Iterator 의 구버전

  • ListIterator
    Iterator 를 상속받아서 기능을 추가한 것으로, 컬렉션 요소에 접근할 때 단방향으로만 이동하는 Iterator 에 비해 ListIterator 는 양방향으로 이동이 가능하다. 다만 ArrayList 나 LinkedList 와 같이 List 인터페이스를 구현한 컬렉션에서만 사용할 수 있다.

 


더보기
List<EocsMap> cmntList = new CopyOnWriteArrayList<>(); 
cmntList = interfaceDAO.selectNtcCmntList(inParam);
boolean flag = false;

System.out.println("*****START: " + cmntList.size() + " +++++");

Iterator<EocsMap> it = cmntList.iterator();
while(it.hasNext()){
    EocsMap item = it.next();

    String seqNo = item.get("cmntSeqNo").toString();
    String topNo = item.get("topSeqNo").toString();
    String delYn = item.get("delYn").toString();
    flag = false;

    topNo = (topNo == null || topNo.isEmpty()) ? seqNo : topNo;
    System.out.println("Iterating: [" + topNo + "]"+ item);
    if("Y".equals(delYn)){
        for(int i = 0 ; i < cmntList.size() ; i++) {
            String upprSeqNo = cmntList.get(i).get("upprSeqNo").toString();
            String topSeqNo = cmntList.get(i).get("topSeqNo").toString();
            String delChk = cmntList.get(i).get("delYn").toString();

            if(!topNo.equals(topSeqNo)){
                continue;
            }

            if(seqNo.equals(upprSeqNo) && "N".equals(delChk)){
                System.out.println("넌 살았구나! :" + upprSeqNo);

                flag = true;

                break;
            }
        }

        if(!flag){
            System.out.println("잡았다 요놈! :" + seqNo);
            it.remove();
        }
    }
}

rslt = cmntList;
System.out.println("*****END: " + cmntList.size() + " +++++");

 

이건 틀렸다.

이 로직대로면 살아있는 댓글의 죽은 부모댓글 윗 한단계만 목록에서 살아남는다.

 

1레벨(죽음) - 2레벨(죽음) - 3레벨(죽음) - 4레벨(Alive)  

 

여기서 3,4레벨만 남고 1레벨, 2레벨은 목록에서 제외될 것이다. 코딩 MISS-!!

 


더보기
    //댓글목록 담을 Map선언
    HashMap<Integer, EocsMap> cmntMap = new HashMap<Integer, EocsMap>();
    List<EocsMap> cmntList = interfaceDAO.selectNtcCmntList(inParam);

    for(int i=0; i<cmntList.size(); i++){
    	//댓글이 살아있고, 자식으로부터 chkYn=Y이면
        if("Y".equals(cmntList.get(i).get("chkYn").toString())){
            //정렬순서를 Key로 하여 해당 댓글 Map에 추가
            int sort = Integer.parseInt(cmntList.get(i).get("sort").toString());
            cmntMap.put(sort, cmntList.get(i));

            //부모댓글에 chkYn=Y 추가
            String uppr = cmntList.get(i).get("upprSeqNo").toString();
            uppr = (uppr == null || uppr.isEmpty()) ? cmntList.get(i).get("cmntSeqNo").toString() : uppr;
            for(EocsMap cmnt : cmntList){
                if(uppr.equals(cmnt.get("cmntSeqNo").toString())){
                    cmnt.put("chkYn", "Y");	

                    break;

                }
            }
        }
    }
    
    // 키 값에 따라 오름차순으로 자동 정렬되는 TreeMap 선언
    Map<Integer, Object> sortedByKey = new TreeMap<>(cmntMap);
    sortedByKey.entrySet().stream().forEach(entry -> {
        rslt.add((EocsMap) entry.getValue());
    });

 

 

 

 

 

Ref


 

[ JAVA ] Iterator 분석 (feat. ArrayList)

개요 요즘 코딩테스트 준비를 위해 알고리즘 문제들을 풀고있다. 그러던 중 옆에서 같이 공부하던 생각한대로 동작하지 않는다며 보여준 코드를 보고 이번 포스팅의 주제를 정했다. public void met

javanitto.tistory.com

 

 

'Language > JAVA' 카테고리의 다른 글

Stream API  (1) 2024.02.27
[Java] Request Parameter 출력  (0) 2024.01.15
[JSTL] c:forEach 역순  (0) 2023.09.25
JDBC, SQL Mapper, ORM  (1) 2023.05.01
JAVA 오픈소스 프레임워크 Netty  (0) 2023.04.19