항해99 2주차 알고리즘 주간 때 주어진 문제를 풀면서 다른 사람들의 풀이도 살펴보니 공통적으로 몇가지 자주 사용되는 것들이 있었고 그 중 하나인 Java의 Stream에 대해 공부하고자 한다.
자바 스트림은 Java8부터 지원되기 시작한 기능으로 컬렉션에 저장되어 있는 요소들을 하나씩 돌면서 처리할 수 있는 코드 패턴이다. Stream 이전에는 for
, foe-each
문을 이용하여 배열이나 컬렉션 인스턴스의 요소를 다룰 수 있었지만 로직이 복잡해질수록 코드가 길어지거나 루프를 여러번 돌게 되는 등의 문제가 있었다.
반면 Stream은 "데이터의 흐름"이라 배열, 컬렉션 안에서 함수를 조합해 원하는 값을 가공하거나 필터링해 얻을 수 있으며, 람다를 이용해 코드를 간결하게 표현할 수 있다.
배열과 컬렉션 외에도 다양한 방법으로 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]
Collection, List, Set과 같은 컬렉션 타입의 경우 stream()
메소드를 이용해 Stream 객체를 만들 수 있다.
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
배열/컬렉션을 이용해서가 아니라 빌더(builder)를 통해 Stream에 직접 원하는 값을 넣을 수 있다. add
로 값을 추가하고 build()
메소드로 Stream을 반환할 수 있다.
Stream<String> builderStream = Stream.<String>builder()
.add("Eric")
.add("Elena")
.add("Java")
.build(); // [Eric, Elena, Java]
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이 생성된다.
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.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) 할 수도 있다.
필터(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된다.
맵(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, 가, 나, 다]
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]
람다식 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)을 해야 한다.
collect
메소드는 Collector
타입의 인자를 받아 처리하며, 이는 Collectors
객체에서 제공된다.
아래와 같은 예제가 있다고 하자.
List<Product> productList =
Arrays.asList(new Product(23, "potatoes"),
new Product(14, "orange"),
new Product(13, "lemon"),
Stream에서 작업한 결과를 리스트로 반환해준다.
List<String> list =
productList.stream()
.map(Product::getName)
.collect(Collectors.toList());
// [potatoes, orange, lemon]
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)는 제일 마지막에 붙는 문자이다.
++
이외에도 아래와 같은 메소드들이 있다.
collector
커스텀하기sum
과 count
는 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();
reduce
메소드에는 3 종류의 파라미터가 존재한다.
// 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)
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