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);
- 스트림의 연산은 크게 두 가지로 분류할 수 있음
- 중간 연산 : 연산 결과가 스트림인 연산. 스트림에 연속해서 중간 연산할 수 있음
- 최종 연산 : 연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 단 한번만 가능
```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 스트림 만들기
- 컬렉션은 최고 조상인
Collection
에stream()
이 정의되어 있음 Stream<T> Collection.stream(); List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); // 가변 인자 Stream<Integer> intStream = list.stream();
- 배열은
Stream
과Arrays
에 static메서드로 정의되어 있음 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);
IntStream
과LongStream
은 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는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 스트림의 중간연산
- 스트림을 자르는 두 가지 연산이 있음
Stream<T> skip(long n)
: 처음 n개의 요소를 건너뜀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
- 스트림 요소를 거르는 두 가지 연산이 있음
Stream<T> filter(Predicate<? super T> predicate)
: 조건에 맞지 않는 요소를 걸러냄Stream<T> distinct()
: 중복된 요소를 제거함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()
을 사용함 // 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)
- 스트림을 컬렉션과 배열로 변환할 때 대표적으로 다섯 가지가 있음
toList()
List<String> names = stuStream.map(Student::getName) .collect(Collectors.toList()); ArrayList<String> names = stuStream.map(Student::getName) .collect(Collectors .toCollection(ArrayList::new));
toSet()
toMap()
Map<String, Person> map = personStream.collect(Collectors.toMap(p->p.getRegId(), p->p));
toCollection()
toArray()
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을 반환 ... }
- 직접 구형해야하는 것은 위 다섯 가지의 메서드임
supplier()
: 작업 결과를 제공할 공간을 제공accumulator()
: 스트림의 요소를 수집(collect)할 방법을 제공combiner()
: 두 저장공간을 병합할 방법을 제공(병렬 스트림)finisher()
: 결과를 최종적으로 변환할 방법을 제공charcteristics()
- 💡 charcteristics()를 제외하면 전부 함수형 타입이 인터페이스임. 즉, 네 개의 람다식 작성 필요
14.2.8 스트림의 변환
'자바의 정석 정리' 카테고리의 다른 글
자바의 정석 - 12.3 애너테이션(annotation) (0) | 2022.09.06 |
---|---|
자바의 정석 - 12.2 열거형(Enums) (0) | 2022.09.06 |
자바의 정석 - 12.1 지네릭스(Generics) (0) | 2022.09.06 |
자바의 정석 - 11.13 Collections (0) | 2022.09.05 |
자바의 정석 - 11.12 Properties (0) | 2022.09.05 |