본문 바로가기

자바의 정석 정리

자바의 정석 - 14.2 스트림

14.2.1 스트림이란?

  • Array, List, Map과 같은 데이터 소스추상화하여 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 해 주는 것
  • String[] strArr = {"aaa", "ddd", "ccc"}; List<String> strLsit = Arrays.asList(strArr); Stream<String> strStream1 = strList.stream(); Stream<String> strStream2 = Arrays.stream(strArr); // 배열이든 리스트든 같은 방식으로 출력 // 메서드 참조 사용 strStream2.sorted().forEach(System.out::println);
  • 스트림은 데이터를 읽기만할 뿐, 데이터 소스를 변경하지 않음
  • 필요시에는 정렬된 결과를 컬렉션이나 배열에 담아서 반환할 수 있음
  • List<String> sortesList = strStream2.sorted().collect(Collectors.toList();
  • 스트림은 Iterator처럼 일회용
  • strStream1.sorted().forEach(System.out::println); int numOfStr = strStream1.count(); // 에러. 스트림은 위에서 닫힘
  • 스트림은 작업을 내부 반복으로 처리하기 때문에 반복문을 숨길 수 있음
  • for(String str : strList) System.out.println(str); // 위와 동일. 반복문을 숨길 수 있음 stream.forEach(System.out::println);
  • 스트림의 연산은 크게 두 가지로 분류할 수 있음
    1. 중간 연산 : 연산 결과가 스트림인 연산. 스트림에 연속해서 중간 연산할 수 있음
    2. Untitled
    3. 최종 연산 : 연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 단 한번만 가능
    4. https://user-images.githubusercontent.com/57219160/136488889-5b853ffe-34b3-4d97-b13b-c2ce18517edf.png
```java
String[] strArr = {"dd", "aaa", "CC", "cc", "b"};

Stream<String> stream           = Stream.of(strArr); // 문자열 배열이 소스인 스트림
Stream<String> filteredStream   = Stream.filter(); // 걸러내기(중간 연산)
Stream<String> distinctedStream = Stream.distinct(); // 중복제거(중간 연산)
Stream<String> sortedStream     = Stream.sort();// 정렬(중간 연산)
Stream<String> limitedStream    = Stream.limit(5); // 스트림 자르기(중간 연산)
int            total            = stream.count(); // 요소 개수 세기(최종 연산)
```

<aside>
💡 중간 연산은 map()과 flatMap()이 핵심

</aside>

<aside>
💡 최종 연산은 reduce()와 collect()가 핵심

</aside>
  • 스트림 연산에서 한 가지 중요한 점은 최종 연산이 수행되기 전 까지 중간 연산이 수행되지 않음
  • 💡 중간 연산은 어떤 작업이 중간에 이뤄져야 하는지 지정하는 것임
  • 최종 연산이 수행되어야 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모됨
  • 요소 타입이 T인 스트림은 기본적으로 Stream 이지만, 오토박싱&언박싱의 비효율을 줄이고자 기본 스트림을 다루는 IntStream, DoubleStream등이 제공됨
  • 스트림은 병렬 처리가 간단함
  • 병렬 스트림은 내부적으로 fork&join 프레임웍을 이용해 자동으로 연산을 병렬로 수행함
  • 단지 parallel() 메서드를 호출하면 병렬로 연산을 수행하도록 지시할 수 있음
  • int sum = strStream.parallel() // strStream을 병렬 스트림으로 전환 .mapToInt(s -> s.length()) .sum();
  • 병렬 처리가 되지 않게 하기 위해서는 sequential()을 호출하면 됨

14.2.2 스트림 만들기

  • 컬렉션은 최고 조상인 Collectionstream()이 정의되어 있음
  • Stream<T> Collection.stream(); List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); // 가변 인자 Stream<Integer> intStream = list.stream();
  • 배열은 StreamArraysstatic메서드로 정의되어 있음
  • Stream<T> Stream.of(T... values) // 가변 인자 Stream<T> Stream.of(T[]) Stream<T> Stream.stream(T[]) Stream<T> Stream.stream(T[] array, int startInclusive, int endExclusive) Stream<String> strStream = Stream.of("a", "b", "c"); // 가변 인자 Stream<String> strStream = Stream.of(new String[]{"a", "b", "c"}); Stream<String> strStream = Stream.stream(new String[]{"a", "b", "c"}); Stream<String> strStream = Stream.stream(new String[]{"a", "b", "c"}, 0, 3);
  • IntStreamLongStream은 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 range()rangeClosed()를 가지고 있음
  • IntStream IntStream.range(int begin, int end) IntStream IntStream.rangeClosed(int begin, int end)
  • ints(), longs(), doubles()는 스트림의 크기가 정해지지 않은 무한 스트림이기 때문에 limit()로 스트림의 개수를 정해줘야 함
  • IntStream intStream = new Random().ints(); // 무한 스트림 intStream.limit(5).forEach(System.out::println);
  • iterate()generate()는 람다식을 매개변수로 받아, 이 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성함
  • static <T> Stream<T> iterator(T seed, UnaryOperator<T> f) static <T> Stream<T> generate(Supplier<T> s) // iterator은 이전 요소를 이용해 다음을 계산함 Stream<Integer> evenStream = Stream.iterate(0, n->n+2); // 0, 2, 4, ... // generator는 이전 요소를 이용하지 않음 Stream<Integer> oneStream = Stream.geneate(()->1);
  • 파일을 스트림으로 다룰 수 있음
  • Stream<Path> Files.list(dir)
  • 빈 스트림을 생성할 수 있음
  • Stream emptyStream = Stream.empty(); // 빈 스트림을 생성해서 반환 long count = emptyStream.count(); // 개수 = 0
  • concat()으로 두 스트림을 하나로 연결할 수 있음
  • String[] str1 = {"123", "456", "789"}; String[] str2 = {"ABC", "abc", "DEF"}; Stream<String> strs1 = Stream.of(str1); Stream<String> strs2 = Stream.of(str2); Stream<String> strs3 = Stream.concat(str1, str2);

