TIL | [Java] 자바 Stream

hyemin·2022년 3월 17일
0

Java

목록 보기
6/10
post-thumbnail
post-custom-banner

항해99 2주차 알고리즘 주간 때 주어진 문제를 풀면서 다른 사람들의 풀이도 살펴보니 공통적으로 몇가지 자주 사용되는 것들이 있었고 그 중 하나인 Java의 Stream에 대해 공부하고자 한다.

스트림(Stream)

자바 스트림은 Java8부터 지원되기 시작한 기능으로 컬렉션에 저장되어 있는 요소들을 하나씩 돌면서 처리할 수 있는 코드 패턴이다. Stream 이전에는 for, foe-each문을 이용하여 배열이나 컬렉션 인스턴스의 요소를 다룰 수 있었지만 로직이 복잡해질수록 코드가 길어지거나 루프를 여러번 돌게 되는 등의 문제가 있었다.

반면 Stream은 "데이터의 흐름"이라 배열, 컬렉션 안에서 함수를 조합해 원하는 값을 가공하거나 필터링해 얻을 수 있으며, 람다를 이용해 코드를 간결하게 표현할 수 있다.

살펴볼 내용

  1. 생성하기 - Stream 인스턴스 생성
  2. 가공하기 - Filter, Map, Sorted, Peak (Intermediate Operations)
  3. 결과 만들기 - Terminal Operations

생성하기

배열과 컬렉션 외에도 다양한 방법으로 Stream을 생성할 수 있다.

1. 배열 Stream

배열은 Arrays.stream() 메소드 인자에 배열을 입력하여 배열을 순회하는 Stream 객체를 만 수 있다.

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

2. 컬렉션 Stream

Collection, List, Set과 같은 컬렉션 타입의 경우 stream() 메소드를 이용해 Stream 객체를 만들 수 있다.

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

3. Stream builder

배열/컬렉션을 이용해서가 아니라 빌더(builder)를 통해 Stream에 직접 원하는 값을 넣을 수 있다. add로 값을 추가하고 build() 메소드로 Stream을 반환할 수 있다.

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

4. Stream generate

Supplier<T>에 해당하는 람다를 이용해 Stream을 생성하는 것이다.

Supplier - 인자는 없고 return값만 존재하는 함수형 인터페이스

Supplier<T>를 통해 return값을 생성하게 된다.

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

Stream.generate는 무한한 Stream이 되기 때문에 크기를 제한해서 Stream을 생성해줘야 한다.

Stream<String> stream = Stream.generate(() -> "hyemco").limit(3);

limit()을 이용해 3개의 "hyemco"가 들어간 Stream이 생성된다.

5. 기본형 Stream

generate를 사용할 수도 있지만, List나 배열을 이용해 기본 타입(int, long, double) Stream을 바로 생성할 수 있다.

IntStream stream = IntStream.range(1, 3)  // [1, 2]
LongStream stream = LongStream.raneClosed(1, 3)  // [1, 2, 3]

range()rangeClosed()는 범위의 차이로 후자가 종료 인자를 포함하여 범위를 생성한다.

++ Stream 연결하기

Stream.concat을 이용하여 두 개의 Stream을 연결해 새로운 Stream 객체를 생성할 수 있다.

Stream<String> stream1 = Stream.of("one", "two", "three");
Stream<String> stream2 = Stream.of("하나", "둘", "셋");
Stream<String> concat = Stream.concat(stream1, stream2);
// [one, two, three, 하나, 둘, 셋]

가공하기

생성한 Stream 객체를 이용하여 Stream 내 요소들을 가공하여 원하는 요소들만 뽑아낼 수 있다. 이러한 과정을 중간 작업(intermediate operations)이라고 하며, 작업을 마친후 다시 Stream 객체에 return하기 때문에 여러 가공작업을 한번에(chaining) 할 수도 있다.

1. Filter

필터(Filter)는 Stream 객체 내 각 요소들을 평가해 원하는 기준에 부합하는 요소들만 걸러내주는 작업이다.

boolean값을 return하는 함수형 인터페이스 Predicate가 사용되어 true인 값만 return하게 된다.

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

1부터 9까지 들어가는 Stream을 생성하고, filter 메소드에 짝수를 선별하는 람다식을 넣어줘서 1~9 중 짝수 요소만 stream 객체에 return된다.

2. Map

맵(Map)은 stream 객체 내 요소들을 특정 값으로 변환해주는 메소드로 변환을 위해 람다식을 인자로 받는다.

List<String> names = Arrays.asList("Eden", "Jin", "Harry");
Stream<String> stream = names.stream().map(String::toUpperCase);
// [ERIC, JIN, HARRY]

map()과 비슷한 flatMap() 메소드도 있는데, 이는 인자로 받는 람다식의 return 타입이 Stream이다. 보통 중첩 구조인 Stream을 한 단계 제거한 단일 Stream으로 만들어주는데 사용하며, 이를 플래트닝(flattening)이라고 부른다.

List<List<String>> list = 
Arrays.asList(Arrays.asList("a", "b", "c"), 
              Arrays.asList("가", "나", "다"));
// [[a, b, c], [가, 나, 다]]

위의 중첩 구조 리스트를 flatMap으로 중첩 구조를 제거할 것이다.

List<String> flatList = list.stream().flatMap(Collection::stream)
									 .collect(Collectiors.toList());
// [a, b, c, 가, 나, 다]

3. Sorted

Stream 객체 내 요소들을 정렬하고자할 때 사용되는 메소드이다.
다른 정렬과 마찬가지로 Comparator를 이용한 메소드이다.

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

