Java 8 Streams
- Streams
Streams
스트림은 ‘데이터의 흐름’입니다. 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있습니다. 또한 람다를 이용해서 코드의 양을 줄이고 간결하게 표현할 수 있습니다. 즉, 배열과 컬렉션을 함수형으로 처리할 수 있습니다.
스트림에 대한 내용은 크게 세 가지로 나눌 수 있습니다.
- 생성하기 : 스트림 인스턴스 생성.
- 가공하기 : 필터링(filtering) 및 맵핑(mapping) 등 원하는 결과를 만들어가는 중간 작업(intermediate operations).
결과 만들기 : 최종적으로 결과를 만들어내는 작업(terminal operations).
전체 -> 맵핑 -> 필터링 1 -> 필터링 2 -> 결과 만들기 -> 결과물
생성하기
배열 / 컬렉션 / 빈 스트림
배열
// Arrays.stream() 메소드를 이용해 생성 가능 String[] arr =new String[]{"a", "b", "c"}; Stream<String> stream = Arrays.stream(arr); Stream<String> streamOfArrayPart = Arrays.stream(arr, 1, 3);// 1~2 요소 [b, c]
컬렉션
// 컬렉션 타입의 추가된 메소드 stream() 으로 생성 가능 publicinterfaceCollection<E>extendsIterable<E> { default Stream<E>stream() { return StreamSupport.stream(spliterator(),false); } // ... } // 아래 방법으로 생성 가능 List<String> list = Arrays.asList("a", "b", "c"); Stream<String> stream = list.stream(); Stream<String> parallelStream = list.parallelStream(); // 병렬 처리 스트림
빈 스트림
// 요소가 없을 때, 빈 스트림을 생성하여 사용한다. public Stream<String> streamOf(List<String> list) { return list == null || list.isEmpty() ? Stream.empty() : list.stream(); }
Stream.builder() / Stream.generate() / Stream.iterate()
bulder() 사용
// 빌더를 사용해 스트림에 직접 원하는 값 넣기. Stream<String> builderStream = Stream.<String>builder() .add("1") .add("2") .add("3") .build(); // [1, 2, 3]
generate()
// Supplier<T> 에 해당하는 람다로 값을 넣는다. Supplier<T> 는 인자는 없고 리턴값만 있는 함수형 인터페이스로, 람다에서 리턴하는 값이 들어간다. public static<T> Stream<T> generate(Supplier<T> s) { ... } // 단 이렇게 생성하게 되면, 길이의 제한이 없기 때문에 길이를 제한해 주어야 한다. Stream<String> generatedStream = Stream.generate(() -> "1").limit(5); // [1, 1, 1, 1, 1]
iterate()
// iterate() 를 이용하면, 초기 값과 해당 값을 다루는 람다를 이용해 스트림에 들어갈 요소를 만든다. // 마찬가지로, 길이의 제한이 없어 제한을 두어야 한다. Stream<Integer> iteratedStream = Stream.iterate(301 n -> n + 2).limit(5); // [1, 3, 5, 7, 9]
기본 타입형 / String / 파일 스트림
기본 타입형
제네릭을 사용하지 않고, 직접적으로 해당 타입의 스트림을 다룬다.
// range() 메소드는, 종료 지점을 포함하냐 안하느냐 차이를 가지는 범위 지정 메소드
IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4]
LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5]
// 제네릭을 사용하지 않기 때문에 오토박싱이 일어나지 않으나, box 메소드를 이용해 박싱 할 수 있다.
Stream<Integer> boxedIntStream = IntStream.range(1, 5).boxed();
// 자바8에서는 랜덤 클래스의 난수 생성 메소드를 이용해, 세가지 타입의 스트림(int, long, double) 을 만들 수 있다.
DoubleStream doubles = new Random().doubles(5); // double 난수 5개 생성
String
String의 각 문자를 스트림화 할 수 있다.
// char는 본질적으로 숫자이기 때문에 IntStream 으로 변환이 가능하다.
IntStream charsStream = "Stream".chars(); // [83, 116, 114, 101, 97, 109]
// 정규표현식으로 문자를 split 하고, 이를 스트림화 할 수 있다.
Stream<String> stringStream = Pattern.compile(", ").splitAsStream("1, 2, 3"); // [1, 2, 3]
File Stream
파일의 각 라인을 스트림화 할 수 있다.
// 텍스트 파일의 각 라인을 읽어오는 Files의 lines() 메소드를 사용함.
Stream<String> lineStream = Files.lines(Paths.get("file.txt"), Charset.forName("UTF-8"));
병렬 스트림 / 스트림 연결하기
병렬 스트림
스트림 생성 시 사용하는 stream
대신 parallelStream
메소드를 사용해서 병렬 스트림을 쉽게 생성할 수 있습니다. 내부적으로는 쓰레드를 처리하기 위해 자바 7부터 도입된 Fork/Join framework을 사용합니다.
// 병렬 스트림 생성
Stream<Product> parallelStream = productList.parallelStream();
// 병렬 여부 확인
boolean isParallel = parallelStream.isParallel();
// 아래 코드는, 쓰레드를 이용해 병렬 처리된다.
boolean isMany = parallelStream
.map(product -> product.getAmount() * 10)
.anyMatch(amount -> amount > 200);
// 배열을 이용하여 생성하는 경우.
Arrays.stream(arr).parallel();
// 컬렉션과 배열이 아닌 경우, 아래와 같이 parallel()을 이용하여 처리.
IntStream intStream = IntStream.range(1, 150).parallel();
boolean isParallel = intStream.isParallel();
// 다시 시퀀셜 모드로 돌리고 싶은 경우, squential() 메소드를 사용한다.
IntStream intStream = intStream.sequential();
boolean isParallel = intStream.isParallel();
스트림 연결하기
Stream.concat
메소드를 이용해 두 개의 스트림을 연결해서 새로운 스트림을 만들어낼 수 있습니다.
// 두 개의 스트림을 합쳐 새로운 스트림 생성.
Stream<String> stream1 = Stream.of("1", "2", "3");
Stream<String> stream2 = Stream.of("4", "5, "6");
Stream<String> concat = Stream.concat(stream1, stream2);
// [1, 2, 3, 4, 5, 6]
가공하기
filter
filter() → 스트림 내 요소를 필터링 함.
// 스트림 내 요소를 평가하여 걸러내는 작업을 하는 필터링. Stream<T> filter(Predicate<? super T> predicate); // name = [apple, banana, kiwi] Stream<String> stream = names.stream() .filter(name -> name.contains("a")); // [apple, banana]
map
map() → 스트림 내 요소를 하나씩 특정 값으로 변환.
// 스트림 내 요소를 특정 값으로 변환하는 맵핑, 변환하여 새로운 스트림으로 리턴된다. <R> Stream<R> map(Function<? super T, ? extends R> mapper); // String::toUpperCase는 대문자로 변환하는 스트링 내 메소드를 표현하는 람다식 Stream<String> stream = names.stream() .map(String::toUpperCase); // [APPLE, BANANA, KIWI] // 임의의 person 객체의 getAge() 를 통해, 나이를 가져오는 예제 Stream<Integer> stream = person.stream() .map(Person::getAge); // [10, 20, 30, 40, 50]
flatMap
flatMap() → 중첩 구조를 제거하고 단일 컬렉션으로 변환.
// Map 보다 조금 복잡한 flatMap 이 있다. 이 메소드는 mapper를 인자로 받아 새로운 스트림을 생성해 리턴한다. // 중첩 구조를 한단계 제거하고, 단일 컬렉션으로 만들어주는 역할을 한다. <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper); // 예제 1 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] // 예제 2 // 학생 객체에서 각 과목 점수를 추출하여 평균을 구하는 예제 students.stream() .flatMapToInt(student -> IntStream.of(student.getKor(), student.getEng(), student.getMath())) .average().ifPresent(avg -> System.out.println(Math.round(avg * 10)/10.0));
sorted
sorted() → 스트림 내 요소 정렬
// comparator를 이용하여 정렬 한다. Stream<T> sorted(); Stream<T> sorted(Comparator<? super T> 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", "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] // Comparator 의 compare 메소드는 두 인자를 비교해서 값을 리턴합니다. int compare(T o1, T o2); // 해당 메소드를 이용해 문자열 길이를 기준으로 정렬하는 예제 lang.stream() .sorted(Comparator.comparingInt(String::length)) .collect(Collectors.toList()); // [Go, Java, Scala, Swift, Groovy, Python] lang.stream() .sorted((s1, s2) -> s2.length() - s1.length()) .collect(Collectors.toList()); // [Groovy, Python, Scala, Swift, Java, Go]
peek
peek() → 스트림 내 요소를 반복 확인, 특정 결과를 리턴하지 않음.
// 스트림 내 요소를 확인 하는 peek(), 결과를 반환하지 않음. Stream<T> peek(Consumer<? super T> action); // 아래 처럼 중간에 값을 확인하는 등의 용도로 사용할 수 있다. int sum = IntStream.of(1, 3, 5, 7, 9) .peek(System.out::println) .sum();
결과 만들기
Calculating
count()
long count = IntStream.of(1, 3, 5, 7, 9).count();
sum()
long sum = LongStream.of(1, 3, 5, 7, 9).sum();
min()
OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min();
max()
OptionalInt max = IntStream.of(1, 3, 5, 7, 9).max();
ifPresent() → Optional 처리를 하기 위해 사용
DoubleStream.of(1.1, 2.2, 3.3, 4.4, 5.5) .average() .ifPresent(System.out::println); // 평균 값 3.3 리턴.
Reduction
- reduce() → 결과를 만들어 내기 위해 사용하는 메소드
- 파라미터
- accumulator : 각 요소를 처리하는 계산 로직. 각 요소가 올 때마다 중간 결과를 생성하는 로직.
- identity : 계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴.
- combiner : 병렬(parallel) 스트림에서 나눠 계산한 결과를 하나로 합치는 동작하는 로직.
// 1개 (accumulator) Optional<T> reduce(BinaryOperator<T> accumulator); // BinaryOperator<T> 는 같은 타입의 인자 2개를 받아, 같은 타입의 결과를 리턴하는 람다형 함수. OptionalInt reduced = IntStream.range(1, 4) // [1, 2, 3] .reduce((a, b) -> { return Integer.sum(a, b); // 6(1+2+3) }); /*----------------------------------------------------------------------------------------------------*/ // 2개 (identity) T reduce(T identity, BinaryOperator<T> accumulator); // 메소드 참조를 통해 2개의 인수를 넘김. int reducedTwoParams = IntStream.range(1, 4) // [1, 2, 3] .reduce(10, Integer::sum); // 초기값 10 + (1+2+3) = 16 리턴. /*----------------------------------------------------------------------------------------------------*/ // 3개 (combiner) <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner); // 3개를 넘기는 경우, 그러나 마지막 combiner는 실행되지 않는다. Integer reducedParams = Stream.of(1, 2, 3) .reduce(10, // identity Integer::sum, // accumulator (a, b) -> { System.out.println("combiner was called"); return a + b; }); // ? // 마지막 Combiner는 병렬 처리 시, 각자 다른 쓰레드에서 실행된 결과를 합치는 단계이기 때문에 실행하기 위해서는 병렬 스트림에서만 동작한다. Integer reducedParallel = Arrays.asList(1, 2, 3) .parallelStream() .reduce(10, Integer::sum, // (10+1)+(10+2)+(10+3) -> 11, 12, 13 (a, b) -> { // 11 + 12 , 12 + 13 System.out.println("combiner was called"); return a + b; }); // (11+12) + 13 = 36 // 병렬 스트림이 무조건 시퀀셜보다 좋은 것은 아닙니다. 오히려 간단한 경우에는 이렇게 부가적인 처리가 필요하기 때문에 오히려 느릴 수도 있습니다.
- 파라미터
Collecting
collect() →
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"));
Collectors.toList() → 스트림에서 작업한 결과를 리스트로 반환.
// 맵으로 이름을 가져온 후, 이를 리스트로 반환. List<String> collectorCollection = productList.stream() .map(Product::getName) .collect(Collectors.toList()); // [potatoes, orange, lemon, bread, sugar]
Collectors.joining() → 스트림에서 작업한 결과를 하나의 스트링으로 리턴.
Collectors.joining
은 세 개의 인자를 받을 수 있습니다. 이를 이용하면 간단하게 스트링을 조합할 수 있습니다.- delimiter : 각 요소 중간에 들어가 요소를 구분시켜주는 구분자
- prefix : 결과 맨 앞에 붙는 문자
- suffix : 결과 맨 뒤에 붙는 문자
// 맵으로 이름을 가져온 후, join() 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>
Collectors.averageingInt() → 숫자 값(Integer value)의 평균(arithmetic mean)을 냅니다.
Double averageAmount = productList.stream() .collect(Collectors.averagingInt(Product::getAmount)); // 17.2
Collectors.summingInt() → 숫자값의 합(sum)을 냅니다.
Integer summingAmount = productList.stream() .collect(Collectors.summingInt(Product::getAmount)); // 86 // IntStream 으로 바꿔주는 mapToInt 메소드를 사용해서 좀 더 간단하게 표현할 수 있습니다. Integer summingAmount = productList.stream() .mapToInt(Product::getAmount) .sum(); // 86
Collectors.summarizingInt()
만약 합계와 평균 모두 필요하다면 스트림을 두 번 생성해야 할까요? 이런 정보를 한번에 얻을 수 있는 방법으로는
summarizingInt
메소드가 있습니다.IntSummaryStatistics statistics = productList.stream() .collect(Collectors.summarizingInt(Product::getAmount));
이렇게 받아온 IntSummaryStatistics 객체에는 다음과 같은 정보가 담겨 있습니다.
IntSummaryStatistics {count=5, sum=86, min=13, average=17.200000, max=23}
- 개수 getCount()
- 합계 getSum()
- 평균 getAverage()
- 최소 getMin()
- 최대 getMax()
이를 이용하면
collect
전에 이런 통계 작업을 위한map
을 호출할 필요가 없게 됩니다. 위에서 살펴본 averaging, summing, summarizing 메소드는 각 기본 타입(int, long, double)별로 제공됩니다.Collectors.groupingBy() → 특정 요소로 그룹지을 수 있음.
// 수량 가져와서 그룹핑 하기. 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'}]}
Collectors.partitioningBy() ****위의
groupingBy
함수형 인터페이스 Function 을 이용해서 특정 값을 기준으로 스트림 내 요소들을 묶었다면,partitioningBy
은 함수형 인터페이스 Predicate 를 받습니다. Predicate 는 인자를 받아서 boolean 값을 리턴합니다.// 함수형 인터페이스로 조건에 맞는 요소들을 그룹핑하여 반환 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'}]}
Collectors.collectingAndThen()
특정 타입으로 결과를
collect
한 이후에 추가 작업이 필요한 경우에 사용할 수 있습니다. 이 메소드의 시그니쳐는 다음과 같습니다.finisher
가 추가된 모양인데, 이 피니셔는 collect 를 한 후에 실행할 작업을 의미합니다.**public** **static**<T,A,R,RR> Collector<T,A,RR> **collectingAndThen**( Collector<T,A,R> downstream, Function<R,RR> finisher) { ... }
다음 예제는
Collectors.toSet
을 이용해서 결과를 Set 으로 collect 한 후 수정불가한 Set 으로 변환하는 작업을 추가로 실행하는 코드입니다.Set<Product> unmodifiableSet = productList.stream() .collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet));
Collector.of()
이 외에 필요한 로직이 있다면 직접 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) { ... } // 스트림의 각 요소에 대해서 LinkedList 를 만들고 요소를 추가하게 됩니다. 마지막으로 combiner 를 이용해 결과를 조합하는데, 생성된 리스트들을 하나의 리스트로 합치는 컬렉터 메소드 예시 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);
Matching
매칭은 조건식 람다 Predicate 를 받아서 해당 조건을 만족하는 요소가 있는지 체크한 결과를 리턴합니다. 다음과 같은 세 가지 메소드가 있습니다.
- 하나라도 조건을 만족하는 요소가 있는지(anyMatch)
- 모두 조건을 만족하는지(allMatch)
- 모두 조건을 만족하지 않는지(noneMatch)
boolean anyMatch(Predicate<? super T> predicate);
boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);
// 예시
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
Iterating
foreach
foreach
는 요소를 돌면서 실행되는 최종 작업입니다. 보통System.out.println
메소드를 넘겨서 결과를 출력할 때 사용하곤 합니다.앞서 살펴본
peek
과는 중간 작업과 최종 작업의 차이가 있습니다.names.stream().forEach(System.out::println);
동작 순서
- 모든 요소가 첫 번째 중간 연산을 수행하고 남은 결과가 다음 연산으로 넘어가는 것이 아니라, 한 요소가 모든 파이프라인을 거쳐서 결과를 만들어내고, 다음 요소로 넘어가는 순서.
성능 향상
- 요소의 범위를 줄이는 작업을 먼저 실행하는 것이 불필요한 연산을 막을 수 있어 성능을 향상시킬 수 있습니다. 이런 메소드로는
skip
,filter
,distinct
등이 있습니다.
스트림 재사용
- 종료 작업을 하지 않는 한 하나의 인스턴스로서 계속해서 사용이 가능합니다. 하지만 종료 작업을 하는 순간 스트림이 닫히기 때문에 재사용은 할 수 없습니다.
- 스트림은 저장된 데이터를 꺼내서 처리하는 용도이지 데이터를 저장 하려는 목적으로 설계되지 않았기 때문입니다.
지연 처리 Lazy Invocation
- 스트림에서 최종 결과는 최종 작업이 이루어질 때 계산됩니다.
- 최종작업을 실행하지 않으면 실제로 스트림의 연산이 실행되지 않는다.
Null-safe 스트림 생성하기
NullPointerException 은 개발 시 흔히 발생하는 예외입니다. Optional 을 이용해서 null에 안전한(Null-safe) 스트림을 생성해보겠습니다.
// 널-세이프 한 스트림을 만드는 메소드 public <T> Stream<T> collectionToStream(Collection<T> collection) { return Optional .ofNullable(collection) .map(Collection::stream) .orElseGet(Stream::empty); } // 테스트 List<Integer> intList = Arrays.asList(1, 2, 3); List<String> strList = Arrays.asList("a", "b", "c"); Stream<Integer> intStream = collectionToStream(intList); // [1, 2, 3] Stream<String> strStream = collectionToStream(strList); // [a, b, c] // 널 테스트 -> 예외가 발생하지 않고 빈 스트림으로 종료됨. List<String> nullList = null; nullList.stream() .filter(str -> str.contains("a")) .map(String::length) .forEach(System.out::println); // NPE!
줄여쓰기 Simplified
스트림 사용 시 다음과 같은 경우에 같은 내용을 좀 더 간결하게 줄여쓸 수 있습니다. IntelliJ 를 사용하면 다음과 같은 경우에 줄여쓸 것을 제안해줍니다. 그 중에서 많이 사용되는 것만 추렸습니다.
collection.stream().forEach() → collection.forEach() collection.stream().toArray() → collection.toArray() Arrays.asList().stream() → Arrays.stream() or Stream.of() Collections.emptyList().stream() → Stream.empty() stream.filter().findFirst().isPresent() → stream.anyMatch() stream.collect(counting()) → stream.count() stream.collect(maxBy()) → stream.max() stream.collect(mapping()) → stream.map().collect() stream.collect(reducing()) → stream.reduce() stream.collect(summingInt()) → stream.mapToInt().sum() stream.map(x -> {...; return x;}) → stream.peek(x -> ...) !stream.anyMatch() → stream.noneMatch() !stream.anyMatch(x -> !(...)) → stream.allMatch() stream.map().anyMatch(Boolean::booleanValue) → stream.anyMatch() IntStream.range(expr1, expr2).mapToObj(x -> array[x]) → Arrays.stream(array, expr1, expr2) Collection.nCopies(count, ...) → Stream.generate().limit(count) stream.sorted(comparator).findFirst() → Stream.min(comparator)
특정 케이스에서 다르게 동작하는 경우.
// 1. 생략 가능 collection.stream().forEach() → collection.forEach() // 2. 동기화에서는 차이 발생 // not synchronized Collections.synchronizedList(...).stream().forEach() // synchronized Collections.synchronizedList(...).forEach() // 바로 호출 가능한 경우 stream.collect(maxBy()) → stream.max() // 스트림이 비어있는 경우에는 또 다르게 동작함. collect(Collectors.maxBy()) // Optional Stream.max() // NPE 발생 가능
참고
- https://futurecreator.github.io/2018/08/26/java-8-streams/
- https://futurecreator.github.io/2018/08/26/java-8-streams-advanced/