14.2.3 스트림의 중간연산

  • 스트림을 자르는 두 가지 연산이 있음
    1. Stream<T> skip(long n) : 처음 n개의 요소를 건너뜀
    2. Stream<T> limit(long maxSize) : 스트림의 요소를 maxSize로 제한함
  • 기본형 스트림에도 정의되있음
  • IntStream skip(long n) IntStream limit(long maxSize) IntStream intStream = IntStream.rangeClosed(1, 10); // 1~10의 요소를 가진 스트림 intStream.skip(3).limit(5).forEach(System.out::println) // 45678
  • 스트림 요소를 거르는 두 가지 연산이 있음
    1. Stream<T> filter(Predicate<? super T> predicate) : 조건에 맞지 않는 요소를 걸러냄
    2. Stream<T> distinct() : 중복된 요소를 제거함
    3. IntStream intStream = Intstream.of(1,2,2,3,3,4,5,5,6); intStream.distinct().forEach(System.out:println); IntStream intStream = IntStream.ramgeClosed(1, 10); intStream.filter(i->i%2==0).forEach(System.out::println); // 2,4,6,8,10
  • 정렬할 때 sorted()를 사용함
    문자열 스트림 정렬 방법 출력결과
    strStream.sorted() // 기본 정렬  
    strStream.sorted(Comparator.natualOrder()) // 기본 정렬  
    strStream.sorted((s1, s2) -> s1.compareTo(s2)); // 람다식도 가능  
    strStream.sorted(String::compareTo); // 위 문장과 동일 CCaaabccdd
    strStream.sorted(Comparator.reverseOrder()) // 기본 정렬의 역순  
    strStream.sorted(Comparator.naturalOrder().reversed()) ddccbaaaCCC
    strStream.sorted(String.CASE_INSENSITIVE_ORDER) // 대소문자 구분 안함strStream.sorted(String.CASE_INSENSITIVE_ORDER.reverse()) aaabCCccdd
    strStream.sorted(Comparator.comparing(String::length)) // 길이 순 정렬  
    strStream.sorted(Comparator.comparingInt(String::length)) // no 오토박싱 bddCCccaaa
    strStream.sorted(Comparator.comparing(String::length).reversed()) aaaddCCccb
      // Comparator의 default 메소드
      reverse()
      thenComparing(Comparator<T> other)
      thenComparing(Function<T, U> keyExtractor)
      thenComparing(Function<T, U> keyExtractor, Comparator<U> keyComp)
      thenComparingInt(ToIntFunction<T> keyExtractor)
      thenComparingLong(ToLongFunction<T> keyExtractor)
      thenComparingDouble(ToDoubleFunction<T> keyExtractor)
    
      // Comparator의 static 메소드
      naturalOrder()
      reverseOrder()
      comparing(Function<T, U> keyExractor)
      comparing(Function<T, U> keyExtractor, Comparator<U> keyComparator)
      comparingInt(ToIntFunction<T> keyExtractor)
      comparingLong(ToLongFUnction<T> keyExtractor)
      comparingDouble(ToDoubleFunction<T> keyExtractor)
      nullsFirst(Comparator<T> comparator)
      nullsLast(Comparator<T> comparator)
      import java.util.*;
      import java.util.stream.*;
    
      class StreamEx1 {
          public static void main(String[] args) {
              Stream<Student> studentStream = Stream.of(
                      new Student("이자바", 3, 300),
                      new Student("김자바", 1, 200),
                      new Student("안자바", 2, 100),
                      new Student("박자바", 2, 150),
                      new Student("소자바", 1, 200),
                      new Student("나자바", 3, 290),
                      new Student("감자바", 3, 180)
              );
    
              studentStream.sorted(Comparator.comparing(Student::getBan) // 반별 정렬
                      .thenComparing(Comparator.naturalOrder())) // 기본 정렬
                      .forEach(System.out::println);
          }
      }
    
      class Student implements Comparable<Student> {
          String name;
          int ban;
          int totalScore;
    
          Student(String name, int ban, int totalScore) {
              this.name =name;
              this.ban =ban;
              this.totalScore =totalScore;
          }
    
          public String toString() {
              return String.format("[%s, %d, %d]", name, ban, totalScore).toString();
          }
    
          String getName()     { return name;}
          int getBan()         { return ban;}
          int getTotalScore()  { return totalScore;}
    
          // 총점 내림차순을 기본 정렬로 한다.
          public int compareTo(Student s) {
              return s.totalScore - this.totalScore;
          }
      }
  • Stream<T> sorted() Stream<T> sorted(Comparator<? super T> comparator)
  • 변환 할 때는 map()을 사용함
  • // T타입을 R타입으로 변환 Stream<R> map(Function<? super T, ? extend R> mapper) Stream<File> fileStream = Stream.of(new File("Ex1.java"), new File("Ex1"), new File("Ex1.bak", new FIle"Ex2.java", new File("Ex1.txt")); Stream<String> filenameStream = fileStream.map(File::getName); filenameStream.forEach(System.out::println); // 스트림의 모든 파일 이름 출력
  • 조회할 때는 peek()를 사용함
  • fileStream.map(File::getName) // Stream<File> -> Stream<String> .filter(s -> s.indexOf('.') != -1) // 확장자가 없는 것은 제외 .peek(s->System.out.printf("filename = %s%n", s)); // 파일명을 출력한다. .map(s -> s.substring(s.indexOf('.') + 1)) // 확장자만 추출 .distinct() // 중복 제거 .forEach(System.out::println);
  • 기본형 스트림의 map()
  • DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper) IntStream mapToInt(ToIntFunction<? super T> mapper) LongStream mapToLong(ToLongFunction<? super T> mapper) // Stream Stream<Integer> studentScoreStream = studentStream.map(Student::getTotalScore); // IntStream IntStream studentScoreStream = studentStream.mapToInt(Student::getTotalScore); int allTotalScore = studentScoreSTream.sum(); // int sum(); // 기본형 스트림이 제공하는 메서드 int sum() : 스트림의 모든 요소의 총합 OptionalDouble average() : sum() / (double)count() OptionalInt max() : 스트림의 요소 중 제일 큰 값 OptionalInt min() : 스트림의 요소 중 제일 작은 값 // 기본형 -> Stream<T> Stream<U> mapToObj(IntFunction<? extends U> mapper) Stream<Integer> boxed() Instream intStream = new Random().ints(1,46); // 1~45 Stream<String> lottoStream = intStream.distinct().limit(6).soted() .mapToObj(i -> i+""); lottoStream.forEach(System.out::println);
  • Stream<T[]>를 Stream로 변환할 때는 flatMap()을 사용함
    https://velog.velcdn.com/images%2Fappti%2Fpost%2Fbd86ede7-68bc-49ce-a1b6-ed19d8f1234a%2FKakaoTalk_20210927_003324333_15.jpg
    https://velog.velcdn.com/images%2Fappti%2Fpost%2F9ff83e32-86f7-474e-b27a-0925619257b2%2FKakaoTalk_20210927_003324333_17.jpg
  • // flatMap()을 사용해 String<String[]> -> String<String> Stream<String> strStrm = strArrStrm.flatMap(Arrays::stream);
  • Stream<String[]> strArrStrm = Stream.of{ new String[] {"abc", "def", "ghi" }, new String[] {"ABC", "GHI", "JKLMN"} } // String<String[]>이 아닌 Stream<Stream<String>>임 Stream<Stream<String>> strStream = strArrStrm.map(Arrays::stream);