1) 인자 없이 호출한 경우 - 오름차순 정렬

IntStream.of(11, 7, 24, 2).sorted().boxed().collect(Collectors.toList());
// [2, 7, 11, 24]

2) 인자를 넘겨 정렬할 경우

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

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


// Comparator로 알파벳 역순으로 정렬
lang.stream()
  .sorted(Comparator.reverseOrder())
  .collect(Collectors.toList());
// [Swift, Scala, Python, Java, Groovy, Go]

4. Peak

람다식 Consumer를 인자로 받기 때문에 요소를 가공은 하지만 return은 하지않는 메소드이다.

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

그렇기에 Stream 내 연산 중 중간 과정의 결과를 확인해 볼 때 자주 사용된다.

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

결과 만들기

위에서는 Stream을 가공하고 또 다른 Stream 객체에 return해준 중간 작업(Intermediate Operations)들이며, 가공한 요소를 출력하거나 다른 배열/컬렉션 등으로 보내주는 최종 작업(Terminal Operations)을 해야 한다.

1. Collecting

collect 메소드는 Collector 타입의 인자를 받아 처리하며, 이는 Collectors 객체에서 제공된다.

아래와 같은 예제가 있다고 하자.

List<Product> productList = 
  Arrays.asList(new Product(23, "potatoes"),
                new Product(14, "orange"),
                new Product(13, "lemon"),

Collectors.toList()

Stream에서 작업한 결과를 리스트로 반환해준다.

List<String> list =
  productList.stream()
    .map(Product::getName)
    .collect(Collectors.toList());
// [potatoes, orange, lemon]

Collectors.joining()

Stream에서 작업한 결과를 하나의 String으로 이어 붙인다.

String listToString = 
 productList.stream()
  .map(Product::getName)
  .collect(Collectors.joining());
// potatoesorangelemon

String listToString = 
 productList.stream()
  .map(Product::getName)
  .collect(Collectors.joining(", ", "<", ">"));
// <potatoes, orange, lemon, bread, sugar>

joining의 첫 번째 인자(delimiter)는 요소 구분자, 두번째 인자(prefix)는 return값 제일 앞에 붙는 문자, 세번째 인자(suffix)는 제일 마지막에 붙는 문자이다.

++
이외에도 아래와 같은 메소드들이 있다.

  • Collectors.averagingInt() - 숫자 값의 평균 구하기
  • Collectors.summingInt() - 숫자 값의 합 구하기
  • Collectors.summarizingInt() - 숫자 값의 개수, 합, 평균, 최솟값, 최댓값 구하기
  • Collectors.groupingBy() - 특정 조건으로 요소 그룹 짓기
  • Collectors.partitioningBy() - 특정 조건으로 요소 그룹 짓기(boolen값 리턴)
  • Collectors.collectingAndThen() - collect 이후 추가 작업이 필요할 때
  • Collectors.of() - collector 커스텀하기

2. 숫자 값의 통계

sumcount는 Stream이 비어있을 경우 0을 출력한다. 최솟값, 최댓값은 Optional을 이용해 return한다.

int sum = IntStream.of(2, 4, 6, 8, 10).sum();
int count = IntStream.of(2, 4, 6, 8, 10).count();
int average = IntStream.of(2, 4, 6, 8, 10).average();

OptionalInt min = IntStream.of(2, 4, 6, 8, 10).min();
OptionalInt max = IntStream.of(2, 4, 6, 8, 10).max();

3. Reduce

reduce 메소드에는 3 종류의 파라미터가 존재한다.

  • accumulator - Stream 요소가 올 때마다 결과를 생성(함수 누적)
  • identify - accumulator와 같으나 초기값이 있음(Stream이 비었다면 초기값 return)
  • combiner - 병렬 Stream의 각각의 Stream에서 계산한 결과를 합쳐 줌
// 1개 (accumulator) 
Optional<T> reduce(BinaryOperator<T> accumulator);

// 2개 (identity)
T reduce(T identity, BinaryOperator<T> accumulator);

// 3개 (combiner)
<U> U reduce(U identity,
  BiFunction<U, ? super T, U> accumulator,
  BinaryOperator<U> combiner);

예시

// accumulator 예시
OptionalInt reduced = IntStream.range(1, 4) // [1, 2, 3]
  	.reduce((a, b) -> {
    	return Integer.sum(a, b);
 	});
// 6 (1부터 3까지 요소들을 더해줌 - 1+2+3)


// identify 예시
int reduced = IntStream.range(1, 4) // [1, 2, 3]
 	.reduce(10, Integer::sum);
// 16 (초기값 10에 1부터 3까지 요소들을 더해줌 - 10+1+2+3)


// combiner 예시
Integer reduced = Arrays.asList(1, 2, 3)
	// 병렬 Stream
  	.parallelStream()
 	.reduce(10, Integer::sum, (a, b) -> {
    	System.out.println("combiner was called");
        return a + b;
    });
// combiner was called
// combiner was called
// 36 
// (accumulator가 초기값 10에 각 Stream 값을 더한 11, 12, 13 세 개의 값을 구하고 
//combiner가 여러 쓰레드에 나눠 계산한 결과를 합져준다 - 11+12=23, 23+13=36)

4. foreach

Stream의 요소가 foreach를 돌면서 가공되어지며, System.out.println을 통해 결과를 출력할 때 많이 사용된다

List<Integer> evenNumber = IntStream.range(1, 10).boxed()
									.filter(n -> n % 2 == 0)
									.forEach(System.out::println);
// 2 / 4 / 6 / 8
post-custom-banner

0개의 댓글