Stream (스트림)

태로미·2023년 9월 11일
0

JAVA

목록 보기
3/3
post-thumbnail




✍🏻개요


JAVA8에서 추가된 스트림(Streams)은 람다를 활용할 수 있는 기술 중 하나이며,
배열, 컬렉션의 요소를 하나씩 참조해서 람다식으로 처리할 수 있는 반복자이다.

JAVA 8 이전에 배열과 컬렉션 인스턴스를 다루는 방법은
for문, 또는 forEach문을 돌면서 요소 하나씩을 꺼내서 다루는 방법이였는데,
로직이 복잡해질수록 코드의 양이 많아져 여러 로직이 섞이게 되고
메서드를 나눌 경우 루프를 여러 번 도는 경우가 발생하게 된다.

Stream은 '데이터의 흐름’.
람다를 이용해서 코드의 양을 줄이고 간결하게 표현할 수 있는데
이는 배열과 컬렉션을 함수형으로 처리할 수 있다는 뜻이며,
또 하나의 장점은 하나의 작업을 둘 이상의 작업으로 잘게 나눠서 동시에 진행하는
병렬처리(multi-threading)가 간단하게 가능하다는 점!
즉, 쓰레드를 이용해 많은 요소들을 빠르게 처리할 수 있다.

더불어,
인터페이스를 이용한 컬렉션을 다루는 방식을 아무리 표준화 했다지만,
List를 정렬할 때는 Collection.sort()를 사용해야하고
배열을 정렬할 때는 Arrays.sort()를 사용해야 하는 것 처럼
각 컬렉션 클래스에는 같은 기능의 메서드들이 중복해서 정의되어 있다.
이렇게 데이터 소스마다 다른 방식으로 다루어야하는 문제점을 해결해주는 것도 바로 Stream!!!!!!!! 이러한 Stream에 대해 알아보자😊







📌생성하기


1. 배열 스트림

Arrays.Stream() 메서드를 사용하여 스트림을 생성.

String[] arr = new String[]{"a", "b", "c"};

Stream<String> stream = Arrays.stream(arr);
Stream<String> stream2 = Arrays.stream(arr, 1, 3);  // [b, c]
  • Stream을 생성하고 제네릭타입을 지정하고 스트림을 생성함.
  • Arrays.stream(arr, a, b) 메서드 사용, 배열 arr의 index a부터 b-1까지.

2. 컬렉션 스트림

컬렉션 타입의 경우, 인터페이스에 추가된 디폴트 메서드 stream() 사용.

List<String> list = Arrays.asList("a", "b", "c");

Stream<String> stream = list.stream();
Stream<String> ps = list.parallelStream();   // 병렬 처리 스트림
  • asList()를 사용해서 List 객체를 만들면 새로운 배열 객체를 만드는 것이 아니라, 원본 배열의 주소값을 가져오는 것.
    • 원본과 엮여있으므로 새로운 원소 추가, 삭제, 사이즈 변경 불가.
  • parallelStream() 메서드는 병렬 처리 스트림.

3. 비어있는 스트림

빈 스트림(empty streams)은 요소가 없을 때 null대신 사용할 수 있음.

Stream emptyStream = Stream.empty();
long count = emptyStream.count();     // count의 값은 0
public Stream<String> streamOf(List<String> list) {

	return list == null || list.isEmpty() ? 
    	   Stream.empty() : list.stream();
}
  • empty() 메서드는 빈 스트림을 생성해서 반환하며,
    위의 코드는 list가 null이 아니거나 비어있지 않다면 stream 생성하게 됨.

4. Stream.<T>builder()

빌더(Builder)를 사용하면 스트림에 직접적으로 원하는 값을 넣을 수 있음.

Stream<String> bst = Stream.<String>builder()
    					   .add("111").add("222").add("333")
    					   .build();   // [111, 222, 333]
  • Stream.builder() 메서드에 제네릭 타입 지정, add() 메서드로 원하는 값을 추가하고 마지막으로 build() 메서드를 사용하여 스트림 return.
    • 메서드 체이닝 방식