14.2.4 Optional와 OptionalInt

  • T타입의 객체를 감싸는 래퍼 클래스
  • 모든 타임의 참조변수를 담을 수 있음
  • public final class Optional<T>{ private final T value; }
  • Optional에정의된 메서드를 통해 반환 결과가 null인지 매번 if문으로 체크하지 않아도 됨
  • Optional객체를 생성할 때 of()ofNullable()을 사용함
  • String str = "abc"; Optional<String> optVal = Optional.of(str); Optional<String> optVal = Optional.of("abc"); Optional<String> optVal = Optional.of(new String("abc")); // 만일 참조변수의 값이 null일 가능성이 있으면 ofNullable()사용 Optional<String> optVal = Optional.of(null); // NullPointerException 발생 Optional<String> optVal = Optional.ofNullable(null); // OK
  • get(), orElse()Optional객체에 저장된 값을 가져올 수 있음
  • Optional<String> optVal = Optional.of("abc"); String str1 = optVal.get(); // 값이 null일 때는 NoSuchElementException가 발생하기 때문에 orElse()로 대체 값을 지정할 수 있음 String str2 = opVal.orElse(""); // null일 때 ""반환
  • orElse()의 변형으로 null값을 반환하는 람다식을 지정할 수 있는 orElseGet()null일 때 지정된 예외를 발생시키는 orElseThrow()가 있음
  • T orElseGet(Supplier<? extends T> other) T orElseThrow(Supplier<> exnteds X> exceptionSupplier) String str3 = optVal.orElse(String::new); // () -> new String()과 동일 STring str4 = optVal.orElseThrow(NullpointException::new) // null이면 예외 발생
  • map()의 결과가 Optional<Optional<T>>일 때, flatMap()을 사용하면 Optional<T>를 결과로 얻을 수 있음
  • 💡 만일 Optional객체의 값이 null이면 flatMap()은 아무것도 하지 않음
  • isPresent()Optional의 값이 null이면 false를, 아니면 true을 반한화게 할 수 있음
  • if(Optional.ofNullable(str).isPresent()){ System.out.println(str); } // ifPrensent()로 더 간단히 할 수 있음 Optional.ofNullable(str).ifPresent(System.out:println);
  • ifPresent()Optinal<T>를 반환하는 findAny()findFirst와 같은 최종 연산과 잘 어울림
  • Optional<T> findAny() Optional<T> findFirst() Optional<T> max(Comparator<? super T> comparator) Optional<T> min(Comparator<? super T> comparator) Optional<T> reduce(BinaryOperator<T> accumulator)
  • 기본형을 값으로 하는 OptionalInt, OptionalLong, OptionalDouble가 있음
  • OptionalInt findAny() OptionalInt findFirst() OptionalInt reduce(IntBinaryOperator op) OptionalInt max() OptionalInt min() OptionalDouble average()

