[Java] - Stream

janjanee·2021년 5월 6일
0

Java

목록 보기
3/19
post-thumbnail

스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메소드들을 정의해 놓았다.
스트림을 이용하면, 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두
같은 방식으로 다룰 수 있다.

스트림 특징

  • 스트림은 데이터를 소스로 이용해서 특정 처리를한다. 데이터를 담고 있는 저장소(컬렉션)이 아니다.

  • Funtional in nature, 스트림이 처리하는 데이터 소스를 변경하지 않는다.

  • 스트림은 일회용이다.

    • 한번 사용하면 닫혀서 다시 사용할 수 없다. 필요하다면 스트림을 다시 생성해야한다.
  • 무제한일 수도 있다. (메소드를 사용해서 제한할 수 있다)

  • 중개 연산 (intermediate operations)

    • 연산 결과가 스트림인 연산. 스트림에 연속해서 중개 연산을 할 수 있다.
    • filter, map, limit, skip, sorted, ...
    • 중개 연산은 lazy 하다. 종료 연산이 올 때 까지 실행되지 않는다.
  • 종료 연산(terminal operations)

    • Stream을 리턴하지 않음.
    • 스트림의 요소를 소모하므로 단 한번만 가능
    • collect, allMatch, count, forEach, min, max, ...
  • 스트림 파이프라인

    • 0 또는 다수의 중개 연산(intermediate operation)과 한 개의 종료 연산(terminal operation)으로 구성
  • Stream<Integer>와 IntStream

    • 기본적으로 Stream<T>지만, 오토박싱&언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림.
    • IntStream, LongStream, DoubleStream이 제공
  • 손쉽게 병렬 처리를 할 수 있다.

    • parallelStream()을 쓴다고 해서 무조건 빨라지거나 좋아지는 것은 아니다.
    • 쓰레드를 만들어서 처리하는 비용이 들고, 오히려 한 쓰레드에서 처리하는 것 보다 더 오래걸릴 수 있다.
    • 유용한 경우는 데이터가 방대하게 큰 경우. 대부분의 경우 stream()을 써도 무방.
    • 데이터 소스에 따라 다르고 처리하는 내용에 따라 달라질 수 있어서 case by case 이다.

스트림 생성

스트림의 소스가 될 수 있는 대상은 배열, 컬렉션

컬렉션

Stream<T> Collection.stream()

컬렉션의 최고 조상인 Collection에 stream() 이 정의되어 있다.
stream()은 해당 컬렉션을 소스(source)로 하는 스트림을 반환한다.

사용예시

List<Integer> list = Arrays.asList(1, 2, 3, 4);
Stream<Integer> intStream = list.stream();

배열

Stream<T> Stream.of(T... values)
Stream<T> Stream.of(T[])
Stream<T> Arrays.stream(T[])
Stream<T> Arrays.stream(T[] array, int startInclusive, int endExclusive)

배열을 소스로 하는 스트림을 생성하는 메소드는 Stream과 Arrays에 static 메소드로 정의되어 있다.

사용예시

Stream<String> strStream = Stream.of("a", "b", "c");

임의의 수

난수를 생성하는 Random 클래스에 아래의 인스턴스 메소드들이 포함되어 있다.

IntStream ints()
LongStream longs()
DoubleStream doubles()

이 메소드들은 해당 타입의 난수들로 이루어진 스트림을 반환한다.

사용예시

IntStream intStream = new Random().ints(0, 11);
intStream.limit(5).forEach(System.out::println);

반환하는 스트림은 크기가 정해지지 않은 '무한 스트림' 이어서 limit() 를 이용해서
스트림의 크기를 제한할 수 있다.

iterate(), generate()

static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
static <T> Stream<T> generate(Supplier<T> s)

람다식을 매개변수로 받아서, 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성한다.

사용예시

Stream<Integer> evenStream = Stream.iterate(0, n -> n + 2);
evenStream.limit(5).forEach(System.out::println);

