CodeGym /행동 /JAVA 25 SELF /컬렉션 요소 필터링

컬렉션 요소 필터링

JAVA 25 SELF
레벨 28 , 레슨 0
사용 가능

1. 소개

프로그래밍에서는 큰 데이터 집합에서 필요한 항목만 골라내야 하는 상황을 자주 마주합니다 — 이것이 바로 필터링입니다. 짝수만 남기거나, "Java"라는 단어를 포함하는 문자열을 찾거나, 18세 이상 사용자만 선택하는 일 — 모두 필터링 과제입니다.

Java에서는 이런 작업이 매우 흔하므로, 이를 수행하는 다양한 방법을 자신 있게 다루고 각각의 특징을 이해하는 것이 중요합니다.

2. 루프를 이용한 필터링

가장 기본적인 방법부터 시작해 봅시다 — 일반적인 for 루프. 이런 접근을 명령형이라고 하며, 무엇을 어떻게 할지 명시적으로 지시합니다. 읽기 쉽고 이해하기 간단합니다.

예시: 짝수만 남기기

import java.util.*;

public class FilterExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        List<Integer> evenNumbers = new ArrayList<>(); // 결과를 위한 새 리스트를 생성

        for (Integer n : numbers) {
            if (n % 2 == 0) { // 조건 확인: 짝수
                evenNumbers.add(n);
            }
        }

        System.out.println("짝수: " + evenNumbers);
    }
}

결과:

짝수: [2, 4, 6, 8, 10]

여기서 중요한 점: 원본 컬렉션(numbers)은 변경되지 않습니다. 우리는 결과를 담을 새 리스트를 만듭니다.

예시: 부분 문자열로 문자열 필터링

List<String> words = Arrays.asList("java", "python", "javascript", "kotlin", "c++");
List<String> javaWords = new ArrayList<>();

for (String word : words) {
    if (word.contains("java")) {
        javaWords.add(word);
    }
}
System.out.println(javaWords); // [java, javascript]

원리는 동일합니다: 문자열 리스트를 순회하며 contains 메서드로 부분 문자열 존재를 확인합니다.

3. 컬렉션에서 요소 삭제: 왜 항상 간단하지 않을까?

어디가 함정일까?

때로는 원본 컬렉션에서 불필요한 요소를 모두 삭제하고 싶습니다. 하지만 for-each 루프로 순회하는 동안 그렇게 시도하면 ConcurrentModificationException 예외가 발생합니다.

List<Integer> numbers = new ArrayList<>(Arrays.asList(1, -2, 3, -4, 5));

for (Integer n : numbers) {
    if (n < 0) {
        numbers.remove(n); // 위험! ConcurrentModificationException!
    }
}

결과:

Exception in thread "main" java.util.ConcurrentModificationException

이처럼 for-each로 순회 중 컬렉션을 수정하면 이터레이션이 깨집니다.

컬렉션에서 요소를 올바르게 삭제하는 방법?

방법 1: Iterator.remove() 사용

List<Integer> numbers = new ArrayList<>(Arrays.asList(1, -2, 3, -4, 5));

Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
    Integer n = it.next();
    if (n < 0) {
        it.remove(); // 안전하게 삭제!
    }
}
System.out.println(numbers); // [1, 3, 5]

여기서는 iterator() 메서드로 이터레이터를 만들고, hasNext()next()로 컬렉션을 순회하며, 불필요한 요소는 it.remove() 호출로 삭제합니다.

방법 2: 필요한 요소만 담은 새 리스트 만들기

List<Integer> numbers = Arrays.asList(1, -2, 3, -4, 5);
List<Integer> positive = new ArrayList<>();

for (Integer n : numbers) {
    if (n >= 0) {
        positive.add(n);
    }
}
System.out.println(positive); // [1, 3, 5]

원본 컬렉션은 건드리지 않고 새 리스트를 수집합니다. 이 방법은 안전하고 가독성이 좋으며, 조건이 복잡해져도 쉽게 확장할 수 있습니다.

방법 3: removeIf(Java 8+) 사용

List<Integer> numbers = new ArrayList<>(Arrays.asList(1, -2, 3, -4, 5));
numbers.removeIf(n -> n < 0);
System.out.println(numbers); // [1, 3, 5]

한 줄로 조건을 전달합니다: «0보다 작은 것은 모두 삭제». 안전한 수정의 내부 처리는 컬렉션이 맡습니다.

정리: 요소를 삭제할 때는 for-each를 사용하지 마세요. Iterator.remove(), 새 리스트 생성, 또는 간결한 removeIf 중에서 선택하세요.

4. 필터링에 어떤 방법을 언제 사용할까?

명령형 접근인 일반 루프는 요소를 선별하는 동시에 바로 무언가를 해야 할 때(예: 출력하거나 변환) 유용합니다. 단순하고 명료합니다.

원본 컬렉션의 삭제가 목적이라면 for-each 안에서의 삭제는 피하세요. 단계별 제어가 필요하면 Iterator.remove()를, 최대한 짧고 표현력 있는 방법을 원하면 최신의 removeIf()를 사용하세요.

예시: 단어 목록에서 길이가 네 글자보다 짧은 모든 단어를 삭제합니다:

List<String> words = new ArrayList<>(Arrays.asList("Java", "is", "fun", "awesome", "code"));
words.removeIf(word -> word.length() < 4);
System.out.println(words); // [Java, awesome, code]

메서드 removeIf는 프레디케이트 — 필터 규칙 — 를 받아 그 조건에 해당하는 모든 항목을 삭제합니다.

5. 컬렉션 필터링에서 흔한 실수

실수 №1: for-each 루프에서 컬렉션 요소를 삭제하려는 시도.
이런 코드는 ConcurrentModificationException을 유발합니다:

for (Integer n : numbers) {
    if (n < 0) {
        numbers.remove(n); // 붐! ConcurrentModificationException
    }
}

올바른 대안: Iterator.remove() 또는 removeIf를 사용하세요.

실수 №2: 잘못 작성된 필터 조건.
음수만 삭제해야 합니다:

List<Integer> numbers = new ArrayList<>(Arrays.asList(-3, -1, 0, 2, 4));
numbers.removeIf(n -> n < 0);
System.out.println(numbers); // [0, 2, 4]

하지만 실수로 ‘음수가 아닌 수’를 의미하게끔 <=를 쓰면 0도 사라집니다:

numbers.removeIf(n -> n <= 0); // 결과: [2, 4] — 0이 잘못 삭제됨

조건의 정확성에 유의하세요: 비교 하나만 잘못되어도 결과가 완전히 달라집니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION