[Java] 스트림(Stream)

최정은·2022년 10월 19일
0

Backend Roadmap

목록 보기
9/23
post-thumbnail

스트림이란?

Java 8부터 추가된 스트림(Stream)은 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자다. 스트림을 활용해서 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있다. 또한 람다를 이용해서 코드의 양을 줄이고 간결하게 표현 가능하다.

  • Java 6 이전 : iterator 객체 이용
  • forEach 구문 이용
  • 스트림 이용

스트림의 특징

  • 스트림은 원본 데이터를 변경하지 않는다.
  • 스트림은 일회성이다. 컬렉션의 요소를 모두 읽고 나면 닫혀서 다시 사용할 수 없다. 필요하다면 재생성해야 한다.
  • 내부 반복자를 사용하므로 병렬 처리가 가능하다. 내부 반복자를 사용해서 얻는 이점은 컬렉션 내부에서 어떻게 요소를 반복시킬 것인가는 컬렉션에게 맡겨두고, 개발자는 요소 처리 코드에만 집중할 수 있다는 것이다.

스트림의 구조

1) 스트림 생성 : 스트림 인스턴스 생성
2) 스트림 데이터 가공 : 필터링 및 매핑 등을 통해 원하는 결과를 만들어가는 중간 작업
3) 결과 만들기 : 최종적으로 결과를 만들어내는 작업

스트림 생성

스트림을 이용하려면 스트림을 생성해야 한다.

스트림 생성 (배열)

String[] array = new String[]{"a", "b", "c", "d"};
Stream<String> stream1 = Arrays.stream(array);
Stream<String> stream2 = Arrays.stream(array, 1, 3); // 시작 인덱스 1, 종료 인덱스 3 -> ["b", "c"]

스트림 생성 (컬렉션)

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

스트림 생성 (컬렉션)

Stream<String> builderStream = Stream.<String>builder()
                .add("Python")
                .add("C")
                .add("Java")
                .build();
                
// ["Python", "C", "Java"]

스트림 생성 (generate)

generate 메서드를 이용하면 Supplier<T> 에 해당하는 람다로 값을 넣을 수 있다. Supplier<T> 는 인자는 없고 리턴값만 있는 함수형 인터페이스다. 람다에서 리턴하는 값이 들어간다.

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

이때 생성되는 스트림은 크기가 무한하기 때문에 limit 을 호출하여 특정 사이즈로 최대 크기를 제한해줘야 한다.

Stream<String> generatedStream = Stream.generate(() -> "hi").limit(5);

// ["hi", "hi", "hi", "hi", "hi"]

스트림 생성 (iterate)

iterate 메서드를 이용하면 초기값과 해당 값을 다루는 람다를 이용해서 스트림에 들어갈 요소를 생성한다. 마찬가지로 생성되는 스트림의 크기가 무한하기 때문에 특정 사이즈로 제한해야 한다.

Stream<String> stream = Stream.iterate(30, n -> n + 2).limit(5);

// ["30", "32", "34", "36", "38"]

스트림 생성 (기본 타입)

range와 rangeClosed는 범위에 차이가 있다. range는 두번째 인자인 종료 지점이 포함되지 않는다.

IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4]
LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5]

위 경우는 오토박싱이 수행되지 않는다.
제네릭을 이용한 클래스로 사용하려면 박싱을 해서 사용해야 한다.

Stream<Integer> boxedIntStream = IntStream.range(1, 5).boxed();

스트림 생성 (두 개의 스트림 연결)

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

// ["Apple", "Banana", "Grape", "Kim", "Lee", "Park"]

이 외에도 빈 스트림, 문자열 스트림, 파일 스트림 등 다양한 형식의 만들 수 있다. 더보기

스트림 데이터 가공

데이터의 형변환, 필터링, 정렬 등 스트림에 대한 가공을 수행한다. 데이터를 가공해주는 메서드들은 가공된 결과를 생성해주는 스트림 객체를 리턴한다.

가공 (filter)

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

filter 메서드는 boolean 값을 리턴하는 람다식을 넘겨주게 된다. 각 요소들에 대해 람다식을 적용해서 true가 리턴되는 데이터만 선별한다.

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

가공 (map)

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

map은 스트림 내 요소들을 하나씩 특정 값으로 변환해준다. 이때 값을 변환하기 위한 람다를 인자로 받는다.

Stream<String> stream = Pattern.compile(", ").splitAsStream("Apple, Banana, Grape");
stream.map(String::toUpperCase)
		.forEach(System.out::println);

가공 (flatMap)

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

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

// 중첩된 리스트
List<List<String>> list = Arrays.asList(Arrays.asList("a"), Arrays.asList("b")); // [["a"], ["b"]]

// 중첩 구조 제거
List<String> flatList = list.stream()
		.flatMap(Collection::stream)
		.collect(Collectors.toList()); // ["a", "b"]

가공 (sorted)

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

인자 없이 sorted 메서드를 호출할 때는 오름차순으로 정렬한다. 만약 정렬할 때 두 값을 비교하는 별도의 로직이 있다면, comparator를 sorted 메서드의 인자로 넘겨줄 수 있다.

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

lang.stream()
		.sorted()
		.collect(Collectors.toList());
// ["Go", "Groovy", "Java", "Python", "Scala", "Swift"]

        lang.stream()
                .sorted(Comparator.reverseOrder())
                .collect(Collectors.toList());
// ["Swift", "Scala", "Python", "Java", "Groovy", "Go"]

가공 (peek)

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

peek 메서드는 스트림 내 요소들을 대상으로 map 메서드처럼 연산을 수행한다. 하지만 새로운 스트림을 생성하지는 않고 인자를 받은 람다를 적용하기만 한다.

int sum = IntStream.of(1, 3, 5, 7, 9)
		.peek(System.out::println)
		.sum();

결과 만들기

가공한 스트림을 가지고 내가 사용할 결과값으로 만들어내는 단계다. 스트림을 끝내는 최종 작업이다.

결과 만들기 (calculating)

총합, 최대값, 최솟값, 숫자 개수, 평균값 등에 대한 계산을 수행한다.
sum, min, max, count, average
최대, 최소, 평균값의 경우 스트림이 비어있을 경우, 표현할 수 없기 때문에 Optional 을 이용해 리턴한다.

결과 만들기 (reduce)

reduce 메서드는 파리미터에 따라 3가지 종류가 있다.

  • accumulator : 스트림의 요소들의 값을 누적시킨다.
  • identity : accumulator 함수로 누적하지만 초기값이 존재한다.
  • combiner : 병렬 스트림에서 나눠서 계산한 결과를 하나로 합치는 동작을 수행한다.

결과 만들기 (collect)

스트림은 collect 메서드를 이용해 나오는 데이터들을 컬렉션으로 모을 수 있다. collect 메소드에 Collector 메서드를 사용할 수 있다.

List<String> fruit = Arrays.asList("Banana", "Apple", "Grape");
String returnValue = fruit.stream()
		.collect(Collectors.joining());

//BananaAppleGrape

Reference

스트림 개념

profile
https://dolmeng22.tistory.com 로 이전했습니다~

0개의 댓글