Java Stream API 사용법

Stream API이란?

Stream API는 Java SE 8에서 추가된 반복(iteration)의 확장 API이다. 지금까지 컬렉션에 갔다 복잡한 처리를 알기 쉬운 코드로 작성하는 것이 가능하다.
여기에서는 람다 식의 설명은 하지 않기에 람다 식을 모르는 사람은 먼저 Lamda 함수를 먼저 이해하고 확인하고 오길 바란다.

stream은 컬렉션의 요소에 대한 액세스를 허용한 것, 컬렉션의 다른 형태라고 생각된다.

기본적인 흐름

스트림의 구조는 크게 3가지로 나뉜다.

  1. 컬렉션에서 Stream을 생성한다.
  2. stream에 원하는 만큼 “중간 연산"을 실행하고, 컬렉션의 내용을 합해서 변환한다.
  3. “최종 연산"으로 변환한 컬렉션의 내용에 대해 처리를 적용한다.

실제 사용법을 표기하면 Stream생성().중간연산().최종연산()와 같은 식으로 작성한다.

그럼, 실제 사용 예를 살펴 보자. 아래 예제는 “1~5에서 짝수만 표시"하도록 해보자

Stream API를 사용하지 않는 경우

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (Integer i : list) {
    if (i % 2 == 0) {
        System.out.println(i);
    }
}

Stream API를 사용한 경우

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
list.stream() // Stream 생성
        .filter(i -> i % 2 == 0) // 중간 연산
        .forEach(i -> System.out.println(i)); // 최종 연산

처리의 차이를 살펴 보도록 하자. Stream API를 사용하지 않는 경우는 “2로 나누어 나머지가 0인 경우에만 표시"하는 구조로 되어 있다. Stream API를 사용한 경우에는 “2로 나누어 떨어지는 것을 모은 요소를 모두 표시하는 구조로 되어 있다.

2개의 처리를 나누는 것이 가능해 졌다. 이로써 가독성 및 부품성이 높아지고, 코드의 추상화도 가능해 진다.

이번 예제는 stream 가져와 중간 연산으로 “filter"메소드를 사용하고 있다. 이름 그대로 컬렉션의 요소를 필터링하는 것이다. filter 메소드는 T -> boolean와 같은 람다 식을 전달해 주고 있다. (※ T의 의미를 모르는 사람은 제네릭에서 확인하길 바란다.) 람다 표현식이 true 인 요소만 남은 stream을 반환한다.

최종 연산으로 forEach 메소드를 사용하고 있다. 이것도 이름도 그대로 “요소를 하나씩 꺼내 오도록 하는 처리를 실행"하는 방법이다. 다른 언어에서 친숙한 구문일 것이다. Java에서의 확장 for 문에 해당한다.

예제는 “2로 나눈 나머지가 0이 되는 것만” 가져오는 중간 연산과 “하나씩 화면에 출력하는” 최종 연산을 나눠진다.

중간 연산 결과는 작업 적용 후에 stream이 반환되므로 예제처럼 연속으로 참조하도록 작성할 수도 있다.

물론, 아래와 같이도 작성 할 수도 있다.

List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = integerList.stream(); // Stream 생성
Stream<Integer> stream2 = stream.filter(i -> i % 2 == 0); // 중간 연산
stream2.forEach(i -> System.out.println(i)); // 최종 연산

이어서 작성하는 것이 가독성 좋기에 특별한 이유가 없다면 연속해서 작성하도록 하자.

중간 연산

자주 사용하게 되는 대표적인 중간 연산에 대해서 설명하겠다.

filter

앞의 예제에서 언급 한 바와 같이 필터링하기 위한 중간 연산이다. 인수로는 T -> boolean와 같은 람다 식을 전달한다. 조건식이 true인 요소만 수집한다.

List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() //  Stream 생성
        .filter(i -> i % 2 == 0) // 중간 연산
        .forEach(i -> System.out.println(i)); // 최종 연산

map

map의 인수로는 T -> U와 같은 람다 식을 전달한다. 요소를 변환하는 중간 연산이다.

스트림 내부의 요소 각각 하나씩 접근하여, 파라미터로 넣어준 함수를 실행한 후에 최종연산에서 지정한 형식으로 반환해 준다.

아래 예제와 같이 값을 2로 곱할 수도 있고,

List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // Stream 생성
        .map(i -> i * 2) // 중간 연산
        .forEach(i -> System.out.println(i)); // 최종 연산

아래 예제와 같이 Integer로부터 String으로 다른 형식을 반환할 수도 있다.

List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // Stream 생성
        .map(i -> "요소는" + i + "입니다.") // 중간 연산
        .forEach(i -> System.out.println(i)); // 최종 연산

또한, 다음과 같은 동물 인터페이스가 있다고 했을 때,

interface Animal {
    /**
     * 울음 소리를 반환한다.
     */
    String getCry();
}

동물 목록에 map을 사용하여 울음 소리를 출력해 볼 수도 있다.

List<Animal> animalList = Arrays.asList(dog, cat, elephant);
animalList.stream() // Stream 생성
        .map(animal -> animal.getCry()) // 중간 연산
        .forEach(cry -> System.out.println(cry)); // 최종 연산