5. Stream.generate()

generate() 메서드로 Supplier<T>에 해당하는 람다로 값을 넣을 수 있으며,
인자는 없고 리턴값만 있는 함수형 인터페이스로 람다에서 리턴하는 값이 들어감.
이 때, 생성되는 스트림은 크기가 정해져있지 않고 무한(infinite)하기 때문에
limit() 메서드로 최대 크기를 제한해야 함.

Stream<String> gs = Stream.generate(() -> "gen").limit(5);   
					// [gen, gen, gen, gen, gen]

6. Stream.iterate()

iterate() 메서드로 초기값과 해당 값을 다루는 람다를 이용하여 스트림에 들어갈 요소를 만듬. 이 방법 또한 크기가 무한하므로 limit() 메서드 사용.

Stream<Integer> is = Stream.iterate(30, n -> n + 2).limit(5);   
  					 // [30, 32, 34, 36, 38]
  • 30이 초기값이고 값이 2씩 증가하는 값들이 들어가게 됨.
    즉, 요소가 다음 요소의 input으로 들어가는 것.

7. 기본 타입형 스트림

제네릭을 사용하면 리스트나 배열을 사용해서 기본 타입(int, long, double) 스트림을 생성할 수 있지만, 제네릭을 사용하지 않고 직접적으로 해당 타입의 스트림을 다룰 수 있음.

range

IntStream intStream = IntStream.range(1, 5);            // [1, 2, 3, 4]
LongStream longStream = LongStream.rangeClosed(1, 5);   // [1, 2, 3, 4, 5]
  • range()rangeClosed()는 두 번째 인자인 종료지점의 포함여부가 다름.

boxing

Stream<Integer> bis = IntStream.range(1, 5).boxed();
  • 제네릭을 사용하지 않기 때문에 오토박싱이 일어나지 않으므로,
    필요한 경우 boxed() 메서드로 박싱하면 됨.

Random

DoubleStream doubles = new Random().doubles(3);   // 난수 3개 생성
  • JAVA8의 Random 클래스는 난수를 가지고 세 가지 타입의 스트림
    (IntStream, LongStream, DoubleStream)을 만들어낼 수 있음.
  • 쉽게 난수 스트림을 생성해서 여러가지 후속 작업을 취할 수 있어 매우 유용.

8. 문자열 스트림

char()

IntStream cs = "Stream".chars();   // [83, 116, 114, 101, 97, 109]
  • String을 이용해서 스트림 생성 가능.
  • 문자열의 각 문자(char)를 IntStream으로 변환함.
    • char는 문자이지만 본질적으로는 숫자이기 때문에 가능.
    • 각 문자에 해당하는 숫자들이 출력됨.

compile() / splitAsStream()

Stream<String> ss = Pattern.compile(", ")
						   .splitAsStream("Eric, Elena, Java");   
                           // [Eric, Elena, Java]
  • 정규표현식(RegEx)을 이용해서 문자열을 자르고, 각 요소들로 스트림을 만듬.

9. 파일 스트림

Stream<String> lineStream = 
               Files.lines(Paths.get("file.txt"), 
               Charset.forName("UTF-8"));
  • 자바 NIO의 Files 클래스의 lines() 메서드는
    해당 파일의 각 라인을 String타입의 스트림으로 만들어줌.

10. 병렬 스트림

병렬처리란, 한가지 작업을 서브 작업으로 나누고,
서브 작업들을 분리된 쓰레드에서 병렬적으로 처리하는 것.

런타임시 하나의 작업을 여러개의 작업으로 나눠 처리 후, 결과 자동으로 결합.

스트림 생성 시 사용하는 stream() 메서드 대신 parallelStream() 메서드,
내부 반복자를 사용하여 병렬 스트림을 쉽게 생성할 수 있음.

내부적으로는 쓰레드 처리를 위해, JAVA7부터 도입된 Fork/Join framework 사용.

// 병렬 스트림 생성
Stream<Product> parallelStream = productList.parallelStream();

// 병렬 여부 확인
boolean isParallel = parallelStream.isParallel();   // true

외부 반복자내부 반복자
개발자가 코드로 직접 컬렉션의 요소를 가져오는 패턴.
(index를 활용한 for문, iterator, while문 등)
컬렉션 내부에서 요소들을 반복시키고,
개발자는 요소당 처리해야할 코드만 제공하는 패턴.

Thread를 이용한 병렬 처리

각 작업이 쓰레드를 이용해 병렬 처리됨.

boolean isMany = parallelStream
                 .map(product -> product.getAmount() * 10)
                 .anyMatch(amount -> amount > 200);

배열의 병렬 스트림

배열을 이용해서 병렬 스트림을 생성하는 경우 parallel() 메서드 사용.

Arrays.stream(arr).parallel();

기본타입의 병렬 스트림

컬렉션과 배열이 아닌 경우 parallel() 메서드를 이용해서 처리.

IntStream intStream = IntStream.range(1, 150).parallel();

// 병렬 여부 확인
boolean isParallel = intStream.isParallel();   // true

순차모드(sequential)

다시 sequential(순차) 모드로 돌리고 싶다면 sequential() 메서드를 사용.
병렬 스트림이 무조건 좋은 것은 아님.

IntStream intStream = intStream.sequential();

// 병렬 여부 확인
boolean isParallel = intStream.isParallel();   // false

순차처리 vs 병렬처리

public static void main(String[] args) {

	List<String> list = Arrays.asList("apple", "banana", "coconut", "grape");
	
	// 순차처리
	Stream<String> stream = list.stream();
	stream.forEach(ParallelExample::print);
	
	// 병렬처리
	Stream<String> parallelStream = list.parallelStream();
	parallelStream.forEach(ParallelExample::print);
}


  private static void print(String s) {
      System.out.println(s + " : " + Thread.currentThread().getName());
  }
  
// 실행결과
// apple : main
// banana : main
// coconut : main
// grape : main
// coconut : main
// grape : main
// banana : ForkJoinPool.commonPool-worker-5
// apple : ForkJoinPool.commonPool-worker-19

11. 스트림 연결하기

concat() 메서드로 두 개의 스트림을 연결, 새로운 스트림 만들 수 있음.

Stream<String> stream1 = Stream.of("Java", "Scala", "Groovy");
Stream<String> stream2 = Stream.of("Python", "Go", "Swift");
Stream<String> concat = Stream.concat(stream1, stream2);   
						// [Java, Scala, Groovy, Python, Go, Swift]






📌가공하기

전체 요소 중에서 다음과 같은 API를 이용해서 원하는 것만 뽑아낼 수 있음.
이러한 가공 단계를 중간 작업(intermediate operations)이라 하며,
스트림을 리턴하기 때문에 여러 작업을 이어 붙여서(chaining) 작성 가능.


1. Filtering

필터(filter)는 스트림 내 요소들을 하나씩 평가해서 걸러내는 작업.

List<String> names = Arrays.asList("Eric", "Elena", "Java");

Stream<String> stream = names.stream()
							 .filter(name -> name.contains("a"));   
                             // [Elena, Java]
  • 리스트 names에 stream() 메서드를 사용하여 스트림을 생성하고,
    filter() 메서드로 스트림의 각 요소에 대해서 평가식을 실행함.
  • a가 포함된 요소만 걸러내는(filter)작업을 거쳐 return됨.

2. Mapping

맵(map)은 스트림 내 요소들을 하나씩 특정 값으로 변환함.
이 때 값을 변환하기 위한 람다를 인자로 받음.
스트림에 들어가 있는 값이 input되어 특정 로직을 거친 후 output되고,
리턴되는 새로운 스트림에 담기는데, 이러한 작업이 바로 맵핑(mapping).


List<String> names = Arrays.asList("Eric", "Elena", "Java");

Stream<String> stream = names.stream()
							 .map(String::toUpperCase);   
                             // [ERIC, ELENA, JAVA]
  • 메서드 참조로 스트림 내 String의 toUpperCase() 메서드를 실행해서 대문자로 변환한 값들이 담긴 스트림을 리턴함.

Stream<Integer> stream = productList.stream()
							  		.map(Product::getAmount);   
                                    // [23, 14, 13, 23, 13]
  • 요소 내 들어있는 Product 개체의 수량을 꺼내올 수도 있음.
    각 ‘상품’을 ‘상품의 수량’으로 맵핑하는 것.

mapToInt / mapToObject

Stream.of(1.0, 2.0)
	  .mapToInt(Double::intValue)
	  .mapToObj(String:valueOf)
	  .collect(Collectors.toList());
  • mapToInt 메서드는 스트림을 IntStream으로 변환.
    • IntStream을 제외한 모든 스트림에서 동일하게 제공하는 메서드.
  • mapToObj 메서드는 스트림을 Stream으로 변환.
  • Stream<Double>이라는 객체를 of() 메서드를 통해서 생성하고,
    메서드 체인으로 int타입으로 변경.
  • mapToInt(Double::intValue)
    일반적인 Stream객체를 원시타입 IntStream으로 변경한 것이고,
    그 이후 IntStream을 mapToObj(String.valueOf) 메서드를 이용해서
    원시타입 IntStream에서 Stream<String>으로 변환.

3. FlatMap

flatMap은 중첩구조를 한 단계 제거하고 단일 컬렉션으로 만들어주는 역할을 하며, 이러한 작업을 플래트닝(flattening)이라고 함.
인자로 mapper를 받고 있는데, 리턴 타입이 Stream.
즉, 새로운 스트림을 생성해서 리턴하는 람다를 넘겨야 함.

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]
  • 위의 중첩된 리스트를 flatMap을 사용해서 중첩 구조 제거 후 작업할 수 있음.

students.stream()
		.flatMapToInt(student -> 
        			  IntStream.of(student.getKor(), 
			                       student.getEng(), 
			                       student.getMath()))
		.average()
        .ifPresent(avg -> System.out.println(Math.round(avg * 10) / 10.0));
  • 객체에 적용한 것.
  • 학생 객체를 가진 스트림에서 학생의 국영수 점수를 뽑아 새로운 스트림을 만들어 평균을 구하는 코드.
  • 이는 map 메서드 자체만으로는 한번에 할 수 없는 기능.

4. Sorting

정렬의 방법은 다른 정렬과 마찬가지로 Comparator를 이용하며,
인자 없이 그냥 호출할 경우 오름차순으로 정렬됨.

인자 없는 경우

IntStream.of(14, 11, 20, 39, 23)
		 .sorted()
		 .boxed()
		 .collect(Collectors.toList());
		 // [11, 14, 20, 23, 39]
  • IntStream의 원소들을 정렬하고,
    제네릭없이 바로 생성했으므로 박싱해준 뒤, Stream을 List로 변환함.
  • 인자없이 그냥 호출했으므로 오름차순으로 정렬됨.

인자 있는 경우

List<String> lang = 
			 Arrays.asList("Java", "Scala", "Groovy", "Python", "Swift");

// 1. 순차정렬
lang.stream()
    .sorted()
    .collect(Collectors.toList());
	// [Groovy, Java, Python, Scala, Swift]

// 2. 역순정렬
lang.stream()
    .sorted(Comparator.reverseOrder())
    .collect(Collectors.toList());
	// [Swift, Scala, Python, Java, Groovy]
  • 리스트에서 알파벳 순으로 순차정렬과 Comparator를 넘겨 역순정렬함.

문자열 길이 기준

// 1. 순차정렬
lang.stream()
	.sorted(Comparator.comparingInt(String::length))
	.collect(Collectors.toList());
	// [Java, Scala, Swift, Groovy, Python]

// 2. 역순정렬
lang.stream()
	.sorted((s1, s2) -> s2.length() - s1.length())
	.collect(Collectors.toList());
	// [Groovy, Python, Scala, Swift, Java]

5. Iterating

스트림 내 요소들 각각을 대상으로 특정 연산을 수행하는 peek() 메서드.
‘peek’ 은 그냥 확인해본다는 단어 뜻처럼,
특정 결과를 반환하지 않는 함수형 인터페이스 Consumer를 인자로 받기에
스트림 내 요소들 각각에 특정 작업을 수행할 뿐 결과에 영향을 미치지 않음.
작업을 처리하는 중간에 결과를 확인해볼 때 사용할 수 있음.

int sum = IntStream.of(1, 3, 5, 7, 9)
				   .peek(System.out::println)   // 1 3 5 7 9
				   .sum();

System.out.println(sum);  // 25






📌결과 만들기

가공한 스트림을 가지고 내가 사용할 결과값으로 만들어내는 단계,
스트림을 끝내는 최종 작업 (terminal operations).
스트림 API는 다양한 종료 작업을 제공함.


1. Calculating

최소, 최대, 합, 평균 등 기본형 타입으로 결과를 만들어낼 수 있음.

long count = IntStream.of(1, 3, 5, 7, 9).count();   // 5
long sum = LongStream.of(1, 3, 5, 7, 9).sum();      // 25

Optional

Optional이라는 객체는 여러가지 에러를 잡아주기 위해 존재.
NullPointerException에 대한 문제를 해결하기 위해 나옴.
그럼에도 null이 발생할 수 있고 null 체크를 해야만 하는 경우가 빈번한데,
이를 orElse()orElseGet() 메서드로 null일 시 default값을 넣어 줄 수 있음.
만약 스트림이 비어 있을 경우 count와 sum은 0을 출력하는 반면에,
평균, 최소, 최대의 경우는 표현할 수 없기 때문에 Optional을 이용해 리턴해야 함.

OptionalInt min = IntStream.of(1, 3, 5, 7).min();                 // 1
OptionalDouble max = DoubleStream.of(1.5, 3.5, 5.7, 7.9).max();   // 7.9

getAsInt()

min(), max() 메서드를 사용할 경우 반환되는 값의 타입은 OptionalInt.
이 때, 정수가 필요하다면? getAsInt() 메서드를 사용하여 정수로 얻을 수 있음.

int min = Arrays.stream(arr).min().getAsInt();

ifPresent()

스트림에서 바로 ifPresent() 메서드를 이용해서 Optional 을 처리할 수 있음.

DoubleStream.of(1.1, 2.2, 3.3, 4.4, 5.5)
			.average()
			.ifPresent(System.out::println);   // 3.3

orElse()

T의 모든 매개 변수를 사용, T 클래스, 값을 인수로 받음.
단순히 메서드가 아닌, null일 때 값을 넘겨야 할 때 사용.

public T orElse(T other) {
    return value != null ? value : other;
}

orElseGet()

T 유형의 개체를 반환하는 Supplier 유형의 인터페이스를 허용.
T 클래스를 상속받은 하위 클래스를 return해주는 Supplier 함수 인터페이스 인수 받으며, Supplier은 함수적 인터페이스로서 get을 호출하여 결과를 리턴하는 역할.
null일 경우에 메서드를 실행해야 할 때 사용.

public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}


2. Reduction

스트림은 reduce() 메서드를 이용해서 결과를 만들어 내며,
스트림에 있는 여러 요소의 총합을 낼 수도 있음.
reduce() 메서드는 총 3가지의 파라미터를 받을 수 있음.

accumulatoridentitycombiner
각 요소를 처리하는 계산 로직. 각 요소가 올 때마다 중간 결과를 생성하는 로직.계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴.병렬(parallel) 스트림에서 나눠 계산한 결과를 하나로 합치는 동작하는 로직.

// 1개 (accumulator)
Optional<T> reduce(BinaryOperator<T> accumulator);

// 2개 (identity)
T reduce(T identity, BinaryOperator<T> accumulator);

// 3개 (combiner)
<U> U reduce(U identity,
	         BiFunction<U, ? super T, U> accumulator,
	         BinaryOperator<U> combiner);

인자가 1개 있는 경우

OptionalInt reduced = 
			IntStream.range(1, 4) // [1, 2, 3]
					 .reduce((a, b) -> {return Integer.sum(a, b);}); // 6
  • 여기서 BinaryOperator<T>는 같은 타입의 인자 두 개를 받아
    같은 타입의 결과를 반환하는 함수형 인터페이스.
  • 두 값을 더하는 람다를 넘겨주고 있으므로 결과는 6 (1 + 2 + 3).

인자가 2개 있는 경우

int reducedTwoParams = 
	IntStream.range(1, 4)        		  // [1, 2, 3]
	         .reduce(10, Integer::sum);   // 16
  • 여기서 10은 초기값이고, 스트림 내 값을 더해서 결과는 16 (10 + 1 + 2 + 3).
  • 람다는 메소드 참조(method reference)를 이용해서 넘길 수 있음.

인자가 3개 있는 경우

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는 실행되지 않음.
  • Combiner는 병렬 처리 시, 각자 다른 쓰레드에서 실행한 결과를 마지막에 합치는 단계이므로 병렬 스트림에서만 동작함.

Integer reducedParallel = 
		Arrays.asList(1, 2, 3)
			  .parallelStream()         // 병렬 스트림
			  .reduce(10,               // identity
					  Integer::sum,     // accumulator
					  (a, b) -> {System.out.println("combiner was called");
								 return a + b;   // 36
                                 });

// 실행결과
// combiner was called
// combiner was called
// 36
  • 먼저 accumulator는 총 3번 동작함.
    • 초기값 10에 각 스트림 값을 더한 세 개의 값
      (10+1=11, 10+2=12, 10+3=13)을 계산.
  • Combiner는 identity와 accumulator를 가지고 여러 쓰레드에서 나눠 계산한 결과를 합치는 역할.
    • 12+13=25, 25+11=36 이렇게 두 번 호출됨.


3. Collecting

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"));
  • 예제에서 사용할 간단한 List.
  • Product 객체는 수량(amout)과 이름(name)을 가지고 있는 설정.

Collectors.toList()

스트림에서 작업한 결과를 담은 리스트로 반환.
map으로 각 요소의 이름을 가져온 후,
Collectors.toList() 메서드를 사용하여 리스트로 결과를 가져옴.

List<String> collectorCollection =
			 productList.stream()
						.map(Product::getName)
						.collect(Collectors.toList());
						// [potatoes, orange, lemon, bread, sugar]

Collectors.joining()

스트림에서 작업한 결과를 하나의 String으로 이어 붙일 수 있음.
3개의 인자를 받아 간단하게 String을 조합할 수 있음.

delimiterprefixsuffix
각 요소 중간에 들어가 요소를 구분시켜주는 구분자결과 맨 앞에 붙는 문자결과 맨 뒤에 붙는 문자

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
Integer summingAmount = 
		productList.stream()
				   .mapToInt(Product::getAmount)
				   .sum(); 
				   // 86
  • IntStream으로 바꿔주는 mapToint() 메서드로 더 간단하게 표현 가능.

Collectors.summarizingInt()

합계와 평균 모두 필요할 때 스트림을 두 번 생성하지 않고 한 번에 얻을 수 있음.

IntSummaryStatistics statistics = 
		productList.stream()
				   .collect(Collectors.summarizingInt(Product::getAmount));
