[JAVA] Stream

가영·2021년 1월 10일
0
post-thumbnail

스트림이란? 🙄

자료가 모여있는 배열이나 컬렉션 또는 특정 범위 안에 있는 일련의 숫자를 처리하는 기능을 구현해놓은 클래스!

이때, 입출력을 위한 I/O 스트림과는 전혀 다른 개념이다 😣

배열을 예시로 보자 ❗

밑에 코드는 정수 5개를 요소로 가진 배열을 선언하고 그 요소들을 모두 출력한다.

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

다음 코드는 위의 코드와 같은 일을 한다.

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

좀 더 자세히 보자면, Arrays.stream(arr)은 배열 arr의 스트림을 생성하는 부분이고, 생성된 스트림 인스턴스의 메서드인 forEach() 를 사용해서 요소 하나마다 접근하는 것이다. 🤩

스트림 연산 🎀

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

  • 중간 연산: 자료를 거르거나 변경하여 또 다른 자료를 내부적으로 생성한다.
  • 최종 연산: 생성된 내부 자료를 소모해 가면서 연산을 수행한다.

지금은 일단 많이 쓰이는 연산만 살펴보장 🥰

중간 연산

: filter(), map()

filter()는 조건을 넣고 그 조건에 맞는 참인 경우만 추출하는 경우에 사용한다.

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

map()은 스트림의 요소들을 원하는 자료로 변형할 수 있다.

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

문서를 보면, map()은 스트림 객체를 반환한다고 한다고 돼있당 🤗

Returns a stream consisting of the results of applying the given function to the elements of this stream.

최종 연산

: forEach(), count(), sum(), reduce(), min(), max(), average()

최종 연산은 스트림의 자료를 소모하면서 연산을 수행하기 때문에 최종연산이 수행되고 나면 해당 스트림은 더 이상 사용할 수 없다 ❗ 최종연산은 결과를 만드는 데 주로 사용한다.

forEach()는 맨 처음 예제 코드에서 쓴 것처럼, 요소를 하나씩 꺼내는 기능을 한다.

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

sum()은 배열 요소의 합계를 구하는 연산, count()는 개수를 출력하는 등의 연산을 수행할 때 사용한다.

다음 간단한 예시들을 보면서 스트림이 어떤건지 느낌이 왔다👍🏻

정수 배열

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(); //count() 메서드 반환 값이 long형
        
        System.out.println("sum: " + sumVal);
        System.out.println("count:" + count);
    }
}
// console output
sum: 15
count: 5

ArrayList 🔗

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

이 ArrayList의 스트림을 생성해서 출력/정렬을 해봅시당🤗

우선 Stream 객체를 stream() 메서드로 생성해준다~!

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

이렇게 생성된 스트림은 내부적으로 ArrayList의 모든 요소를 가지고 있다. 각각의 요소들을 원하는대로 연산하면 된다 ❗

Stream<String> stream = sList.stream();
stream.forEach(s -> System.out.print(s+" ")); //출력
System.out.println();
Stream<String> stream2 = sList.stream();
stream2.sorted().forEach(s -> System.out.print(s+" "));
// console output
Tomas Edward Jack
Edward Jack Tomas

스트림의 특징 😝

이걸 알아야 스트림을 쓸 수 있겠지 ✨

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

배열이나 컬렉션에 저장된 자료를 가지고 수행할 수 있는 연산은 여러가지가 있는데, 클래스마다 같은 일을 수행하는 메서드도 다르게 사용해야하는 경우가 많다. 스트림은 컬렉션의 여러 자료구조에 대해 일관성있는 작업이 가능하게 한다 🥰

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

어떤 자료에 대한 스트림을 생성하고 이 스트림에 메서드를 호출해서 연산을 수행했으면, 그 스트림은 다시 다른 연산에 사용할 수 없다😂 최종연산에서 요소들을 하나씩 순회하면서 소모한다고 표현을 하기도 한다.

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

스트림을 생성해서 연산을 수행한다고 해서 기존 배열이나 컬렉션이 변경되지는 않는다! 스트림 연산을 위해 사용하는 메모리 공간이 별도로 존재하므로, 스트림의 여러 메서드를 호출해도 기존 자료에는 영향을 미치지 않는당✨

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