14.2.5 스트림의 최종 연산

  • 최종 연산은 스트림의 요소를 소모해서 결과를 만듦
  • 스트림의 요소가 단일 값이거나, 배열 또는 컬렉션일 수 있음
  • forEach()는 반환타입이 void
  • void forEach(Cinsumer<? super T> action)
  • 지정된 조건에 모든 요수가 모두 또는 일부가 일치하는지, 어떤 요소도 일치하지 않는지에 따라 사용할 수 있는 메서드가 다름
  • // 매개변수가 predicate임 boolean allMatch(Predicate<? super T> predicate) boolean anyMatch(Predicate<? super T> predicate) boolean noneMatch(Predicate<? super T> predicate) boolean noFailed = stuStream.anyMatch(s->s.getTotalScore() <= 100) // 조건에 일치하는 첫 번째 것을 반환 Optional<Student> stu = stuStream.filter(s->s.getTotalScore()<=100).findFirst(); // 병렬일 때 findFirst대신 findAny 사용 Optional<Student> stu = parallelStream.filter(s->s.getTotalScore()<=100).findAny();
  • 기본형 스트림의 경우 count(), sum(), average(), max(), min()이 있음
  • 💡 기본형이 아닌 일반 스트림은 count(), max(), min()만 있음
  • 리듀상(Reducing)은 스트림의 요소를 줄여나거면서 연산을 수행함
  • Optional<T> reduce(BinaryOperator<T> accumulator)