Stream<Integer> oneStream = Stream.generate(() -> 1);
oneStream.forEach(System.out::println);
  • iterate()는 seed로 지정된 값부터 시작, 람다식에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복.

  • generate()는 이전 결과를 이용해서 다음 요소를 계산하지 않는다.

빈 스트림

Stream empty = Stream.empty();
long count = empty.count();

요소가 하나도 없는 비어있는 스트림을 생성할 수도 있다.

두 스트림 연결

String[] str1 = {"123", "456", "789"};
String[] str2 = {"abc", "def", "ghi"};

Stream<String> strStream1 = Stream.of(str1);
Stream<String> strStream2 = Stream.of(str2);
Stream<String> concatStream = Stream.concat(strStream1, strStream2);
  • concat()을 이용하여 두 스트림을 연결할 수 있다.
  • 두 스트림의 요소가 같은 타입이어야 한다.

스트림 중개 연산

스트림 자르기 - skip(), limit()

Stream<T> skip(long n)
Stream<T> limit(long maxSize)

스트림의 일부를 잘라낼 때 사용한다.

사용예시

IntStream intStream1 = IntStream.rangeClosed(1, 10);
intStream1.skip(3).limit(5).forEach(System.out::println);

skip(3)은 처음 요소 3개를 건너뛰고, limit(5)은 스트림의 요소를 5개로 제한한다.

스트림 요소 걸러내기 - filter(), distinct()

Stream<T> filter(Predicate<? super T> predicate)
Stream<T> distinct()
  • distinct() : 중복요소 제거
  • filter() : 주어진 조건에 맞지 않는 요소를 걸러냄

사용예시

IntStream intStream2 = IntStream.of(1, 2, 3, 4, 5, 6, 6, 6, 7, 3, 4);
intStream2.distinct()
        .filter(i -> i % 2 == 0)
        .forEach(System.out::println);

정렬 - sorted()

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

sorted()는 지정된 Comparator로 스트림을 정렬한다.

사용예시

Stream<String> sortStream = Stream.of("e", "d", "a", "b", "c", "f");
sortStream.sorted(Comparator.reverseOrder())
        .forEach(System.out::println);
  • Comparator 대신 int값을 반환하는 람다식을 사용하는 것도 가능하다.

  • Comparator를 지정하지 않으면 스트림 요소의 기본 정렬 기준(Comparable)로 정렬한다.

  • 스트림의 요소가 Comparable을 구현한 클래스가 아니라면 예외 발생.

변환 - map()

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

스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑아내거나 특정 형태로 변환.

사용예시

List<Coffee> starbucksCoffee = new ArrayList<>();
starbucksCoffee.add(new Coffee(1, "caffe americano", false));
starbucksCoffee.add(new Coffee(2, "caffe latte", false));
starbucksCoffee.add(new Coffee(3, "cold brew", false));
starbucksCoffee.add(new Coffee(4, "dolce cold brew", true));
starbucksCoffee.add(new Coffee(5, "vanilla cream cold brew", true));

Stream<String> nameStream = starbucksCoffee.stream()
        .map(Coffee::getName);
  • name으로 이루어진 stream 생성

조회 - peek()

starbucksCoffee.stream()
        .map(Coffee::getName)
        .peek(s -> System.out.println("name : " + s))
        .filter(s -> s.startsWith("cold"))
        .forEach(System.out::println);
  • 연산과 연산 사이에 올바르게 처리되었는지 확인하고 싶다면, peek()를 사용

  • forEach()와 달리 스트림의 요소를 소모하지 않으므로 연산 사이에 여러 번 넣어도 상관없다.

mapToInt(), mapToLong(), mapToDouble()

DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)
IntStream mapToInt(ToIntFunction<? super T> mapper)
LongStream mapToLong(ToLongFunction<? super T> mapper)

map()은 연산 결과로 Stream<T> 타입의 스트림을 반환한다.
스트림의 요소를 숫자로 변환하는 경우 IntStream과 같은 기본형 스트림으로 변환하는 것이
더 유용할 수 있다.

