스트림(Stream) 사용법 및 예제

강우엉·2023년 6월 28일
0

study

목록 보기
11/44

💡 스트림(Stream)이란?

자바 8에서 추가된 스트림은 컬렉션, 배열 등에 저장된 요소들을 하나씩 순회하며 코드를 실행할 수 있느 기능이다.

불필요한 for문을 사용하지않고, 람다식을 사용하여 코드를 간결하고 직관적이게 만들 수 있다.

아래와 같은 특징을 가진다.

  • Stream은 데이터를 담는 저장소가 아니다.
  • Stream은 데이터를 변경하지 않는다.
  • Stream은 재사용할 수 없다.
  • Stream은 각 요소가 1번씩 처리된다.
  • Stream은 무제한일 수도 있다. (실시간으로 계속 들어올 수 있음)

💡 스트림(Stream) 사용법

스트림을 사용하려면 먼저 스트림 객체를 생성해야한다.

다양한 예제를 통해 살펴보자.

📌 컬렉션

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

자바 코드에서 자주 사용하는 객체들은 stream() 메소드를 지원한다. 컬렉션 객체에서 stream() 메소드를 호출하면 스트림 객체를 만들 수 있다.

📌 배열

String[] array = new String[]{"a", "b", "c"};
Stream<String> stream1 = Arrays.stream(array);
Stream<String> stream2 = Arrays.stream(array, 1, 3); 
// 인덱스 1포함, 3제외 ("b", "c")

인자로 배열을 입력하면 배열을 순회하는 스트림 객체를 만들 수 있다. 배열과 시작, 종료 인덱스를 인자로 주면 배열의 일부를 순회하는 스트림 객체를 만들 수도 있다. 종료하는 인덱스 번호는 제외된다.

📌 빌더

String<String> stream = Stream<String>builder()
                          .add("kang")
                          .add("kim")
                          .add("lee")
                          .build();

직접 값을 입력해서 스트림 객체를 생성하는 법도 있다.

📌 Generator

데이터를 생성하는 람다식을 이용해서 스트림을 생성할 수도 있다.

public static<T> Stream<T> generate(Supplier<T> s) { ... }

Supplier에 해당하는 람다식이 데이터를 생성하는 람다식이다.

Stream<String> stream = Stream.generate(() -> "Hello").limit(5);

generate() 메소드의 인자로 "Hello"를 찍어주는 람다식을 주었다. 이렇게 되면 "Hello"라는 데이터를 무한대로 생성하는 스트림이 만들어진다. 여기에 limit() 메소드를 이용해서 5개 까지만 찍도록 정해주었다.

📌 Iterator

Stream<String> stream = Stream.iterate(100, n -> n + 10).limit(3);
// (100, 110, 120)

위와 같은 람다를 인자로 넘겨 100부터 10씩 증가하는 스트림을 만들었다. limit() 메소드를 활용하여 3개까지 출력하였다.

📌 Empty 스트림

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

stream 객체를 참조하는 변수가 null일 가능성이 있다. 이럴때 NullPointException이 발생하게 되는데, Stream.empty()를 사용하면 된다.

📌 기본 타입

IntStream intStream = IntStream.range(1, 10); // 1 ~ 9
LongStream longStream = LngStream.range(1, 10000); // 1 ~ 9999

자바에서 기본타입에 대해서 오토박싱과 언박싱이 발생한다. int 변수를 다룰 때, Integer 클래스로 오토박싱해서 처리하는 경우가 있는데, 이 경우 오버헤드가 발생해서 성능저하가 있을 가능성이 있다. 스트림 객체에서도 오토박싱을 하지 않으려면 위 코드와 같이 스트림을 사용하면 된다.

Stream<Integer> stream = IntStream.range(1, 10).boxed();

제네릭을 이용한 클래스로 사용하려면 박싱을 해서 사용해야한다.

DoubleStream stream = new Random().double(3); // 랜덤 숫자 3개 생성

랜덤값을 스트림을 사용해서 뽑고 싶다면 Random() 클래스를 사용하면 된다.

📌 문자열

IntStream stream = "Hello,World".chars(); 
//(72, 101, 108, 108, 111, 44, 87, 111, 114, 108, 100)

문자열을 구성하고 있는 문자들의 ASCII 코드 값을 스트림형태로 뽑아주는 예제코드다.

Stream<String> stream = Pattern.compile(",").splitAsStream("Apple,Banana,Melon");

특정 구분자(Delimiter)를 이용해서 문자열을 스플릿 한 다음 각각을 스트림으로 뽑아낼 수도 있다.

📌 파일

Stream<String> stream = 
        Files.lines(Paths.get("test.txt"), Charset.forName("UTF-8"));

test.txt 파일의 데이터를 라인단위로 읽어서 뽑아주는 스트림 객체이다. 이 때, 데이터는 ' UTF-8'로 디코딩해서 읽어들인다.

📌 스트림 연결

Stream<String> stream1 = Stream.of("Apple", "Banana", "Melon");
Stream<String> stream2 = Stream.of("Kim", "Lee", "Park");

Stream<String> stream3 = Stream.concat(stream1, stream2);
// "Apple", "Banana", "Melon", "Kim", "Lee", "Park"

Stream.concat() 메소드를 이용해서 두 개의 스트림을 붙여서 새로운 스트림을 만들 수 있다.

📌 Filter

filter는 스트림에서 뽑아져 나오는 데이터에서 특정 데이터들만 골라내는 역할을 한다.

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