14.2.6 collect()

  • 스트림 최종 연산중에서 가장 복잡하면서 유용함
  • 리듀싱과 유사함
  • collect()가 스트림 요소를 수집하려면, 어떻게 수집할 것인가에 대한 방법이 정의되어 있어야 하는데, 이 것이 컬렉터(Collector)
  • 컬렉터는 Collector인터페이스를 구현한 것임
  • Collectors클래스Collector인터페이스를 구현한 것으로 미리 작성된 다양한 종류의 컬렉터를 반한화는 static메서드를 가지고 있음
    • collect() : 스트림의 최종연산, 매개변수로 컬렉터를 필요로 함
    • Collector : 인터페이스, 컬렉터는 이 인터페이스를 구현해야 함
    • Collectors : 클래스, static 메서드로 미리 작성된 컬렉터를 제공함
    • Object collect(Collector collector) Object collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)
  • 스트림을 컬렉션과 배열로 변환할 때 대표적으로 다섯 가지가 있음
    1. toList()
    2. List<String> names = stuStream.map(Student::getName) .collect(Collectors.toList()); ArrayList<String> names = stuStream.map(Student::getName) .collect(Collectors .toCollection(ArrayList::new));
    3. toSet()
    4. toMap()
    5. Map<String, Person> map = personStream.collect(Collectors.toMap(p->p.getRegId(), p->p));
    6. toCollection()
    7. toArray()
    8. Student[] stuName = studentStream.toArray(Student[]::new) Obect[] stuName = studentStream.toArray(); // 에러 Student[] stuName = studentStream.toArray();
  • 통계 정보를 collect()로도 얻을 수 있음
  • // counting() long count = stuStream.count(); long count = stuStream.collect(counting()); // Collectors.counting() // summingInt() long totalScore = stuStream.mapToInt(Student::getTotalScore).sum(); long totalScore = stuStream.collect(summingInt(Student::getTotalScore); // maxby() OptionalInt topScore = studentStream.mapToInt(Student:getTotalScore).max(); Optional<Student> topStudent = stuStream.max(Comparator.comparingInt(Student::getTotalScore))); Optional<Student> topStudent = stuStream.collect(maxby(Comparator.comparingint(Student::getTotalScore))); // summarizingInt() IntSummaryStatistic stat = stuStream.mapToInt(Student::getTotalScore).summaryStatistics(); IntSummaryStatistic stat = stuStream.collect(summarizingInt(Student::getTotalScore));
  • 리듀싱 역시 collect()로 가능함
  • IntStream intStream = new Random().ints(1,46).distinct().limit(6); OptionalInt max = intStream.reduce(Integer::max); Optional<Integer> amx = intSTream.boxed().collect(reducing(Integer::max)); long sum = intStream.reduce(0, (a,b) -> a + b); long sum = intSTream.boxed().collect(reducing(0, (a,b) -> a + b); int grandTotal = stuSTream.map(Student::getTotalSocre).reduce(0, Integer::sum); int grandTotal = stuStream.collect(reducing(0,Student::getTotalScore,Integer::sum));
  • 문자열 결합은 joining()을 사용함
  • // 스트림 요소가 문자열이 아닌경우에는 먼저 map()을 이용해 스트림의 요소를 문자열로 변환해야 함 String studentsNames = stuStream.map(Student::getName).collect(joining()); String studentsNames = stuStream.map(Student::getName).collect(joining(",")); String studentsNames = stuStream.map(Student::getName).collect(joining(",","[","]")); // 만일 map()없이 스트림에 바로 joining()하면, 스트림의 요소에 toString()을 호출한 결과를 결합함 String studentInfo = stuStream.collect(joining(","));
  • 특정 기준으로 그룹화할 수 있음
  • Collector groupBy(Function classifier) Collector groupBy(Function classifier, Collector downstream) Collector groupBy(Function classifier, Supplier mapFactory, Collector downstream) Map<Integer, List<Student>> stuByBan = stuStream .collect(groupingBy(Student::getBan, toList())); // toList() 생략 가능 Map<Integer, HashSet<Student>> stuByBan = stuStream .collect(groupingBy(Student::getBan, toCollection(HashSet::new)));
  • 특정 기준으로 분할할 수 있음
  • Collector partitioningBy(Predicate predicate) Collector partitioningBy(Predicate predicate, Collector downstream) // 성별로 분할 Map<Boolean, List<Student>> stuBySex = stuStream.collect(partitioningBy(Student::isMale)); List<Student> maleStudent = stuBySex.get(true); List<Student> femaleStudent = stuBySex.get(false); // counting()을 이용해 수를 얻을 수 있음 Map<Boolean, Long> stuBySex = stuStream.collect(partitioningBy(Student::isMale counting())); Long maleStudent = stuBySex.get(true); // 8 Long femaleStudent = stuBySex.get(false); // 10

14.2.7 Collector 구현하기

  • Collector인터페이스를 구현하는 것을 의미함
  • public interface Collector<T, A, R> { Supplier<A> supplier(); BiConsumer<A, T> accumulator(); BinaryOperator<A> combiner(); Function<A, R> finisher(); Set<Charcteristics> characteristics(); // 컬렉터의 특성이 담긴 Set을 반환 ... }
  • 직접 구형해야하는 것은 위 다섯 가지의 메서드임
    1. supplier() : 작업 결과를 제공할 공간을 제공
    2. accumulator() : 스트림의 요소를 수집(collect)할 방법을 제공
    3. combiner() : 두 저장공간을 병합할 방법을 제공(병렬 스트림)
    4. finisher() : 결과를 최종적으로 변환할 방법을 제공
    5. charcteristics()
    6. 💡 charcteristics()를 제외하면 전부 함수형 타입이 인터페이스임. 즉, 네 개의 람다식 작성 필요

14.2.8 스트림의 변환

https://velog.velcdn.com/images%2Fnawheat%2Fpost%2Ff2fac3e5-ce25-4c19-8a2c-8dfcabf48af7%2F0101.PNGhttps://velog.velcdn.com/images%2Fnawheat%2Fpost%2F392d5f36-4811-4b65-b32e-c145fc665aa1%2F0202.PNG