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)); // 최종 연산

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와 같이 람다식을 전달 해주고 있다. 요소를 하나씩 꺼내 어떠한 처리를 하는 최종 연산이다. 사용법은 앞에 예제를 참고 바랍니다.