CodeGym /행동 /JAVA 25 SELF /Stream API의 기본 연산: map, filter, collect

Stream API의 기본 연산: map, filter, collect

JAVA 25 SELF
레벨 30 , 레슨 1
사용 가능

1. 스트림 만들기

Stream API를 사용하려면 우선 컬렉션이나 배열에서 스트림을 얻어야 합니다.

스트림 생성 예시

// 리스트에서
List<String> names = List.of("Anna", "Boris", "Alex", "Alina");
Stream<String> stream = names.stream();

// 배열에서
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);

// 개별 값에서
Stream<String> letters = Stream.of("A", "B", "C");

요약:

  • list.stream() — 컬렉션용
  • Arrays.stream(array) — 배열용
  • Stream.of(...) — 개별 값에 대해

우리 애플리케이션 맥락에서의 예

사용자 목록이 있다고 가정해 봅시다:

List<String> users = List.of("Ivan", "Anna", "Petr", "Alexey");
Stream<String> userStream = users.stream();

중간 연산과 종단 연산

중요한 포인트: Stream API의 연산은 두 가지 유형으로 나뉩니다.

  • 중간 연산 (예: filter, map, distinct) — 처리 단계를 기술합니다. 새 스트림을 반환하지만 그 자체로는 아무 것도 실행하지 않습니다.
  • 종단 연산 (예: collect, forEach, count) — 파이프라인을 실행하고 결과를 산출합니다.

스트림은 “지연(lazy)” 방식으로 동작합니다: 종단 연산을 호출하기 전까지는 어떤 계산도 수행되지 않습니다. 그래서 우리는 종종 collect(...)로 체인을 끝내는데, 이 지점이 스트림이 다시 컬렉션 또는 다른 결과로 바뀌는 시점입니다.

2. filter 연산: 조건으로 요소 걸러내기

filter는 중간 연산으로, 주어진 조건을 만족하는 요소만 통과시킵니다.

시그니처

Stream<T> filter(Predicate<? super T> predicate); 

Predicate는 요소를 받아 true(유지) 또는 false(제거)를 반환하는 함수형 인터페이스입니다.

예: 짝수만 남기기

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);

List<Integer> evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());

System.out.println(evenNumbers); // [2, 4, 6]

무슨 일이 일어나나요?

  • n -> n % 2 == 0 — 람다 표현식으로, 숫자가 2로 나누어떨어지는지 확인합니다.
  • filter는 짝수만 남깁니다.

예: "A"로 시작하는 이름만 필터링

List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");

List<String> aNames = names.stream()
    .filter(name -> name.startsWith("A"))
    .collect(Collectors.toList());

System.out.println(aNames); // [Anna, Alex, Alina]

중요 포인트: filter는 컬렉션을 변경하지 않습니다 — 필요한 요소만 포함한 새 스트림을 만듭니다.

3. map 연산: 요소를 다른 것으로 변환

map은 변환 연산입니다. 스트림의 각 요소에 함수를 적용하고 새 요소를 반환합니다.

시그니처

<R> Stream<R> map(Function<? super T, ? extends R> mapper)

Function은 요소를 받아 무언가(다른 타입일 수도 있음)를 반환하는 인터페이스입니다.

예: 문자열의 길이 구하기

List<String> names = List.of("Anna", "Boris", "Alex");

List<Integer> nameLengths = names.stream()
    .map(name -> name.length())
    .collect(Collectors.toList());

System.out.println(nameLengths); // [4, 5, 4]

무슨 일이 일어나나요?

  • map이 문자열을 그 길이로 변환합니다 (name -> name.length()).
  • 결과로 숫자 스트림이 됩니다.

예: 문자열을 대문자로 변환

List<String> names = List.of("Anna", "Boris", "Alex");

List<String> upperNames = names.stream()
    .map(name -> name.toUpperCase())
    .collect(Collectors.toList());

System.out.println(upperNames); // [ANNA, BORIS, ALEX]

4. collect 연산: 결과를 다시 컬렉션으로 수집

collect는 종단 연산으로, 스트림을 종료하고 결과를 컬렉션이나 다른 컨테이너로 수집합니다.

시그니처

<R, A> R collect(Collector<? super T, A, R> collector)

시그니처가 어려워 보여도 겁먹지 마세요! 99% 경우에는 Collectors 클래스의 준비된 컬렉터를 사용합니다.

Collectors는 여러 “수집기”를 제공하는 유틸리티 클래스입니다. 결과를 어떤 형태로 모을지: 리스트, 집합, 문자열 등.

예:

  • Collectors.toList() — List로
  • Collectors.toSet() — Set으로
  • Collectors.joining(", ") — 쉼표로 구분된 문자열로

즉, Collectors는 다양한 모양의 상자 세트와 같아서, 스트림의 요소를 그 안에 포장한다고 생각하면 됩니다.