스트림에 중간연산은 여러 개가 적용될 수 있고, 최종 연산은 맨 마지막에 한 번 적용된다. 만약 중간 연산이 호출하고 나서, 최종 연산을 호출하지 않으면 스트림의 중간 연산이 적용되지 않는다. 😥

예를 들어, 자료를 정렬하거나 검색하는 중간 연산이 호출돼도 최종 연산이 호출되지 않으면 중간 연산 결과를 가져올 수 없다. 🤔


기능을 지정하는 reduce() 연산

reduce() 연산은 내부적으로 스트림의 요소를 하나씩 소모하면서 프로그래머가 직접 지정한 기능을 수행한다.

reduce() 메서드의 정의는

T reduce(T identify, BinaryOperator<T> accumulator)

첫 번째 매개변수 T identify는 초깃값을 의미,

두 번째 매개변수 BinaryOperator<T> accumulator는 수행해야 할 기능🔨이다.

BinaryOperator 인터페이스는 두 매개변수로 람다식을 구현하며 이 람다식이 기능 메서드가 된다. 람다식을 직접 매개변수에 써도 되고, 람다식이 길면 인터페이스를 구현한 클래스를 생성해서 대입해도 된다. 바로 예시를 보자.

Arrays.stream(arr).reduce(0, (a, b) -> a + b);

위의 코드는 reduce()를 이용해서 arr의 원소의 합을 구한다! 초깃값은 0이고 스트림의 요소가 매개변수로 전달되면서 람다식이 요소 갯수만큼 반복 호출되는 것이당.🎈

여기서 문제😏 그럼 reduce()는 중간연산일까 최종연산일까?

👉🏻 최종연산이다 ❗

왜냐하면 reduce()는 처음부터 마지막까지 모든 요소를 소모하면서 람다식을 반복 수행하기 때문이라고 할 수 있다.


다음은 문자열 배열 dummy 중에서 가장 바이트수가 큰 문자열을 찾기 위해 스트림을 이용해보는 예제다 👻

Use Lambda expression 👍🏻

public class ReduceTest {
    public static void main(String[] args) {
        String[] dummy = {"Hello I'm potato", "넌거북목왕", "자바를 자바봄", "보고씨뻐"};
        System.out.println(Arrays.stream(greetings).reduce("", (a, b) -> {
            if(s1.getBytes().length >= s2.getBytes().length)
                return s1;
            else return s2;
        }))
    }
}

Implement BinaryOperator 🥰

class CompareString implements BinaryOperator<String> {
    @override
    public String apply(String s1, String s2) {
        if(s1.getBytes().length >= s2.getBytes().length) return s1;
        else return s2;
    }
}

public class ReduceTest {
    public static void main(String[] args) {
        String[] dummy = {"Hello I'm potato", "넌거북목왕", "자바를 자바봄", "보고씨뻐"};
        String str = Arrays.stream(greetings).reduce(new CompareString()).get();
        System.out.println(str);
    }
}

위 스니펫에서와 같이 reduce()의 파라미터로 BinaryOperator를 구현한 객체 자체를 넣어주면 된다. 이렇게만 해주면 클래스에 구현된 apply() 메서드가 자동으로 호출된다!

reduce() 메서드의 도큐먼트를 보면

Returns:

an Optional describing the result of the reduction

Optional 객체를 반환하기 때문에, 객체 안에 담겨있는 결과 값 (저 예제에서는 String이 되겠징)을 얻기 위해서는 Optional 클래스의 get() 메서드를 호출해야 한다 😜


호오옹 스트림이 이런거였구나 하고 알 수 있는 시간 ~ 😘 자바스크립트랑 되게 비슷하댜 . 반갑다 증말

1개의 댓글

comment-user-thumbnail
2022년 12월 22일

스트림이랑 reduce부분 이해하느라 시간이 초큼 걸렸네요^^ 이모티콘으로 글자를 나눠놔서 보기 너무 편하네요^^ 더 써주시길 바랍니다~

답글 달기