Java

[java] stream

수수한개발자 2022. 7. 27.
728x90

스트림이란?

자료가 묘여 있는 배열이나 컬렉션 또는 특정 범위 안에 있는 일련의 숫자를 처리하는 기능이 미리 구현되어 있다면 프로그램의 코드가 훨씬 간결해지고 일관성 있게 다룰 수 있습니다.

예를 들어 배열 요소를 특정 기준에 따라 정렬(sorting)하거나, 요소중 특정 값은 제외하고 출력하는(filter) 기능처럼 말입니다.

이렇게 여러 자료의 처리에 대한 기능을 구현해 놓은 클래스가 스트림(stream)입니다. 스트림을 활용하면 배열, 컬렉션 등의 자료를 일관성 있게 처리할 수 있습니다. 자료에 따라 기능을 각각 새로 구현하는 것이 아니라 처리해야 하는 자료가 무엇인지와 상관없이 같은 방식으로 메서드를 호출할 수 있기 때문에 다른 말로는 자료를 추상화했다고 표현할 수 있습니다.

 

배열을 예로 들어 보겠습니다.

 

int[] arr = {1,2,3,4,5};
for(int i=0; i < arr.length; i++){
System.out.println(arr[i]);
}

 

위의 코드는 정수 5개를 요소로 가진 배열이고, 이를 모두 출력하는  출력문입니다.

 

이 배열에 대한 스트림을 생성하여 출력하면 다음과 같습니다.

 

int[] arr = {1,2,3,4,5}
Arrays.stram(arr).forEach(n -> Systeom.out.println(n));

 

엄청 간단해졌습니다.

 

Arrays.stram(arr) : 스트림 생성 부분

. forEach(n -> Systeom.out.println(n)) : 요소를 하나씩 꺼내어 출력하는 기능

 

스트림을 생성하고 미리 구현되어 있는 forEach() 메서드(최종 연산)를 사용하여 배열의 요소를 하나씩 꺼내여 출력할 수 있습니다. 그러면 스트림에 미리 구현되어 있는 연산 기능은 무엇이 있는지 알아보겠습니다.

 

스트림 연산

 

스트림 연산의 종류에는 크게 중간 연산과 최종 연산 두 가지가 있습니다.

중간 연산은 자료를 거르거나 변경하여 또 다른 자료를 내부적으로 생성합니다. 최종 연산은 생성된 내부 자료를 소모해 가면서 연산을 수행합니다. 따라서 최종 연산은 마지막에 한 번만 호출됩니다. 그리고 최종 연산이 호출되어야 중간 연산의 결과가 만들어집니다. 중간 연산과 최종 연산에는 여러 종류가 있지만, 그중에서 많이 사용하는 연산만 정리해보겠습니다.

 

중간 연산

 

filter()

filter()는 조건을 넣고 그 조건에 맞는 참인 경우만 추출하는 경우에 사용합니다. 문자열 배열이 있을 때 문자열의 길이가 5 이상인 경우만 출력하는 코드는 다음과 같습니다.

 

sList.stream().filter(s -> s.length() >= 5).forEach(s -> System.out.println(s));

 

sList.stream(). : 스트림 생성

filter(s -> s.length() >= 5). : 중간 연산

forEach(s -> System.out.println(s)); : 최종 연산

 

map()

map()은 클래스가 가진 자료 중 이름만 출력하는 경우에 사용합니다. 예를 들어 고객 클래스가 있다면 고객 이름만 가져와서 출력할 수 있습니다. map()은 요소들을 순회하여 다른 형식으로 변환하기도 합니다.

 

customerList.stream().map(c -> c.getName()).forEach(s -> System.out.println(s));

 

customerList. : 스트림 생성

stream(). map(c -> c.getName()). : 중간 연산

forEach(s -> System.out.println(s)); : 최종 연산

 

filter()와 map() 둘 다 함수를 수행하면서 해당 조건이나 함수에 맞는 결과를 추출해 내는 중간 역할을 합니다. 그리고 최종 연산으로 중간 연산 결과를 출력합니다.

 

최종 연산

최종연 산은 스트림의 자료를 소모하면서 연산을 수행하기 때문에 최종 연산이 수행되고 나면 해당 스트림은 더 이상 사용할 수 없습니다. 최종 연산은 결과를 만드는 데 주로 사용합니다. forEach()는 앞에서도 보았듯이 요소를 하나씩 꺼내는 기능을 합니다. 통계용으로 사용되는 sum(), count()는 배열 요소의 합계를 구한다든가 개수를 출력하는 등의 연산을 수행합니다.

최종 연산에는 forEach(), count(), sum(), reduce() 등이 있습니다.

 

스트림 생성하고 사용하기

정수 배열에 스트림 생성하고 사용하기

 

import java.util.Arrays;

public class IntArrayTest {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};

        int sumVal = Arrays.stream(arr).sum();
        int count  = (int) Arrays.stream(arr).count();

        System.out.println("count = " + count);
        System.out.println("sumVal = " + sumVal);
    }
}

 

 

출력 결과를 보면 배열의 합과 개수가 계산되는 것을 알 수 있습니다. count()의 반환 값이 long 형이므로 int 형으로 변환합니다. count(), sum() 이외에 max(), min(), average() 등 통계 연산을 위한 메서드도 제공합니다.

 