예: 결과를 List로 수집

List<String> filtered = names.stream()
    .filter(name -> name.length() > 3)
    .collect(Collectors.toList());

예: 결과를 Set으로 수집

Set<String> uniqueNames = names.stream()
    .map(String::toLowerCase)
    .collect(Collectors.toSet());

예: 문자열을 쉼표로 이어 붙이기

String result = names.stream()
    .collect(Collectors.joining(", "));

System.out.println(result); // Anna, Boris, Alex

5. 연산 체이닝: 필터링 + 변환 + 결과 수집

Stream API의 가장 큰 강점은 연산을 연이어 체이닝할 수 있다는 점입니다.

예: "A"로 시작하는 이름들의 길이 구하기

List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");

List<Integer> aNameLengths = names.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::length)
    .collect(Collectors.toList());

System.out.println(aNameLengths); // [4, 4, 5]

단계별 설명:

  1. .stream() — 리스트에서 스트림을 생성합니다.
  2. .filter(name -> name.startsWith("A"))"A"로 시작하는 이름만 남깁니다.
  3. .map(String::length) — 각 이름을 그 길이로 변환합니다.
  4. .collect(Collectors.toList()) — 결과를 리스트로 수집합니다.

동일한 명령형 코드

같은 작업을 “옛 방식”으로 작성하면 다음과 같습니다:

List<Integer> result = new ArrayList<>();
for (String name : names) {
    if (name.startsWith("A")) {
        result.add(name.length());
    }
}

비교해 보세요: Stream API는 한 줄로, “무엇을 하는지”에 초점을 맞춰 읽히며 “어떻게 하는지”가 아닙니다.

6. 실습: 짧은 과제 몇 가지

연습해 봅시다! 모든 예제는 하나의 파일에서 실행할 수 있습니다 — 데이터만 바꾸면 됩니다.

과제 1: 홀수만 남기고 제곱하기

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);

List<Integer> oddSquares = numbers.stream()
    .filter(n -> n % 2 != 0)
    .map(n -> n * n)
    .collect(Collectors.toList());

System.out.println(oddSquares); // [1, 9, 25, 49]

과제 2: 문자열 리스트에서 첫 글자들의 리스트 구하기

List<String> names = List.of("Anna", "Boris", "Alex");

List<Character> initials = names.stream()
    .map(name -> name.charAt(0))
    .collect(Collectors.toList());

System.out.println(initials); // [A, B, A]

과제 3: 길이가 3보다 큰 문자열만 필터링하여 Set으로 수집

List<String> words = List.of("cat", "dog", "elephant", "ant", "bear");

Set<String> longWords = words.stream()
    .filter(word -> word.length() > 3)
    .collect(Collectors.toSet());

System.out.println(longWords); // [bear, elephant]

7. filter, map, collect 사용 시 흔한 실수

실수 № 1: collect를 빼먹어서 결과가 없음!
Stream API는 창가의 고양이처럼 게으릅니다: 종단 연산(예: collect 또는 forEach)을 호출하기 전에는 아무 일도 일어나지 않습니다. users.stream().filter(...).map(...); 만 작성하면 아무 동작도 수행되지 않습니다.

실수 № 2: filter와 map의 순서를 바꿈
가끔 초보자는 먼저 map을 하고 그 다음 filter를 합니다. 예를 들어 names.stream().map(String::length).filter(len -> len > 3)는 문자열이 아니라 숫자를 얻게 됩니다. 길이가 3보다 큰 문자열이 필요하다면 먼저 필터링하고 그 다음 변환하세요.

실수 № 3: 불변성을 잊음
Stream API 연산은 원본 컬렉션을 변경하지 않습니다! List<String> upper = names.stream().map(String::toUpperCase).collect(Collectors.toList()); 이후에도 names 컬렉션은 그대로입니다.

실수 № 4: 외부의 변경 가능한 리스트를 사용하려 함
다음과 같이 작성하지 마세요:

List<String> result = new ArrayList<>();
names.stream().filter(...).forEach(name -> result.add(name));

collect를 사용하는 편이 더 안전하고 간결합니다.

실수 № 5: NullPointerException
컬렉션에 null 요소가 있을 수 있다면, null에 대해 name.startsWith("A")를 호출하면 예외가 발생합니다. 가능하다면 null 체크 필터를 추가하세요:

.filter(name -> name != null && name.startsWith("A"))
1
과제
JAVA 25 SELF, 레벨 30, 레슨 1
잠금
마법 상점 재고 조사 🍎
마법 상점 재고 조사 🍎
1
과제
JAVA 25 SELF, 레벨 30, 레슨 1
잠금
실험실 센서 모니터링 🧪
실험실 센서 모니터링 🧪
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION