본문 바로가기
IT/자바

Java8 스트림(Stream)연산을 사용해보자

by 모띠 2022. 1. 23.

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(); // 스트림

스트림은 데이터를 변환하고 추출할수있어서 겉으로는 컬렉션과 유사해보이지만 큰 차이점이 있다.

  • 스트림은 요소들을 보관하지않는다. 요소들은 하부의 컬렉션에 보관되거나 필요할때 생성된다.
  • 스트림 연산은 원본을 변경하지않는다. 대신 결과를 담은 새로운 스트림을 반환한다.
  • 스트림 연산은 가능하면 지연처리된다.

스트림 연산의 파이프라인은 세단계로 구분된다.

  1. 스트림생성
  2. 초기스트림을 다른스트림으로 변환하는 중간 연산
  3. 결과를 산출하기 위한 최종연산. 여기서 지연연산의 실행을 강제. 이후로는 해당스트림을 사용할수없다.

스트림생성

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)); // 공유되는 병행맵을 사용

스트림 API에서 사용하는 함수형 인터페이스

  • 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

댓글