사용예시

Stream<Integer> integerStream = starbucksCoffee.stream().map(Coffee::getId);
IntStream intStream = starbucksCoffee.stream().mapToInt(Coffee::getId);

count()만 지원하는 Stream<T> 와 달리 IntStream같은 기본형 스트림은 아래와 같이

숫자를 다루는데 편리한 API를 제공한다.

int sum() : 스트림의 모든 요소의 총합
OptionalDouble average() : sum() / (double)count()
OptionalInt max() : 스트림의 요소 중 제일 큰 값
OptionalInt min() : 스트림의 요소 중 제일 작은 값

스트림의 요소가 하나도 없을 때, sum()의 경우 0을 반환해도 상관없지만,

다른 메소드들은 단순히 0을 반환할 수 없다. 따라서 Optional이 리턴 타입이다.

또한 종료연산이기 때문에 호출 후에 스트림이 닫힌다는 것을 주의해야한다.

따라서, sum()과 average()를 연달아 호출할 수가 없다.

sum()과 average()를 모두 호출해야 할 경우, 스트림을 또 생성하기 불편하므로

summaryStatistics()라는 메소드가 제공된다.

IntSummaryStatistics stat = intStream.summaryStatistics();
System.out.println(stat.getMax());
System.out.println(stat.getAverage());
System.out.println(stat.getSum());

flatMap()

스트림의 요소가 배열이거나 map()의 연산결과가 배열 인 경우

스트림의 타입이 Stream<T[]>일 때, Stream<T> 로 다루는 것이 더 편리할 수 있다.

map()을 사용했을 때와 flatMap()을 사용했을 때 리턴 타입을 살펴보면 차이를 알 수 있다.

사용예시

Stream<String[]> strArrStrm = Stream.of(
        new String[]{"apple", "banana", "mango"},
        new String[]{"cat", "dog", "horse"}
);

Stream<Stream<String>> strStrStrm = strArrStrm.map(Arrays::stream);
Stream<String> strStream = strArrStrm.flatMap(Arrays::stream);

종료 연산

forEach()

void forEach(Consumer<? super T> action)
  • peek()와 달리 스트림의 요소를 소모하는 종료연산이다.
  • 반환 타입 void, 스트림의 요소를 출력하는 용도로 많이 사용된다.

조건 검사 - allMatch(), anyMatch(), noneMatch(), findFirst(), findAny()

boolean allMatch (Predicate<? super T> predicate)
boolean anyMatch (Predicate<? super T> predicate)
boolean noneMatch (Predicate<? super T> predicate)

스트림의 요소에 대해 지정된 조건을 검사한다.

사용예시

boolean result = starbucksCoffee.stream()
          .anyMatch(c -> c.getName().contains("cold"));

Optional<Coffee> caffe = starbucksCoffee.stream()
        .filter(c -> c.getName().startsWith("caffee"))
        .findFirst();
  • allMatch - 모든 요소 일치
  • anyMatch - 일부가 일치
  • noneMatch - 어떤 요소도 일치 X
  • findFirst - 조건에 일치하는 첫 번째 것을 반환
  • findAny - 병렬스트림인 경우 사용

통계 - count(), sum(), average(), max(), min()

long count()
Optional<T> max(Comparator<? super T> comparator)
Optional<T> min(Comparator<? super T> comparator)

통계와 관련된 메소드들이다. 이름만봐도 무슨 동작을 하는지 유추가능하다.

사용예시

long count = starbucksCoffee.stream()
                .filter(c -> c.getName().startsWith("caffee"))
                .count();
  • 기본형 스트림(IntStream)이 아닌 경우에는 count(), max(), min()만 사용 가능

