Java 에서 list 를 다루는 것은 매우 흔한 일이면서도 자주 사용하는 기능 중의 하나입니다. 저 역시 개발업무를 진행하면서 가장 많이 사용하는 것이 list 와 map 입니다.

일반적으로 backend 개발을 하던 front/app 개발을 하던, list 를 다루는 것은 매우 중요한 부분 중 하나이고, 특히 database 에서 가져온 데이터를 조작하는 데 있어서 중요한 기능 중 하나입니다. 그런데 이러한 list 에서 특정한 조건에 해당하는 것을 제거해야 하는 경우가 있습니다. 물론 database 에서 데이터를 가져올 때, 조건을 잘 넣어 제거하려는 데이터를 제외한 채로 가져오는 것이 가장 좋겠지만, 부득이하게 프로그램 영역에서 이 처리를 해줘야 하는 경우가 있습니다. 여기서는 특정 요소(element)를 제거하는 데에만 초점을 두고 설명해보려고 합니다.

출처에서 설명하는 것 처럼, 아래와 같은 list(collection)이 있다고 가정합니다.

List<Character> letters = new ArrayList<>();
letters.addAll(Arrays.asList('A', 'B', '1', '2', 'C', 'D', '3', 'E', '4', '5'));

쉽게 접근해보자

list 에 어떠한 함수가 있는지부터 한번 살펴보는 것이 좋다. 가장 쉬운 것은 바로 remove 함수입니다.

다음은 Oracle 의 공식 문서에 있는 설명입니다.

boolean remove(Object o)

Removes a single instance of the specified element from this collection, if it is present (optional operation). More formally, removes an element e such that (o==null ? e==null : o.equals(e)), if this collection contains one or more such elements. Returns true if this collection contained the specified element (or equivalently, if this collection changed as a result of the call).

Parameters:

o - element to be removed from this collection, if present

Returns:

true if an element was removed as a result of this call

Throws:

  • ClassCastException - if the type of the specified element is incompatible with this collection (optional)
  • NullPointerException - if the specified element is null and this collection does not permit null elements (optional)
  • UnsupportedOperationException - if the remove operation is not supported by this collection

지우고자 하는 원소(element) 자체를 넣으면 그것을 목록에서 지워준다고 설명합니다. 그런데 아래 exceptions 설명을 보면 이것저것 위험한 부분도 있어 보입니다. 우선 아래처럼 지워보겠습니다.

for (Character letter : letters) {
  if (Character.isDigit(letter)) {
    letters.remove(letter);
  }
}

위 코드를 실행하면 ConcurrentModificationException 이 발생합니다. 이는 현재 for문을 돌고 있는 대상에 대해서 그 값을 변경하려고 했을 때 발생하는 exception 입니다.

사실 이 ConcurrentModificationException 은 singleton 형태로 구현된 모든 소스에서 매우 주의해야 할 exception 입니다.

그렇다면 조금 다른 접근을 시도해 보겠습니다. 이번에는 list 에 대해서 index 를 뽑아서 for문을 돌고, 특정 조건에 해당하면 그 원소를 삭제하도록 합니다.

for (int i = 0; i < letters.size(); i++) {
  Character letter = letters.get(i);
  if (Character.isDigit(letter)) {
    letters.remove(i);
  }
}

여기서 list 를 다루는 데 있어서 주의해야 할 점이 바로 나타납니다. 위에서 설명하는 remove함수는 그 결과, 해당 원소를 지워주는 것은 맞지만, 그 결과, 뒤에오는 원소들의 index 가 하나씩 줄어듭니다. 즉, 삭제한 원소 때문에 뒷자리에 있는 원소들이 한 칸씩 당겨진다고 생각하면 쉽습니다. 따라서 위 결과는 다음과 같이 모든 원소에 대해서 조건이 맞는지 체크하지 못한 결과가 나타납니다.

[A, B, 2, C, D, E, 5]

정확히는 지우고 난 바로 뒤 원소는 검열 대상에서 제외되었습니다.

이터레이터 사용하기

Collection에 대해서 각 원소를 뽑아가며 무언가 작업을 해야 한다면 고민할 여지없이 iterator를 사용해야 합니다. 쉽게 구할 수 있는 아래와 같은 형태의 코드를 이용해서 삭제하는 코드를 작성하면 아무런 문제도 없고 정상적인 결과를 얻을 수 있습니다.

for (Iterator<Character> iter = letters.iterator(); iter.hasNext(); ) {
  Character letter = iter.next();
  if (Character.isDigit(letter)) {
    iter.remove();
  }
}

java 8 이후..

java 8 이후로 수많은 변경사항이 있었고, 이 중에서 stream()이 있습니다. stream 은 collection(set) 등에서 각각의 요소에 대해서 검토를 할 수 있게 해줍니다. 마치 for 문을 만들거나 iterator를 돌리는 것과 매우 흡사하지만, 가독성도 좋고 간결한 코드를 작성할 수 있습니다.

List<Character> alphabets = letters.stream()
  .filter(Character::isAlphabetic)
  .collect(Collectors.toList());

또한 collection 에 removeIf 라는 함수가 추가되었습니다. 이 함수를 이용하면 아래와 같이 간단하게 코드 작성이 가능해 집니다.

default boolean removeIf(Predicate<? super E> filter)

Removes all of the elements of this collection that satisfy the given predicate. Errors or runtime exceptions thrown during iteration or by the predicate are relayed to the caller.

Implementation Requirements:

The default implementation traverses all elements of the collection using its iterator(). Each matching element is removed using Iterator.remove(). If the collection’s iterator does not support removal then an UnsupportedOperationException will be thrown on the first matching element.

Parameters:

filter - a predicate which returns true for elements to be removed

Returns:

true if any elements were removed

Throws:

  • NullPointerException - if the specified filter is null
  • UnsupportedOperationException - if elements cannot be removed from this collection. Implementations may throw this exception if a matching element cannot be removed or if, in general, removal is not supported.
letters.removeIf(Character::isDigit);

참고자료 및 출처


Leave a comment