Stream(스트림)연산
컬렉션을 처리할때 보통은 요소들을 첨부터 끝까지 순회하면서 각 요소를 대상으로 작업한다. 하지만, Java8 부터는 스트림연산을 통해 반복하지 않아도 된다.
List<String> word;
int count = 0;
for(String w : words){ // 일반 방식
if(w.length() > 12) {
count++;
}
}
long count = words.stream().filter(w -> w.length() > 12).count(); // 스트림
스트림은 데이터를 변환하고 추출할수있어서 겉으로는 컬렉션과 유사해보이지만 큰 차이점이 있다.
- 스트림은 요소들을 보관하지않는다. 요소들은 하부의 컬렉션에 보관되거나 필요할때 생성된다.
- 스트림 연산은 원본을 변경하지않는다. 대신 결과를 담은 새로운 스트림을 반환한다.
- 스트림 연산은 가능하면 지연처리된다.
스트림 연산의 파이프라인은 세단계로 구분된다.
- 스트림생성
- 초기스트림을 다른스트림으로 변환하는 중간 연산
- 결과를 산출하기 위한 최종연산. 여기서 지연연산의 실행을 강제. 이후로는 해당스트림을 사용할수없다.
스트림생성
List<String> list = new ArrayList<>();
Stream<String> s0 = list.stream(); // 컬렉션을 스트림으로 만드는방법
Stream<String> s1 = list.parallelStream(); // 컬렉션을 스트림으로 만드는방법(병렬)
String[] array = { "a", "b", "c" };
Stream<String> s2 = Stream.of(array); // 배열을 스트림으로 만드는방법
Stream<String> s3 = Arrays.stream(array); // 배열을 스트림으로 만드는방법
Stream<String> s4 = Stream.empty(); // 비어있는 스트림 생성
Stream<Integer> s5 = Stream.generate(() -> 12); // 상수를 스트림으로 만드는방법, 무한스트림도 가능
Stream<Integer> s6 = Stream.iterate(1.0, p -> p * 2); // 무한스트림으로 만드는방법
스트림 중간연산
filter, map, flatMap
- filter : 스트림에 필터를 입혀서 조건에 해당하는 요소들만 포함하는 스트림을 반환한다. Predicate<T>를 인자로 받으므로 T를 받고 boolean을 리턴하는 함수를 만들어서 파라미터에 넣어주면된다.
- map : 스트림에 있는 값들을 특정 방식으로 변환한 스트림을 반환한다. 변환을 수행할 함수를 파라미터로 전달하면된다.
- Stream<String> = stream.map(String::toLowerCase); // 모든단어 소문자로 변경 Stream<String> = stream.map(s -> s.charAt(0)); // 각 단어의 첫번째 문자로 변경
- flatMap : map처럼 변환하는데, 결과물들을 하나로 합침
- Stream<Stream<Character>> result = words.map(w -> characterStream(w)); // [ ..['a','b','c'], ['d','e'] ..] map을 쓰면 이렇게 나오지만 Stream<Character> result = words.flatMap(w -> characterStream(w)); // [ ..,'a','b','c','d','e', .. ] 이런식으로 펼침
limit, skip, concat, peek
- limit : n개 요소 이후(원본이 n보다 짧을경우는 원본스트림이 끝날때) 끝나는 새로운 스트럼을 리턴
- Stream<Double> randoms = Stream.generate(Math::random).limit(100); // 무한 스트림을 필요한크기(100개)로 돌려줌
- skip : 처음 n개 요소를 버림, skip(1) 이면 처음 1개스트림을 버리고 스트림을 리턴
- concat : 두 스트림을 연결
- peek : 원본과 동일한 요소들을 포함하는 다른 스트림을 돌려주지만, 전달받은 함수를 요소 추출시마다 호출함. 디버깅시 유용
- Object[] powers = Stream.iterate(1.0, p -> p*2) .peek(e -> System.out.println("Fetching" + e)) .limit(20).toArray();
distinct, sorted
지금까지의 스트림변환은 무상태 변환이다. 스트림에서 요소를 추출할때 결과가 이전 요소에 의존하지않았지만, 아래의 스트림변환은 이전 요소의 상태를 기억하고있어야한다.
- distinct : 스트림요소간의 중복을 제거하여, 같은순서로 스트림을 리턴한다 (순서를 유지해야하기때문에 이전요소를 기억하고있어야함)
- sorted : 스트림요소들을 정렬한 후, 스트림을 리턴(당연히 무한스트림은 정렬불가)
- Stream<String> longestFirst = words.**sorted(Comparator.comparing(String::length)**.reversed());
스트림 최종연산
단순리덕션(count, max, min, findFirst, findAny, anyMatch, allMatch, nonMatch)
- count : 개수를 반환
- max, min : 최댓값&최솟값을 반환. 비어있는 스트림일수도있기때문에 Optional<T>값을 리턴
- findFirst : 비어있지않은 첫번째 값을 리턴. Optional 리턴
- findAny : 어떤 결과라도 괜찮으니 처음 일치가 발견되면 리턴. 병렬처리에 유용. Optional 리턴
- anyMatch : 하나라도 일치하는 요소가 있는지 알고싶을때 사용. boolean 리턴. predicate**(boolean을 리턴하는 함수)**인자를 받으므로 filter사용할필요 없음
- allMatch : 모든 요소가 predicate와 일치하면 true리턴
- nonMatch : 모든 요소가 predicate와 일치하지 않으면 true리턴
Optional<String> s1 = list.stream().max(String::compareToIgnoreCase); // 최댓값 리턴
Optional<String> s2 = list.stream().min(String::compareToIgnoreCase); // 최솟값 리턴
Optional<String> s3 = list.stream().filter(s -> s.startsWith("a")).findFirst(); // a로 시작하는 요소들중 첫번째로 일치하는 요소 리턴
Optional<String> s4 = list.stream().parallel().filter(s -> s.startsWith("a")).findAny();
boolean s5 = list.stream().parallel().anyMatch(s -> s.startsWith("a")); // 하나라도 predicate와 일치하는 요소가 있으면 true
boolean s6 = list.stream().parallel().allMatch(s -> s.startsWith("a")); // 모든 요소가 predicate와 일치하면 true
boolean s7 = list.stream().parallel().noneMatch(s -> s.startsWith("a")); // 모든 요소가 predicate와 일치하지않으면 true
리덕션 연산(reduce)
간단하게 합계를 계산하거나 스트림의 요소들을 다른방법으로 결합할 수 있다. 하지만, 보통 숫자 스트림에 맵핑하는게 더 쉽기때문에 실전에서는 거의 쓰이지 않는다.
- reduce
- Stream<Integer> val = ...; Optional<Integer> sum = values.reduce((x,y) -> x+y); Optional<Integer> sum = values.reduce(Integer::sum); Optional<Integer> sum = values.reduce(0, (x,y) -> x+y); // 0 + x + y / 0부터 시작하여 계산한다는 의미
결과값 모으기(iterator, toArray, collect, forEach, forEachOrdered)
스트림 작업을 마칠때 보통은 값으로 리듀스하기보다는 결과를 살펴보고 싶을때가 많다.
- iterator : Iterator 객체를 리턴해준다. 알아서 반복자 돌려서 봐라..
- toArray : Object[] 배열을 리턴해준다. 원하는 타입의 배열을 원하는 경우 배열생성자에 전달한다. ex) toArray(Integer[]::new)
- Iterator<String> i = list.stream().iterator(); Object[] o1 = list.stream().toArray(); // 이런식이면 Object배열밖에 안됨. String[] o2 = list.stream().toArray(String[]::new); // 원하는 배열을 만들고싶을때 생성자에 전달
- collect : 컬렉션(굳이 아니여도됨. StringBuilder나 카운트와 합계를 관리하는 객체면 ok)에 결과들을 모으려고할때, reduce대신(쓰레드 등의 이유) collect사용
List<String> l1 = list.stream().collect(Collectors.toList()); // 스트림을 List로 모으고 싶을때 사용 ArrayList<String> l2 = list.stream().collect(Collectors.toCollection(ArrayList::new)); // 스트림을 리스트로 모으고 싶을때 사용, 이런식으로도 가능 Set<String> l3 = list.stream().collect(Collectors.toSet()); // 스트림을 Set로 모으고 싶을때 사용 TreeSet<String> l4 = list.stream().collect(Collectors.toCollection(TreeSet::new)); // 스트림을 TreeSet로 모으고 싶을때 사용 String result = list.stream().collect(Collectors.joining()); // 스트림에 있는 모든 문자열 연결 String result = list.stream().collect(Collectors.joining(", ")); // 요소간에 구분자를 넣어서 연결 // 스트림이 문자열 외의 객체를 포함하는 경우, 다음처럼 해당객체를 먼저 문자열로 바꿔줘야함 String result = list.stream().map(Object::toString).collect(Collectors.joining(", ")); // 요소간에 구분자를 넣어서 연결 // 스트림의 결과를 합계, 평균, 최대최솟값으로 리듀스 하려는 경우 IntSummaryStatistics summary = list.stream().peek(System.out::println).collect(Collectors.summarizingInt(String::length)); double avg = summary.getAverage(); double max = summary.getMax(); double sum = summary.getSum();
- collect는 공급자, 누산자, 결합자 의 3가지 인자를 받지만, 실제로 사용할때는 Collectors클래스를 가져다쓰면 되므로 간편
- forEach : 전달하려는 함수가 각요소에 전달. 단 병렬실행이므로 순서가 임의임. 최종연산이므로 호출이후에는 스트림이 사라지므로 단순히 값을보고싶으면 peek사용
- forEachOrdered : 전달하려는 함수가 각요소에 전달. 순서를 지키면서 순회하지만 병렬이 주는 이점을 잃음. 최종연산
Optional<String> s1 = list.stream().max(String::compareToIgnoreCase); // 최댓값 리턴
Optional<String> s2 = list.stream().min(String::compareToIgnoreCase); // 최솟값 리턴
Optional<String> s3 = list.stream().filter(s -> s.startsWith("a")).findFirst(); // a로 시작하는 요소들중 첫번째로 일치하는 요소 리턴
Optional<String> s4 = list.stream().parallel().filter(s -> s.startsWith("a")).findAny();
boolean s5 = list.stream().parallel().anyMatch(s -> s.startsWith("a")); // 하나라도 predicate와 일치하는 요소가 있으면 true
boolean s6 = list.stream().parallel().allMatch(s -> s.startsWith("a")); // 모든 요소가 predicate와 일치하면 true
boolean s7 = list.stream().parallel().noneMatch(s -> s.startsWith("a")); // 모든 요소가 predicate와 일치하지않으면 true
맵으로 모으기
위의 collection을 이용해 list, set뿐만아니라 map으로 스트림을 모을수있다. 각 인자별로 '키', '값', '키중복시 처리방법' 을 나타낸다.
Map<Integer, String> ab = list.stream().collect(Collectors.toMap(Person::getId, Person::getName); // 중복 처리를 안해줬으므로 키 중복시 IllegalStateException 예외 발생
Map<Integer, String> ab = list.stream().collect(Collectors.toMap(Person::getId, Person::getName, (existVal, newVal) -> newVal); // 키 중복일어났을때 새로운값으로 대체
// 키 중복일때 값을 set안에 계속 추가. 각 맵별로 싱글톤 생성 <- 이렇게 직접 쓸일은 없음..
Map<Integer, Set<String>> a3 =
list2.stream().collect(
Collectors.toMap(Person::getId, l -> Collections.singleton(l.getName()), (old, newVal) -> {
Set<String> s = new HashSet<>(old);
s.addAll(newVal);
return s;
}));
그룹핑과 파티셔닝
위처럼 각 맵값에대해 싱글톤 집합을 생성하고, 기존값과 새값을 병합하는 방법을 명시하는 방법은 복잡하다.
코딩에서 성질이 같은 값들의 그룹을 만드는일은 아주 흔한 작업으로, groupingBy메서드는 그룹작업을 직접 지원한다.
- groupingBy : 한 기준으로 여러그룹으로 나눠질때 사용
// 나라들 기준으로 분류 CH KO JP US
Map<String, List<Locale>> countryToLocale = list.stream().collect(Collectors.groupingBy(Locale::getCountry));
List<Locale> swissLocales = countryToLocale.get("CH"); // [it_CH, de_CH, fr_CH]
그룹으로 묶인 요소들을 특정방식으로 처리하려면 '다운스트림 컬렉터'를 이용한다.
- toSet / counting / summinInt / maxBy / minBy / mapping
// groupingBy는 기본적으로 list를 값으로 사용하는 맵을 리턴하지만, 다른 컬렉션을 원하면 넣어주면됨
Map<String, Set<Locale>> m5 = list3.stream().collect(Collectors.groupingBy(Locale::getCountry, **Collectors.toSet()**));
Map<String, Long> m7 = // 모인 요소들의 개수를 센다. 동명이인을 기준으로 그룹
list.stream().collect(Collectors.groupingBy(Person::getName, **Collectors.counting()**));
Map<String, Integer> m8 = // 모인 요소들의 합계를 구한다.
list.stream().collect(Collectors.groupingBy(Person::getName, **Collectors.summingInt(Person::getAge)**));
Map<String, Optional<Person>> m9 = // 모인 요소들의 최댓값을 구한다.
list.stream().collect(
Collectors.groupingBy(Person::getName, **Collectors.maxBy(Comparator.comparing(Person::getAge))**));
// mapping은 함수를 다운스트림 결과에 적용하며, 이결과를 처리하는데 필요한 또 다른 컬렉터를 요구한다.
// getName해서 나온값에서 Person::getName을 찾을수있어야함. 모인요소들의 이름중 가장짧은 이름을 구함
Map<String, Optional<String>> m10 =
list.stream().collect(
Collectors.groupingBy(Person::getName, **Collectors.mapping(Person::getName, Collectors.minBy(Comparator.comparing(String::length)))**));
// 그룹핑이나 맵핑함수가 in, long, double을 리턴한다면 요소들의 요약통계 객체(SummaryStatistics)안으로 모을수있다.
Map<String, IntSummaryStatistics> m11 =
list2.stream().collect(
Collectors.groupingBy(Person::getName, **Collectors.summarizingInt(Person::getAge)**));
m11.get("shin").getMax(); // 각 그룹의 요약통계 객체로부터 함수값들의 합계, 카운트, 평균, 최댓(솟)값을 얻을수있다.
// 이름 기준으로 그룹핑한 요소들의 ~를 ,로 합침
Map<String, String> m12 =
list2.stream().collect(
**Collectors.groupingBy(Person::getName, Collectors.mapping(Person::getName, Collectors.joining(", "))**));
- partitioningBy : 분류함수가 predicate인 경우, 스트림요소가 리스트2개 (true/false)로 분할될때 유용
// 영어를 사용하는 경우외 그 외의 경우로 분리
Map<Boolean, List<Locale>> m6 = // true / false기준으로 분류할때는 파티셔닝
list.stream().collect(Collectors.partitioningBy(l -> l.getLanguage().equals("en")));
다운스트림 컬렉터는 아주 난해한 표현식을 야기할 수 있기때문에, 다운스트림 맵값을 처리하기위해서는 반드시 groupingBy & partitioningBy와 연계해야한다. (그렇지않으면 굉장히 복잡해짐)
기본타입 스트림
지금까지는 정수를 래퍼객체로 감싸서 처리하는 비효율적인 일을 진행하였다. 하지만 스트림 라이브러리는 기본타입값들을 직접 저장할수있도록 IntStream, LongStream, DoubleStream를 지원한다.
short, char, byte, boolean → IntStream
float, double → DoubleStream
long → LongStream
기본타입 스트림 생성
// Stream<int> 스트림 생성
IntStream stream1 = IntStream.of(1, 2, 3, 4, 5, 6);
// 배열로도 Stream<int> 스트림생성가능
IntStream stream2 = Arrays.stream(array);
스트림 사용
IntStream stream3 = IntStream.range(0, 10); // 1씩 증가. 10제외
IntStream stream4 = IntStream.rangeClosed(0, 10); // 1씩 증가. 10포함
stream4.forEach(System.out::println);
// 객체스트림 -> 기본타입스트림
IntStream stream5 = list.stream().mapToInt(String::length);
stream5.forEach(System.out::println);
**// 리스트<Integer> -> 배열(int)**
list.stream().mapToInt(i -> i.intValue()).toArray();
list.stream().mapToInt(Integer::intValue).toArray();
// 기본타입스트림 -> 객체스트림
Stream<Integer> integers1 = stream5.boxed();
기본타입스트림 눈여겨볼점
- toArray메서드는 기본타입배열을 리턴.
- 옵션결과를 돌려주는 메서드는 OptionalInt, OptionalLong, OptionalDouble을 리턴. optional과 유사하지만 get메서드대신 getAsInt, getAsLong, getAsDouble메서드 포함
- 각각 합계, 평균, 최댓(솟)값을 리턴하는 sum, average, max, min 메서드가 있음. 객체스트림에는 이러한 메서드가 없음
- summaryStatistics 메서드는 스트림의 합계, 평균, 최댓(솟)값을 동시에 보고할 수 있는 Int/Long/DoublesummaryStatistics 타입객체를 돌려줌
- Random 클래스는 난수로 구성된 기본 타입스트림을 리턴하는 ints, longs, doubles메서드를 포함
Stream<Double> double1 = Stream.generate(Math::random).limit(100); // 객체스트림
DoubleStream double2 = new Random().doubles(); // 기본타입스트림
병렬 스트림
병렬 스트림을 이용하면 순서를 보장할 수 는없지만, 빠르게 결과를 확인할 수 있다. 우선 병렬 스트림을 얻어야한다.
- parallel / parallelStream : 병렬 스트림으로 변경 및 생성
1. 병렬스트림으로 생성
Collection.parallelStream(); // 이것을 제외하고는 모두 순차스트림
2. 순차스트림을 병렬스트림으로 변경
list.parallel();
병렬스트림을 사용할때 스레드에 안전한지 유의
int[] words = new int[20];
list.stream().parallel().forEach(s -> {
if (s.length() > 5) {
words[s.length()]++; // 다수의 쓰레드가 하나의 배열을 접근하고있다. 경쟁조건오류
}
});
System.out.println(Arrays.toString(words));
//AtomicInteger객체의 배열을 사용하여 다수의 스레드에 동시에 실행되는것을 방지
스트림은 배열/리트스, range, generator, Stream.sorted()로 호출해서 얻는 스트림에대해 기본적으로 순서를 유지한다.
하지만 몇몇 연산은 순서를 버리면 효과적으로 병렬화될 수 있다. (Stream.distinct는 기본적으로 같은 요소중 첫번째를 유지하지만 순서가 의미없다면 아무거나 고르면됨)
- unordered : 순서를 지키지않겠다라는 의미(병렬처리에 유용)
Stream<T> s = stream.parallel().unordered().limit(100);
- Collectors.groupingByConcurrent : 맵을 병합하는 일은 비용이 많이들지만, 이 메서드는 공유되는 병행맵을 사용하여 맵을 만듦
Map<String, List<Person>> result = list2.parallelStream().collect(Collectors.groupingByConcurrent(Person::getName)); // 공유되는 병행맵을 사용
- optional타입을 잘 다루려면 ifPresent 와 orElse메서드를 이용해야한다.
'IT > 자바' 카테고리의 다른 글
classpath (0) | 2023.03.26 |
---|---|
ExecutorService (0) | 2023.03.26 |
Java8 람다(Lambda)를 이용한 프로그래밍 (0) | 2022.01.23 |
Java8 람다(Lambda)를 사용해보자 (0) | 2022.01.23 |
Java8 옵셔널(Optional<T>)객체 올바르게 사용하기 (0) | 2022.01.23 |
댓글