reduce()

Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce (T identity, BinaryOperator<T> accumulator)
U reduce (U identity, BiFunction<U, T, U> accumulator, BinaryOperator<U> combiner)
  • 스트림의 요소를 줄여나가면서 연산을 수행하고 최종결과를 반환한다.
  • 초기값(identity)을 갖는 reduce()도 있는데, 초기값과 스트림의 첫 번째 요소로 연산을 시작한다.
    • 스트림의 요소가 하나도 없는 경우, 초기값이 반환되므로 반환타입이 Optional이 아니라 T
  • 위 세 번째 메소드 마지막 매개변수 combiner는 병렬 스트림에 의해 처리된 결과를 합칠 때 사용

사용예시

int reduce = intStream1.reduce(0, (a, b) -> a + 1);
OptionalInt reduce1 = intStream1.reduce((a, b) -> a + b);
OptionalInt reduce2 = intStream1.reduce(Integer::max);
OptionalInt reduce3 = intStream1.reduce(Integer::min);

collect()

collect()는 종료 연산 중 하나이며 분량이 많아 별도로 분리

collect() : 스트림의 종료 연산, 매개변수로 컬렉터를 필요로 한다.
Collector : 인터페이스, 컬렉터는 이 인터페이스를 구현해야한다.
Collectors : 클래스, static 메소드로 미리 작성된 컬렉터를 제공

  • 스트림 종료 연산 중 가장 복잡하면서도 유용하게 활용되는 것이 collect()이다.
  • 스트림 요소를 어떻게 수집할 것인가에 대한 방법의 정의가 collector이다.
  • collector는 Collector 인터페이스를 구현한 것으로, 직접 구현할 수도 또는 미리 작성된 것을 사용할 수 도 있다.
  • Collectors 클래스는 미리 작성된 다양한 종류의 컬렌터를 반환하는 static 메소드를 갖고있다.
    • 이 클래스를 통해 제공되는 collector만으로도 많은 것을 할 수 있다.

컬렉션과 배열로 변환 - toList(), toSet(), toMap(), toCollection(), toArray()

List<String> names = starbucksCoffee.stream().map(Coffee::getName)
                .collect(Collectors.toList());

ArrayList<String> list = starbucksCoffee.stream().map(Coffee::getName)
                .collect(Collectors.toCollection(ArrayList::new));

Map<Integer, String> map = starbucksCoffee.stream()
                .collect(Collectors.toMap(Coffee::getId, Coffee::getName));

String[] strings = starbucksCoffee.stream().map(Coffee::getName)
                .toArray(String[]::new);
  • List나 Set이 아닌 특정 컬렉션을 지정하려면 toCollection() 에 해당 컬렉션의 생성자 참조를 매개변수로 넣는다.
  • Map은 키와 값을 객체의 어떤 값으로 사용할지 지정해주어야 한다.
  • T[] 타입 배열로 변환하려면, 매개변수로 해당 타입 생성자 참조를 지정해야 한다.
    • 지정하지 않을 경우 'Object[]'이다.

통계 - counting(), summingInt(), averagingInt(), maxBy(), minBy()

long count = starbucksCoffee.stream()
                .filter(c -> c.getName().startsWith("caffee"))
                .collect(Collectors.counting());

int count = starbucksCoffee.stream()
                .collect(Collectors.summingInt(Coffee::getId));

double count = starbucksCoffee.stream()
                .collect(Collectors.averagingDouble(Coffee::getId));
  • 종료 연산의 통계 정보를 collect()로 똑같이 얻을 수 있다.

  • groupingBy()와 함께 사용할 때 이 메소드들의 필요성을 알 수 있다. 아래에 이어서 나온다.

reducing()

IntStream lottoStream = new Random().ints(1, 46).distinct().limit(6);
Optional<Integer> max = lottoStream.boxed()
        .collect(Collectors.reducing(Integer::max));

int sum = starbucksCoffee.stream()
                .collect(Collectors.reducing(0, Coffee::getId, Integer::sum));
  • reducing 또한 collect() 로 가능하다.
  • IntStream에는 매개변수 3개짜리 collect()만 정의되어있어서 boxed() 를 통해
    IntStream을 Stream<Intger>로 변환 후 매개변수 1개짜리 collect()를 사용
  • 마지막 코드 매개변수 3개짜리는 map()과 reduce()를 하나로 합쳐놓은 것이다.

