본문 바로가기

JAVA

[JAVA] 스트림 (Stream) - 중간 연산


스트림(Stream)이란?


스트림은 '데이터의 흐름'입니다. 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있습니다. 또한 이전에 배웠던 람다를 이용하여 코드의 양을 줄여 간결하게 표현할 수 있습니다.

즉, 배열과 컬렉션을 함수형으로 처리할 수 있습니다.

 

또한 간단하게 병렬처리(multi-threading)가 가능하다는 장점이 있습니다.

이 말은 즉, 스레드를 이용해 많은 요소들을 빠르게 처리할 수 있습니다.

 

스트림에 대한 내용은 크게 3가지로 나눌 수 있습니다.

  1. 생성
  2. 중간연산
  3. 최종연산

이번 글은 이 세 가지 내용 중 중간연산에 대한 내용을 다뤄보고자 합니다.

 

중간연산


전체 요소 중에서 다음과 같은 api를 사용해 내가 원하는 것만 뽑을 수 있습니다. 이러한 중간 연산 단계를 중간 작업(intermediate operations)이라고 하는데, 이러한 작업은 스트림을 리턴하기 때문에 여러 작업을 이어 붙여서(chaining) 작성할 수 있습니다.

List<String> names = Arrays.asList("i","am","nom");

 

Filtering


필터(filter)는 스트림 내 요소들을 하나씩 평가해 추출하는 작업입니다. 인자로 받는 Predicate는 boolean을 반환하는 함수형 인터페이스로 평가식에 들어가게 됩니다.

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

간단한 예제 코드입니다.

Stream<String> stream =
	name.stream()
    .filter(name -> name.contains("m"));
    
    // [Nom, man]

스트림의 각 요소에 대해서 평가식을 수행하여 "m"이 들어간 요소만을 반환합니다.

 

Mapping

맵(map)은 스트림 내 요소들을 하나씩 특정 값으로 변환해 줍니다. 이때 값을 변환하기 위해 람다를 인자로 받습니다.

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

스트림에 들어가 있는 값이 input이 되어서 특정 로직을 거친 후 output이 되어 (리턴되는) 새로운 스트림에 담기게 됩니다.

이러한 작업을 맵핑(mapping)이라고 합니다.

import java.util.Arrays;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        String[] arr = {"nom", "is", "man"};
        Stream<String> stream = Arrays.stream(arr)
                .map(String::toUpperCase);
        System.out.println(Arrays.toString(stream.toArray()));
    }
}

// 출력 결과
[NOM, IS, MAN]

 

map 이외에도 조금 더 복잡한 flatMap 메서드도 있습니다.

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

 

인자로 mapper를 받고 있는데, 리턴 타입이 Stream입니다. 즉, 새로운 스트림을 생성해서 리턴하는 람다를 넘겨야 합니다. flatMap은 중첩구조를 한 단계 제거하고 단일 컬렉션으로 만들어주는 역할을 합니다. 이러한 작업을 플래트닝(flattening)이라고 합니다.

 

다음과 같이 중첩된 리스트가 있습니다

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {

        List<List<String>> list = Arrays.asList(Arrays.asList("a"),
                                                Arrays.asList("b"));

        System.out.println(list);
    }
}

//출력 결과 
[[a], [b]]

이를 flatMap을 사용해서 중첩 구조를 제거한 후 작업할 수 있습니다.

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {

        List<List<String>> list = Arrays.asList(Arrays.asList("a"),
                                                Arrays.asList("b"));


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

        System.out.println(flatList);
    }
}

// 출력 결과
[a, b]

 

Sorting

정렬의 방법은 다른 정렬과 마찬가지로 Comparator를 사용합니다.

Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);

인자 없이 호출할 경우 오름차순으로 정렬합니다.

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {

        List<Integer> il = IntStream.of(6,4,3,7,2,1)
                .sorted()
                .boxed()
                .collect(Collectors.toList());

        System.out.println(il);
    }
}

// 출력 결과
[1, 2, 3, 4, 6, 7]

이번에는 인자를 넣는 경우를 확인해 보겠습니다.

확인을 위해 스트링 리스트를 만들고, 알파벳 순으로 정렬한 리스트와 역순으로 정렬한 코드를 확인해 보도록 하겠습니다.

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {

        List<String> lang = Arrays.asList("Java", "Scala", "Groovy", "Python", "Go", "Swift");

        lang= lang.stream()
                .sorted()
                .collect(Collectors.toList());
        System.out.println(lang);


        lang = lang.stream()
                .sorted(Comparator.reverseOrder())
                .collect(Collectors.toList());
        System.out.println(lang);
    }
}

//출력 결과
[Go, Groovy, Java, Python, Scala, Swift]
[Swift, Scala, Python, Java, Groovy, Go]

Comparator의 compare 메서드는 두 인자를 비교해서 값을 반환합니다

int compare(T o1, T o2)

기본적으로 Comparator 사용법과 동일합니다. 이를 이용해서 문자열 길이를 기준으로 정렬해 보겠습니다.

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {

        List<String> lang = Arrays.asList("Java", "Scala", "Groovy", "Python", "Go", "Swift");

        lang= lang.stream()
                .sorted(Comparator.comparingInt(String::length))
                .collect(Collectors.toList());

        System.out.println(lang);


        lang = lang.stream()
                .sorted((s1, s2) -> s2.length() - s1.length())
                .collect(Collectors.toList());
        System.out.println(lang);
    }
}

//출력 결과

[Go, Java, Scala, Swift, Groovy, Python]
[Groovy, Python, Scala, Swift, Java, Go]

 

 

Iterating

스트림 내 요소들 각각을 대상으로 특정 연산을 수행하는 메서드로는 peek이 있습니다.

Stream<T> peek(Consumer<? super T> action);

따라서 스트림 내 요소들 각각에 특정 작업을 수행할 뿐 결과에 영향을 미치지 않습니다. 다음 예제처럼 작업을 처리하는 중간에 결과를 확인해 볼 때 사용할 수 있습니다.

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {

        int sum = IntStream.of(1,2,3,4,5)
                .peek(System.out::println)
                .sum();
        System.out.println("sum = " +sum);

    }
}

//출력 결과
1
2
3
4
5
sum = 15

 


이번 글을 통하여 스트림의 중간연산에 대해 공부해 보았습니다.

다음 글을 통하여 스트림의 마지막 요소인 최종 연산에 대해 공부해 보도록 하겠습니다.

 

 

Reference


https://futurecreator.github.io/2018/08/26/java-8-streams/