JAVA8에서 추가된 스트림(Streams)은 람다를 활용할 수 있는 기술 중 하나이며,
배열, 컬렉션의 요소를 하나씩 참조해서 람다식으로 처리할 수 있는 반복자이다.
JAVA 8 이전에 배열과 컬렉션 인스턴스를 다루는 방법은
for문, 또는 forEach문을 돌면서 요소 하나씩을 꺼내서 다루는 방법이였는데,
로직이 복잡해질수록 코드의 양이 많아져 여러 로직이 섞이게 되고
메서드를 나눌 경우 루프를 여러 번 도는 경우가 발생하게 된다.
Stream은 '데이터의 흐름’.
람다를 이용해서 코드의 양을 줄이고 간결하게 표현할 수 있는데
이는 배열과 컬렉션을 함수형으로 처리할 수 있다는 뜻이며,
또 하나의 장점은 하나의 작업을 둘 이상의 작업으로 잘게 나눠서 동시에 진행하는
병렬처리(multi-threading)가 간단하게 가능하다는 점!
즉, 쓰레드를 이용해 많은 요소들을 빠르게 처리할 수 있다.
더불어,
인터페이스를 이용한 컬렉션을 다루는 방식을 아무리 표준화 했다지만,
List를 정렬할 때는 Collection.sort()
를 사용해야하고
배열을 정렬할 때는 Arrays.sort()
를 사용해야 하는 것 처럼
각 컬렉션 클래스에는 같은 기능의 메서드들이 중복해서 정의되어 있다.
이렇게 데이터 소스마다 다른 방식으로 다루어야하는 문제점을 해결해주는 것도 바로 Stream!!!!!!!! 이러한 Stream에 대해 알아보자😊
Arrays.Stream()
메서드를 사용하여 스트림을 생성.
String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);
Stream<String> stream2 = Arrays.stream(arr, 1, 3); // [b, c]
Arrays.stream(arr, a, b)
메서드 사용, 배열 arr의 index a부터 b-1까지.컬렉션 타입의 경우, 인터페이스에 추가된 디폴트 메서드 stream()
사용.
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
Stream<String> ps = list.parallelStream(); // 병렬 처리 스트림
asList()
를 사용해서 List 객체를 만들면 새로운 배열 객체를 만드는 것이 아니라, 원본 배열의 주소값을 가져오는 것.parallelStream()
메서드는 병렬 처리 스트림.빈 스트림(empty streams)은 요소가 없을 때 null대신 사용할 수 있음.
Stream emptyStream = Stream.empty();
long count = emptyStream.count(); // count의 값은 0
public Stream<String> streamOf(List<String> list) {
return list == null || list.isEmpty() ?
Stream.empty() : list.stream();
}
empty()
메서드는 빈 스트림을 생성해서 반환하며,빌더(Builder)를 사용하면 스트림에 직접적으로 원하는 값을 넣을 수 있음.
Stream<String> bst = Stream.<String>builder()
.add("111").add("222").add("333")
.build(); // [111, 222, 333]
Stream.builder()
메서드에 제네릭 타입 지정, add()
메서드로 원하는 값을 추가하고 마지막으로 build()
메서드를 사용하여 스트림 return.generate()
메서드로 Supplier<T>에 해당하는 람다로 값을 넣을 수 있으며,
인자는 없고 리턴값만 있는 함수형 인터페이스로 람다에서 리턴하는 값이 들어감.
이 때, 생성되는 스트림은 크기가 정해져있지 않고 무한(infinite)하기 때문에
limit()
메서드로 최대 크기를 제한해야 함.
Stream<String> gs = Stream.generate(() -> "gen").limit(5);
// [gen, gen, gen, gen, gen]
iterate()
메서드로 초기값과 해당 값을 다루는 람다를 이용하여 스트림에 들어갈 요소를 만듬. 이 방법 또한 크기가 무한하므로 limit()
메서드 사용.
Stream<Integer> is = Stream.iterate(30, n -> n + 2).limit(5);
// [30, 32, 34, 36, 38]
제네릭을 사용하면 리스트나 배열을 사용해서 기본 타입(int, long, double) 스트림을 생성할 수 있지만, 제네릭을 사용하지 않고 직접적으로 해당 타입의 스트림을 다룰 수 있음.
IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4]
LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5]
range()
와 rangeClosed()
는 두 번째 인자인 종료지점의 포함여부가 다름.Stream<Integer> bis = IntStream.range(1, 5).boxed();
boxed()
메서드로 박싱하면 됨.DoubleStream doubles = new Random().doubles(3); // 난수 3개 생성
IntStream cs = "Stream".chars(); // [83, 116, 114, 101, 97, 109]
Stream<String> ss = Pattern.compile(", ")
.splitAsStream("Eric, Elena, Java");
// [Eric, Elena, Java]
Stream<String> lineStream =
Files.lines(Paths.get("file.txt"),
Charset.forName("UTF-8"));
lines()
메서드는병렬처리란, 한가지 작업을 서브 작업으로 나누고,
서브 작업들을 분리된 쓰레드에서 병렬적으로 처리하는 것.
런타임시 하나의 작업을 여러개의 작업으로 나눠 처리 후, 결과 자동으로 결합.
스트림 생성 시 사용하는 stream()
메서드 대신 parallelStream()
메서드,
내부 반복자를 사용하여 병렬 스트림을 쉽게 생성할 수 있음.
내부적으로는 쓰레드 처리를 위해, JAVA7부터 도입된 Fork/Join framework
사용.
// 병렬 스트림 생성
Stream<Product> parallelStream = productList.parallelStream();
// 병렬 여부 확인
boolean isParallel = parallelStream.isParallel(); // true
외부 반복자 | 내부 반복자 |
---|---|
개발자가 코드로 직접 컬렉션의 요소를 가져오는 패턴. (index를 활용한 for문, iterator, while문 등) | 컬렉션 내부에서 요소들을 반복시키고, 개발자는 요소당 처리해야할 코드만 제공하는 패턴. |
각 작업이 쓰레드를 이용해 병렬 처리됨.
boolean isMany = parallelStream
.map(product -> product.getAmount() * 10)
.anyMatch(amount -> amount > 200);
배열을 이용해서 병렬 스트림을 생성하는 경우 parallel()
메서드 사용.
Arrays.stream(arr).parallel();
컬렉션과 배열이 아닌 경우 parallel()
메서드를 이용해서 처리.
IntStream intStream = IntStream.range(1, 150).parallel();
// 병렬 여부 확인
boolean isParallel = intStream.isParallel(); // true
다시 sequential(순차) 모드로 돌리고 싶다면 sequential()
메서드를 사용.
병렬 스트림이 무조건 좋은 것은 아님.
IntStream intStream = intStream.sequential();
// 병렬 여부 확인
boolean isParallel = intStream.isParallel(); // false
public static void main(String[] args) {
List<String> list = Arrays.asList("apple", "banana", "coconut", "grape");
// 순차처리
Stream<String> stream = list.stream();
stream.forEach(ParallelExample::print);
// 병렬처리
Stream<String> parallelStream = list.parallelStream();
parallelStream.forEach(ParallelExample::print);
}
private static void print(String s) {
System.out.println(s + " : " + Thread.currentThread().getName());
}
// 실행결과
// apple : main
// banana : main
// coconut : main
// grape : main
// coconut : main
// grape : main
// banana : ForkJoinPool.commonPool-worker-5
// apple : ForkJoinPool.commonPool-worker-19
concat()
메서드로 두 개의 스트림을 연결, 새로운 스트림 만들 수 있음.
Stream<String> stream1 = Stream.of("Java", "Scala", "Groovy");
Stream<String> stream2 = Stream.of("Python", "Go", "Swift");
Stream<String> concat = Stream.concat(stream1, stream2);
// [Java, Scala, Groovy, Python, Go, Swift]
전체 요소 중에서 다음과 같은 API를 이용해서 원하는 것만 뽑아낼 수 있음.
이러한 가공 단계를 중간 작업(intermediate operations)이라 하며,
스트림을 리턴하기 때문에 여러 작업을 이어 붙여서(chaining) 작성 가능.
필터(filter)는 스트림 내 요소들을 하나씩 평가해서 걸러내는 작업.
List<String> names = Arrays.asList("Eric", "Elena", "Java");
Stream<String> stream = names.stream()
.filter(name -> name.contains("a"));
// [Elena, Java]
stream()
메서드를 사용하여 스트림을 생성하고,filter()
메서드로 스트림의 각 요소에 대해서 평가식을 실행함.맵(map)은 스트림 내 요소들을 하나씩 특정 값으로 변환함.
이 때 값을 변환하기 위한 람다를 인자로 받음.
스트림에 들어가 있는 값이 input되어 특정 로직을 거친 후 output되고,
리턴되는 새로운 스트림에 담기는데, 이러한 작업이 바로 맵핑(mapping).
List<String> names = Arrays.asList("Eric", "Elena", "Java");
Stream<String> stream = names.stream()
.map(String::toUpperCase);
// [ERIC, ELENA, JAVA]
toUpperCase()
메서드를 실행해서 대문자로 변환한 값들이 담긴 스트림을 리턴함.Stream<Integer> stream = productList.stream()
.map(Product::getAmount);
// [23, 14, 13, 23, 13]
Stream.of(1.0, 2.0)
.mapToInt(Double::intValue)
.mapToObj(String:valueOf)
.collect(Collectors.toList());
mapToInt
메서드는 스트림을 IntStream으로 변환.mapToObj
메서드는 스트림을 Stream으로 변환.of()
메서드를 통해서 생성하고,mapToInt(Double::intValue)
는mapToObj(String.valueOf)
메서드를 이용해서flatMap은 중첩구조를 한 단계 제거하고 단일 컬렉션으로 만들어주는 역할을 하며, 이러한 작업을 플래트닝(flattening)이라고 함.
인자로 mapper를 받고 있는데, 리턴 타입이 Stream.
즉, 새로운 스트림을 생성해서 리턴하는 람다를 넘겨야 함.
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]
students.stream()
.flatMapToInt(student ->
IntStream.of(student.getKor(),
student.getEng(),
student.getMath()))
.average()
.ifPresent(avg -> System.out.println(Math.round(avg * 10) / 10.0));
정렬의 방법은 다른 정렬과 마찬가지로 Comparator를 이용하며,
인자 없이 그냥 호출할 경우 오름차순으로 정렬됨.
IntStream.of(14, 11, 20, 39, 23)
.sorted()
.boxed()
.collect(Collectors.toList());
// [11, 14, 20, 23, 39]
List<String> lang =
Arrays.asList("Java", "Scala", "Groovy", "Python", "Swift");
// 1. 순차정렬
lang.stream()
.sorted()
.collect(Collectors.toList());
// [Groovy, Java, Python, Scala, Swift]
// 2. 역순정렬
lang.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
// [Swift, Scala, Python, Java, Groovy]
// 1. 순차정렬
lang.stream()
.sorted(Comparator.comparingInt(String::length))
.collect(Collectors.toList());
// [Java, Scala, Swift, Groovy, Python]
// 2. 역순정렬
lang.stream()
.sorted((s1, s2) -> s2.length() - s1.length())
.collect(Collectors.toList());
// [Groovy, Python, Scala, Swift, Java]
스트림 내 요소들 각각을 대상으로 특정 연산을 수행하는 peek()
메서드.
‘peek’ 은 그냥 확인해본다는 단어 뜻처럼,
특정 결과를 반환하지 않는 함수형 인터페이스 Consumer를 인자로 받기에
스트림 내 요소들 각각에 특정 작업을 수행할 뿐 결과에 영향을 미치지 않음.
작업을 처리하는 중간에 결과를 확인해볼 때 사용할 수 있음.
int sum = IntStream.of(1, 3, 5, 7, 9)
.peek(System.out::println) // 1 3 5 7 9
.sum();
System.out.println(sum); // 25
가공한 스트림을 가지고 내가 사용할 결과값으로 만들어내는 단계,
스트림을 끝내는 최종 작업 (terminal operations).
스트림 API는 다양한 종료 작업을 제공함.
최소, 최대, 합, 평균 등 기본형 타입으로 결과를 만들어낼 수 있음.
long count = IntStream.of(1, 3, 5, 7, 9).count(); // 5
long sum = LongStream.of(1, 3, 5, 7, 9).sum(); // 25
Optional이라는 객체는 여러가지 에러를 잡아주기 위해 존재.
NullPointerException에 대한 문제를 해결하기 위해 나옴.
그럼에도 null이 발생할 수 있고 null 체크를 해야만 하는 경우가 빈번한데,
이를 orElse()
와 orElseGet()
메서드로 null일 시 default값을 넣어 줄 수 있음.
만약 스트림이 비어 있을 경우 count와 sum은 0을 출력하는 반면에,
평균, 최소, 최대의 경우는 표현할 수 없기 때문에 Optional을 이용해 리턴해야 함.
OptionalInt min = IntStream.of(1, 3, 5, 7).min(); // 1
OptionalDouble max = DoubleStream.of(1.5, 3.5, 5.7, 7.9).max(); // 7.9
min()
, max()
메서드를 사용할 경우 반환되는 값의 타입은 OptionalInt.
이 때, 정수가 필요하다면? getAsInt()
메서드를 사용하여 정수로 얻을 수 있음.
int min = Arrays.stream(arr).min().getAsInt();
스트림에서 바로 ifPresent()
메서드를 이용해서 Optional 을 처리할 수 있음.
DoubleStream.of(1.1, 2.2, 3.3, 4.4, 5.5)
.average()
.ifPresent(System.out::println); // 3.3
T의 모든 매개 변수를 사용, T 클래스, 값을 인수로 받음.
단순히 메서드가 아닌, null일 때 값을 넘겨야 할 때 사용.
public T orElse(T other) {
return value != null ? value : other;
}
T 유형의 개체를 반환하는 Supplier 유형의 인터페이스를 허용.
T 클래스를 상속받은 하위 클래스를 return해주는 Supplier 함수 인터페이스 인수 받으며, Supplier은 함수적 인터페이스로서 get을 호출하여 결과를 리턴하는 역할.
null일 경우에 메서드를 실행해야 할 때 사용.
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
스트림은 reduce()
메서드를 이용해서 결과를 만들어 내며,
스트림에 있는 여러 요소의 총합을 낼 수도 있음.
reduce()
메서드는 총 3가지의 파라미터를 받을 수 있음.
accumulator | identity | combiner |
---|---|---|
각 요소를 처리하는 계산 로직. 각 요소가 올 때마다 중간 결과를 생성하는 로직. | 계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴. | 병렬(parallel) 스트림에서 나눠 계산한 결과를 하나로 합치는 동작하는 로직. |
// 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);
OptionalInt reduced =
IntStream.range(1, 4) // [1, 2, 3]
.reduce((a, b) -> {return Integer.sum(a, b);}); // 6
int reducedTwoParams =
IntStream.range(1, 4) // [1, 2, 3]
.reduce(10, Integer::sum); // 16
Integer reducedParams =
Stream.of(1, 2, 3)
.reduce(10, // identity
Integer::sum, // accumulator
(a, b) -> {System.out.println("combiner was called");
return a + b;
});
Integer reducedParallel =
Arrays.asList(1, 2, 3)
.parallelStream() // 병렬 스트림
.reduce(10, // identity
Integer::sum, // accumulator
(a, b) -> {System.out.println("combiner was called");
return a + b; // 36
});
// 실행결과
// combiner was called
// combiner was called
// 36
collect 메서드는 또 다른 종료 작업.
Collector타입의 인자를 받아 처리, 자주 사용하는 작업은 Collectors 객체에서 제공.
List<Product> productList =
Arrays.asList(new Product(23, "potatoes"),
new Product(14, "orange"),
new Product(13, "lemon"),
new Product(23, "bread"),
new Product(13, "sugar"));
스트림에서 작업한 결과를 담은 리스트로 반환.
map으로 각 요소의 이름을 가져온 후,
Collectors.toList()
메서드를 사용하여 리스트로 결과를 가져옴.
List<String> collectorCollection =
productList.stream()
.map(Product::getName)
.collect(Collectors.toList());
// [potatoes, orange, lemon, bread, sugar]
스트림에서 작업한 결과를 하나의 String으로 이어 붙일 수 있음.
3개의 인자를 받아 간단하게 String을 조합할 수 있음.
delimiter | prefix | suffix |
---|---|---|
각 요소 중간에 들어가 요소를 구분시켜주는 구분자 | 결과 맨 앞에 붙는 문자 | 결과 맨 뒤에 붙는 문자 |
String listToString =
productList.stream()
.map(Product::getName)
.collect(Collectors.joining());
// potatoesorangelemonbreadsugar
String listToString =
productList.stream()
.map(Product::getName)
.collect(Collectors.joining(", ", "<", ">"));
// <potatoes, orange, lemon, bread, sugar>
숫자 값(Integer value )의 평균(arithmetic mean)을 내줌.
Double averageAmount =
productList.stream()
.collect(Collectors.averagingInt(Product::getAmount));
// 17.2
숫자값의 합(sum)을 내줌.
Integer summingAmount =
productList.stream()
.collect(Collectors.summingInt(Product::getAmount));
// 86
Integer summingAmount =
productList.stream()
.mapToInt(Product::getAmount)
.sum();
// 86
mapToint()
메서드로 더 간단하게 표현 가능.합계와 평균 모두 필요할 때 스트림을 두 번 생성하지 않고 한 번에 얻을 수 있음.
IntSummaryStatistics statistics =
productList.stream()
.collect(Collectors.summarizingInt(Product::getAmount));
IntSummaryStatistics {count=5, sum=86, min=13, average=17.200000, max=23}
averaging
,summing
,summarizing
메서드는 각 기본 타입(int, long, double) 별로 제공됨.특정 조건으로 요소들을 그룹지을 수 있음.
Map<Integer, List<Product>> collectorMapOfLists =
productList.stream()
.collect(Collectors.groupingBy(Product::getAmount));
// 실행결과
// {23=[Product{amount=23, name='potatoes'},
// Product{amount=23, name='bread'}],
// 13=[Product{amount=13, name='lemon'},
// Product{amount=13, name='sugar'}],
// 14=[Product{amount=14, name='orange'}]}
위의 groupingBy 함수형 인터페이스 Function을 이용해서 특정 값을 기준으로 스트림 내 요소들을 묶었다면, partitioningBy은 함수형 인터페이스 Predicate를 받으며, Predicate는 인자를 받아서 boolean값을 리턴함.
따라서 평가를 하는 함수를 통해서 스트림 내 요소들을 true와 false로 나눌 수 있음.
Map<Boolean, List<Product>> mapPartitioned =
productList.stream()
.collect(Collectors.partitioningBy(el -> el.getAmount() > 15));
// 실행결과
// {false=[Product{amount=14, name='orange'},
// Product{amount=13, name='lemon'},
// Product{amount=13, name='sugar'}],
// true=[Product{amount=23, name='potatoes'},
// Product{amount=23, name='bread'}]}
특정 타입으로 결과를 collect한 이후에 추가 작업이 필요한 경우에 사용할 수 있음.
finisher가 추가되었으며, collect를 한 후에 실행할 작업을 의미함.
public static<T,A,R,RR> Collector<T,A,RR> collectingAndThen(
Collector<T,A,R> downstream,
Function<R,RR> finisher) { ... }
Set<Product> unmodifiableSet =
productList.stream()
.collect(Collectors.collectingAndThen(Collectors.toSet(),
Collections::unmodifiableSet));
Collectors.toSet()
메서드를 사용해서 결과를 Set으로 collect한 후 수정불가한 Set으로 변환하는 작업을 추가 (finisher)로 실행함.이 외에 필요한 로직이 있다면 직접 collector를 만들 수도 있음.
accumulator와 combiner는 reduce() 메서드에서 살펴본 내용과 동일함.
public static<T, R> Collector<T, R, R> of(
Supplier<R> supplier, // new collector 생성
BiConsumer<R, T> accumulator, // 두 값을 가지고 계산
BinaryOperator<R> combiner, // 계산한 결과를 수집하는 함수
Characteristics... characteristics) { ... }
Collector<Product, ?, LinkedList<Product>> toLinkedList =
Collector.of(LinkedList::new,
LinkedList::add,
(first, second) -> {
first.addAll(second);
return first;
});
LinkedList<Product> linkedListOfPersons =
productList.stream()
.collect(toLinkedList);
조건식 람다를 받아 해당 조건을 만족하는 요소가 있는지 체크한 결과를 리턴.
아래와 같이 3가지 메서드가 있음.
anyMatch | allMatch | noneMatch |
---|---|---|
하나라도 조건을 만족하는 요소가 있는지 | 모두 조건을 만족하는지 | 모두 조건을 만족하지 않는지 |
List<String> names = Arrays.asList("Eric", "Elena", "Java");
boolean anyMatch = names.stream()
.anyMatch(name -> name.contains("a")); // true
boolean allMatch = names.stream()
.allMatch(name -> name.length() > 3); // true
boolean noneMatch = names.stream()
.noneMatch(name -> name.endsWith("s")); // true
foreach는 요소를 돌면서 실행되는 최종 작업.
보통 System.out.println()
메서드를 넘겨서 결과를 출력할 때 사용함.
앞서 살펴본 peek()
메서드와는 중간 작업과 최종 작업의 차이가 있음.
names.stream().forEach(System.out::println);
전체 → 맵핑 → 필터링1 → 필터링2 → 결과 만들기 → 결과물
// 기존
String[] strArr = {"aaa", "bbb", "ccc"};
List<String> strList = Arrays.asList(strArr);
// 스트림 생성
Stream<String> strStream1 = strList.stream();
Stream<String> strStream2 = Arrays.stream(strArr);
// 스트림 출력
strStream1.sorted().forEach(System.out::println);
strStream2.sorted().forEach(System.out::println);