Collection에서 스트림 생성하고 사용하기

 

Collection 인터페이스를 구현한 클래스 중 가장 많이 사용하는 ArrayList에 스트림을 생성하고 활용해 보겠습니다. 

 

List<String> sList = new ArrayList<String>();
sList.add("Tomas");
sList.add("Edward");
sList.add("Jack");

위와 같은 문자열을 요소로 가지는 ArrayList가 있습니다. 이 ArrayList의 스트림을 생성하여 출력하고 , 정렬해보겠습니다.

 

Stream <E> stream() : 스트림 클래스를 반환합니다.

Collection<String> stream = sList.stream();

이렇게 생성된 스트림은 내부적으로 ArrayList의 모든 요소를 가지고 있습니다. 각 요소를 하나씩 출력하는 기능을 구현해보겠습니다.

모든 요소를 하나씩 가져와서 처리할 때 스트림의 forEach() 메서드를 활용합니다.

Stream<String> stream = sList.stream();
stream.forEach(s -> System.out.println(s));

 

forEach() 메서드는 내부적으로 반복문이 수행됩니다. 그럼 forEach() 괄호 안에 구현되는 람다식의 의미는 forEach() 메서드가 수행되면 요소가 하나씩 차례로 변수 s에 대입되고 이를 매개변수로 받아 출력문이 호출됩니다.

 

이번에는 ArrayList에 저장된 이름을 정렬하여 그 결과를 출력해 보겠습니다.

앞에서 stream 변수에 스트림을 생성했지만 forEach() 메서드가 수행되면서 자료가 소모됩니다. 따라서 스트림을 새로 생성해야 합니다.

 

        Stream<String> stream = sList.stream();
        stream.forEach(s -> System.out.println(s));
        System.out.println("================");
        Stream<String> stream2 = sList.stream();
        stream2.sorted().forEach(s -> System.out.println(s));

 

여기서에는 중간 연산으로 정렬을 위한 sorted() 메서드를 호출하고, 최종 연산으로 출력을 위해 forEach() 메서드를 사용합니다. sorted() 메서드를 사용하려면 정렬 방식에 대한 정의가 필요합니다. 따라서 사용하는 자료 클래스가 Comparable 인터페이스를 구현해야 합니다. 만약 구현되어 있지 않다면 sorted() 메서드의 매개 변수로 Comparator 인터페이스를 구현한 클래스를 지정할 수 있습니다. ArrayList 이외에 다른 Colection의 자료도 같은 방식으로 정렬하고 출력할 수 있습니다.

이것이 스트림을 사용하는 장점입니다.

 

 

 

스트림의 특징

지금까지 살펴본 스트림의 특징을 정리해보면서 글을 마치겠습니다.

 

자료의 대상과 관계없이 동일한 연산을 수행한다.

배열이나 컬렉션에 저장된 자료를 가지고 수행할 수 있는 연산은 여러 가지가 있습니다. 배열에 저장된 요소 값을 출력한다든지, 조건에 따라 자료를 추출하거나, 자료가 숫자일 때 합계 평균 등을 구할 수도 있습니다. 스트림은 컬렉션의 여러 자료 구조에 대해 이러한 작업을 일관성 있게 처리할 수 있는 메서드를 제공합니다.

 

한 번 생성하고 사용한 스트림은 재사용할 수 없다.

어떤 자료에 대한 스트림을 생성하고 이 스트림에 메서드를 호출하여 연산을 수행했다면 해당 스트림을 다시 다른 연산에 사용할 수 없습니다. 예를 들어 스트림을 생성하여 배열에 있는 요소를 출력하기 위해 각 요소들을 하나씩 순회하면서 출력에 사용하는데, 이때 요소들이 `소모된다`고  이야기합니다. 소모된 요소는 재사용할 수 없습니다. 만약 다른 기능을 호출하려면 스트림을 새로 생성해야 합니다.

 

스트림의 연산은 기존 자료를 변경하지 않는다.

스트림을 생성하여 정렬한다거나 합을 구하는 등의 여러 연산을 수행한다고 해서 기존 배열이나 컬렉션이 변경되지는 않습니다. 스트림 연산을 위해 사용하는 메모리 공간이 별도로 존제하므도, 스트림의 여러 메서드를 호출하더라도 기존 자료에는 영향을 미치지 않습니다.

 

스트림의 연산은 중간 연산과 최종 연산이 있다.

스트림에서 사용하는 메서드는 크게 중간 연산과 최종 연산 두 가지로 나뉩니다. 스트림에 중간 연산은 여러 개가 적용될 수 있고, 최종연 산은 맨 마지막에 한 번 적용됩니다. 만약 중간 연산이 여러 개 호출되었더라도 최종 연산이 호출되어야 스트림의 중간 연산이 모두 적용됩니다. 예를 들어 자료를 정렬하거나 검색하는 중간 연산이 호출되어도 최종 연산이 호출되지 않으면 정렬이나 검색한 결과를 가져올 수 없습니다. 이를 `지연 연산`이라고 합니다.

 

728x90

댓글