IntSummaryStatistics {count=5, sum=86, min=13, average=17.200000, max=23}
  • 이렇게 받아온 IntSummaryStatistics 객체에는 다음과 같은 정보가 담겨 있는데, 아래의 메서드를 이용하면 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'}]}
  • 수량을 기준으로 그룹핑, 여기서 받는 인자는 함수형 인터페이스 Function.
  • 결과는 Map 타입으로 나오는데, 같은 수량이면 리스트로 묶어서 보여줌.

Collectors.partitioningBy()

위의 groupingBy 함수형 인터페이스 Function을 이용해서 특정 값을 기준으로 스트림 내 요소들을 묶었다면, partitioningBy은 함수형 인터페이스 Predicate를 받으며, Predicate는 인자를 받아서 boolean값을 리턴함.

따라서 평가를 하는 함수를 통해서 스트림 내 요소들을 true와 false로 나눌 수 있음.

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) { ... }
Set<Product> unmodifiableSet = 
	  productList.stream()
	  .collect(Collectors.collectingAndThen(Collectors.toSet(),
											Collections::unmodifiableSet));
  • Collectors.toSet() 메서드를 사용해서 결과를 Set으로 collect한 후 수정불가한 Set으로 변환하는 작업을 추가 (finisher)로 실행함.

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) { ... }
Collector<Product, ?, LinkedList<Product>> toLinkedList = 
		 Collector.of(LinkedList::new, 
					  LinkedList::add, 
					  (first, second) -> {
					   first.addAll(second);
					   return first;
                       });
  • collector를 하나 생성함.
  • collector를 생성하는 supplier에 LinkedList의 생성자를 넘겨주고, accumulator에는 리스트에 추가하는 add() 메서드를 넘겨주고 있음.
    • 따라서 이 collector는 스트림의 각 요소에 대해서 LinkedList를 만들고 요소를 추가하게 됨.
  • 마지막으로 combiner를 이용해 결과를 조합하여 생성된 리스트들을
    하나의 리스트로 합침.

LinkedList<Product> linkedListOfPersons = 
					productList.stream()
							   .collect(toLinkedList);
  • collect 메서드에 위에서 만든 커스텀 collector를 넘겨줄 수 있고,
    결과가 담긴 LinkedList가 반환됨.


4. Matching

조건식 람다를 받아 해당 조건을 만족하는 요소가 있는지 체크한 결과를 리턴.
아래와 같이 3가지 메서드가 있음.

anyMatchallMatchnoneMatch
하나라도 조건을 만족하는 요소가 있는지모두 조건을 만족하는지모두 조건을 만족하지 않는지

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


5. Iterating

foreach는 요소를 돌면서 실행되는 최종 작업.
보통 System.out.println() 메서드를 넘겨서 결과를 출력할 때 사용함.
앞서 살펴본 peek() 메서드와는 중간 작업과 최종 작업의 차이가 있음.

names.stream().forEach(System.out::println);






💭마무리


  1. 생성하기
    – 스트림 인스턴스 생성.
  2. 가공하기
    – 필터링 및 맵핑 등 원하는 결과를 만들어가는 중간 작업.
  3. 결과 만들기
    – 최종적으로 결과를 만들어내는 작업.

전체 → 맵핑 → 필터링1 → 필터링2 → 결과 만들기 → 결과물

// 기존
String[] strArr = {"aaa", "bbb", "ccc"};
List<String> strList = Arrays.asList(strArr);

// 스트림 생성
Stream<String> strStream1 = strList.stream();
Stream<String> strStream2 = Arrays.stream(strArr);

// 스트림 출력
strStream1.sorted().forEach(System.out::println);
strStream2.sorted().forEach(System.out::println);
  • 스트림은 데이터 소스로 부터 데이터를 읽기만 할 뿐, 변경하지 않음.
  • 스트림은 한번 사용하면 닫혀서 다시 사용할 수 없음.






📑REFERENCE

참조링크1 / 참조링크2 / 참조링크3

profile
Here And Now

0개의 댓글