문자열 결합 - joining()

String join = starbucksCoffee.stream()
                .map(Coffee::getName)
                .collect(Collectors.joining());

String joinDelimiter = starbucksCoffee.stream()
                .map(Coffee::getName)
                .collect(Collectors.joining(","));

String joinDelemiterPrefixSuffix = starbucksCoffee.stream()
                .map(Coffee::getName)
                .collect(Collectors.joining(",", "[", "]"));
  • 스트림의 모든 요소를 하나의 문자열로 연결해서 반환한다.
  • 구분자를 지정할 수도 있고, 접두사와 접미사 지정도 가능하다.
  • 스트림의 요소가 String이나 StringBuffer처럼 CharSequence의 자손인 경우만 결합 가능

그룹화, 분할 - groupingBy(), partitioningBy()

  • 분할 : 스트림의 요소를 두 가지, 지정된 조건에 일치하는 그룹일치하지 않는 그룹으로 분할
// 1. 성별로 분할
Map<Boolean, List<Student>> stuBySex = studentStream
                .collect(partitioningBy(Student::isFemale));

// 2. 성별로 분할하여 학생 수 찾기
Map<Boolean, Long> stuNumBySex = studentStream
                .collect(partitioningBy(Student::isFemale, counting()));

// 3. 성별로 분할하여 1등 찾기
Map<Boolean, Optional<Student>> topScoreBySex = studentStream
                .collect(partitioningBy(Student::isFemale,
                        maxBy(Comparator.comparingInt(Student::getScore))));
  • 그룹화 : 스트림의 요소를 특정 기준으로 그룹화하는 것을 의미
// 1. 반 별로 그룹핑
Map<Integer, List<Student>> stdByBan = studentStream
                .collect(groupingBy(Student::getBan));

// 2. 성적 레벨(HIGH, MID, LOW)로 분류
Map<Student.Level, Long> stdByLevel = studentStream
                .collect(groupingBy(s -> {
                    if (s.getScore() >= 90) return Student.Level.HIGH;
                    else if (s.getScore() >= 70) return Student.Level.MID;
                    else return Student.Level.LOW;
                }, counting()));

Collector 구현

직접 컬렉터를 구현해보자. 컬렉터를 구현하는 것은 Collector 인터페이스를 구현하는 것이다.

Collector 인터페이스는 다음과 같이 정의되어 있다.

  • suppler() - 작업 결과를 저장할 공간을 제공
  • accumulator() - 스트림의 요소를 수집(collect)할 방법을 제공
  • combiner() - 두 저장공간을 병합할 방법을 제공(병렬 스트림)
  • finisher() - 결과를 최종적으로 변환할 방법을 제공
  • characteristics() - 컬렉터가 수행하는 작업의 속성에 대한 정보 제공

Stream의 모든 문자열을 하나로 결합해서 String으로 반환하는 ConcatCollector를 작성하자.

public class CollectorEx {
    public static void main(String[] args) {
        String[] sArr = { "hello", "cat", "dog" };
        Stream<String> sStream = Stream.of(sArr);
        String result = sStream.collect(new ConcatCollector());
        System.out.println(result);

    }
}

class ConcatCollector implements Collector<String, StringBuilder, String> {

    @Override
    public Supplier<StringBuilder> supplier() {
        return StringBuilder::new;
    }

    @Override
    public BiConsumer<StringBuilder, String> accumulator() {
        return StringBuilder::append;
    }

    @Override
    public BinaryOperator<StringBuilder> combiner() {
        return StringBuilder::append;
    }

    @Override
    public Function<StringBuilder, String> finisher() {
        return StringBuilder::toString;
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.EMPTY_SET;
    }
}

References

profile
얍얍 개발 펀치

0개의 댓글