flatMap

flatMap은 스트림 평면화라고도 한다. flatMap은 데이터가 2차원 배열 또는 목록을 있을 때, 1차원으로 변경해 준다.

아래의 코드를 통해 알아 보도록 하자.

List<List<String>> alphabets = Arrays.asList(Arrays.asList("a", "b", "c"), Arrays.asList("d", "e", "f"));

List<String> result = alphabets.stream()
        .flatMap(Collection::stream)
        .collect(Collectors.toList());

System.out.println(result);

Output:

[a, b, c, d, e, f]

sorted

sorted의 인수로는 (T, T) -> int와 같은 람다 식을 전달한다. 요소를 정렬 중간 연산이다. 요소를 2개씩 가지고, 람다 식을 전달하고 있다. 반환 값이 양수이면 내림차순이고, 음수이면 오름차순으로 정렬된다.

List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // Stream 생성
        .sorted((a, b) -> Integer.compare(a, b)) // 중간 연산
        .forEach(i -> System.out.println(i)); // 최종 연산

이보다 더 간단한 정렬(오름차순, 내림차순) 방법으로는아래와 예제와 같이 Comparator 인터페이스 naturalOrder(), reverseOrder()을 넘겨주는 것이 가장 알기 쉽다.

List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // Stream 생성
        .sorted(Comparator.reverseOrder()) // 중간 연산
        .forEach(i -> System.out.println(i)); // 최종 연산

이상 대표적인 3가지 자주 사용하는 중간 연산에 대해 알아보았다. 그 외에도 여러가지 더 있으므로 관심이 있는 분은 찾아 보길 바란다.

최종 연산

forEach

forEach 인수는(T) -> void와 같이 람다식을 전달 해주고 있다. 전체 요소를 하나씩 가져와 어떠한 처리를 하는 최종 연산이다.

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

numbers.stream()
        .map(x -> x + 3) // 4를 더한다.
        .filter(x -> x % 2 == 0) // 짝수만 필터링한다.
        .limit(3) // 3개까지만 가져온다.
        .forEach(System.out::println);

Output:

4
6
8

forEach를 호출하게 되면, 호출되면서 전체 스트림이 소비된다.

peek

peek은 호출되면서 전체 스트림을 소비하는 forEach에 비해 peek은 실제로 스트림의 요소를 소비하지는 않는다. peek은 자신이 확인한 요소를 파이프라인의 다음 연산으로 그대로 전달하고, 각 동작 전후의 중간값을 출력한다.

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

List<Integer> result = numbers.stream()
        .peek(v -> System.out.println("From stream: " + v)) // 처음 소비한 요소를 출력

        .map(v -> v + 3)
        .peek(v -> System.out.println("Mapped value: " + v)) // map 실행 후 결과를 출력

        .filter(v -> v % 2 == 0)
        .peek(v -> System.out.println("Filtered value: " + v)) // filter 실행 후 결과를 출력

        .limit(3)
        .peek(v -> System.out.println("limited value: " + v)) // limit 실행 후 결과를 출력

        .collect(Collectors.toList());

System.out.println(result);

Output:

From stream: 1
Mapped value: 4
Filtered value: 4
limited value: 4
From stream: 2
Mapped value: 5
From stream: 3
Mapped value: 6
Filtered value: 6
limited value: 6
From stream: 4
Mapped value: 7
[4, 6]

다양한 Stream 예제

문자열(String)에서 숫자가 아닌 값만 추출하기, 다시 문자열(String)로 변환하기

String input = "1Ca2Adf34eB342";

String output = input.chars()
        .mapToObj(ch -> (char) ch)// Stream<Character>
        .sorted()
        .filter(ch -> !Character.isDigit(ch))
        .map(Object::toString)
        .collect(Collectors.joining());

System.out.println(output);

Output:

ABCadef

두 목록의 조합해서 쌍으로 만들기

List<Integer> numberArray1 = Arrays.asList(1, 2);
List<Integer> numberArray2 = Arrays.asList(3, 4, 5);

List<int[]> pairs = numberArray1.stream()
        .flatMap(i -> numberArray2.stream()
                .map(j -> new int[]{i, j}))
        .collect(Collectors.toList());

for (int[] a : pairs) {
        System.out.println(Arrays.toString(a));
}

Output:

[1, 3]
[1, 4]
[1, 5]
[2, 3]
[2, 4]
[2, 5]

String 에서 숫자를 추출하여 총 합(sum) 구하기

String input = "1Ca2Adf9";

int sum = input.chars()
        .filter(Character::isDigit)
        .map(a -> Character.digit(a, 10))
        .sum();

System.out.println(sum); // 1 + 2 + 9

Output:

12

문자열(String)이 null이 아닌 경우만, joining 수행하기

String front = "abc";
String middle = null;
String end = "ghi";

String output = Stream.of(front, middle, end)
        .filter(Objects::nonNull)
        .collect(Collectors.joining());

System.out.println(output);

Output:

abcghi



최종 수정 : 2024-01-18