filter() 메소드에는 boolean 값을 리턴하는 람다식을 넘겨주게 된다. 뽑아져 나오는 데이터에 대해 람다식을 적용해서 true가 리턴되는 데이터만 선별한다.

filter를 사용한 간단한 예제이다.

Stream<Integer> stream = IntStream.range(1, 10).boxed();
stream.filter(v -> ((v % 2) == 0))
            .forEach(System.out::println);
// 2, 4, 6, 8

1부터 9까지 데이터를 뽑아내는 스트림을 만들고, filter 메소드에 짝수만을 출력하는 람다를 넣어주면 짝수만 출력을 할 수 있다.

📌 Map

map()은 스트림에서 뽑아져 나오는 데이터에 변경을 가해준다.

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

map() 메소드는 값을 변환해주는 람다식을 인자로 받는다. 스트림에서 생성된 데이터에 map() 메소드의 인자로 받은 람다식을 적용해 새로운 데이터를 만들어낸다.

map()을 사용한 예제를 살펴보저

Stream<Integer> stream = IntStream.range(1, 10).boxed();
stream.filter(v -> ((v % 2) == 0))
            .map(v -> v * 10)
            .forEach(System.out::println);
// 20, 40, 60, 80

1부터 9까지의 수 중 filter를 사용하여 짝수만 뽑아내고 그 수에 10을 곱합 값을 출력한다.

📌 flatMap

map() 메소드와 비슷한 역할을 하는 flatMap() 메소드도 있다.

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

flatMap()메소드의 인자로 받는 람다는 리턴 타입이 Stream이다. 즉, 새로운 스트림을 생성해서 리턴하는 람다를 인자로 받는다. flatMap()은 중첩된 스트림 구조를 한단계 적은 단일 컬렉션에 대한 스트림으로 만들어주는 역할이다. 이러한 작업을 플랫트닝(Flattening) 이라고 한다.

예제로 살펴보자

List<List<String>> list = Arrays.asLists(Arrays.asList("A", "B", "C"),
                                         Arrays.asList("a", "b", "c"));
// [["A", "B", "C"], ["a", "b", "c"]]

이런 형태의 이중 구조 리스트를 스트림으로 순회한다고 생각해보자.

List<List<String>> list = Arrays.asLists(Arrays.asList("A", "B", "C"),
                                        Arrays.asList("a", "b", "c"));
List<String> flatList = list.stream()
                            .flatMap(Collection::stream)
                            .collect(Collectors.toList());
// ["A", "B", "C", "a", "b", "c"]

위에서 람다는 (e)->Collection.stream(e) 이며, Collection::stream으로 축약하였다.

📌 Sorted

sorted() 메소드를 이용하여 정렬한다.

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

sorted()를 그냥 호출하면 기본값인 오름차순으로 정렬한다. 만약 두 값을 비교하고 싶다면 comparator를 sorted() 메소드의 인자로 넘겨줄 수도 있다.

📌 Peek

peek() 메소드는 map() 메소드처럼 연산을 수행한다. 하지만 새로운 스트림이 아닌 그냥 인자로 받은 람다를 적용하기만 한다.

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

아래부터는 수정 연산이아닌 값을 생성하는 부분이다.

📌 통계값

정수 값을 받는 스트림의 마무리는 총합을 구하거나 최대, 최소, 숫자의 개수, 평균 등에 대한 계산이다.

int sum = IntStream.range(1, 10).sum();
int count = IntStream.range(1, 10).count();

int max = IntStream.range(1, 10).max();
int min = IntStream.range(1, 10).min();
int avg = IntStream.range(1, 10).average();

// 짝수 숫자의 총합
int evenSum = IntStream.range(1, 10)
                       .filter(v -> ((v % 2) == 0))
                       .sum();

비어있는 스트림의 경우 개수, 합계를 출력하였을때 0을 리턴한다.

📌 Reduce

중간 연산을 한 값들은 reduce 메소드를 사용해 결과값을 만든다.
redyce() 메소드는 파라미터에 따라 3가지 종류가 있다.

// 스트림에서 나오는 값들을 accumulator 함수로 누적
Optional<T> reduce(BinaryOperator<T> accumulator);

// 동일하게 accumulator 함수로 누적하지만 초기값(identity)이 있음
T reduce(T identity, BinaryOperator<T> accumulator);

우선 스트림에서 뽑아져 나오는 값들을 누적시키는 accumulator 함수는 2개의 파라미터를 인자로 받아 하나의 값을 리턴하는 함수형 인터페이스다.

📌 Collect

스트림에서 많이 이용하는 패턴 중 하나는 컬렉션의 엘리먼트 중 일부분을 필터링하고, 값을 가공하여 또 다른 컬렉션으로 만드는 것이다.

Set<Integer> evenNumber = IntStream.range(1, 1000).boxed()
                                   .filter(n -> (n%2 == 0))
                                   .collect(Collectors.toSet());

collect() 메소드를 이용해 뽑아져 나오는 데이터들을 컬렉션으로 모아 둘 수 있다. 위 코드는 Set 컬렉션에 모아두는 예제이다.

Collector.toList()를 호출하면 리스트를 만들고, Collector.toSet()을 호출하면 Set으로 만들어준다.

📌 foreach

스트림에서 나오는 값을 어떤 작업을 하고싶다면 foreach 메소드를 사용한다.

Set<Integer> evenNumber = IntStream.range(1, 1000).boxed()
                                   .filter(n -> (n%2 == 0))
                                   .forEach(System.out::println);

Reference

자바 스트림(Stream) 사용법 및 예제

profile
우엉이의 코딩 성장일기💻

0개의 댓글