Java

스트림(Stream)

KJihun 2023. 6. 21. 13:55
728x90

데이터를 작은 조각으로 나누어 순차적으로 처리하는 방식이다. 

유튜브로 스트림에 대한 간단한 예시를 들 수 있다.

유튜브 시청 시, 한번에 분 단위로 스킵을 한다면 버퍼링이 걸리게 된다.

왜냐하면 유튜브는 한꺼번에 영상 전부를 가져오는 방식이 아닌,

동영상 하나를 작은 조각으로 나눠 순차적으로 받아오는 스트리밍 방식이기 때문이다.

이처럼 스트리밍을 사용하면 전체 동영상을 모두 가져오지 않아도 실시간으로 재생할 수 있다.

이를 통해 실시간으로 데이터를 처리하고, 메모리를 효율적으로 사용할 수 있다

 

 

스트림의 특징

1. 원본 데이터를 변경하지 않는다

2. 휘발성이다 - 한번 사용한 스트림은 사라지게 된다.

3. 스트림은 collection에 정의되어 있다.

    모든 컬렉션을 상속하는 구현체(map, filter, forEach 등...)들은 스트림을 반환할 수 있다.

 

Stream은 각 단계('생성 -> 가공 -> 결과')를 거친 후 생성된다. 

 

 


1. 생성

Collection의 Stream 생성

앞서 말했듯이, stream은 collection에 포함되어 있어서 점(.)을 사용하여 손쉽게 호출이 가능하다.

List<String> list = Arrays.asList("Apple", "Banana", "Orange");
Stream<String> listStream = list.stream();

 

배열의 Stream 생성

배열은 Stream.of를 사용하여 생성한다.

String[] fruits = {"Apple", "Banana", "Orange"};
Stream<String> stringStream = Stream.of(fruits);

 


 

2. 가공하기(중간연산)

중간연산을 사용하여 원하는 결과를 출력할 수 있도록 유도한다.

중간연산은 함수형 인터페이스를 사용하며 여러 중간연산을 연결시켜 사용하여 복잡한 조건에 맞게 출력할 수도 있다.

아래는 자주 사용되는 중간연산자이다.

함수형 인터페이스를 사용하는 이유는 Java의 람다식은 함수형 인터페이스로만 선언이 가능하다.

필터링 - Filter 

Filter는 조건에 맞지않는 데이터를 걸러내는 역할을 한다.

조건에 맞는 데이터만을 사용해 더 작은 컬렉션을 만들어 낸다.

함수형 인터페이스 Predicate를 구현하고 있기 때문에, 출력값은 boolean이다.

(Predicate<T> : 객체 T를 매개 변수로 받아 처리한 후 Boolean을 반환)

아래 코드는 어떤 String의 stream에서 'Apple'이 들어간 문자열만을 포함하도록 필터링하는 예제이다.

Stream<String> stream = 
	fruits.stream()
    .filter(name -> name.contains("Apple"));

 

데이터 변환 - Map

Map은 저장된 데이터를 다른 값으로 교체 할 때 주로 사용된다.

함수형 인터페이스 function를 구현하고 있으며  받고 있다.

(Function<T, R> : T는 매개변수, R로 반환하는 함수형 인터페이스)

아래 코드는 fruits를 모두 대문자로 변환한 stream코드이다.

Stream<String> stream = 
	fruits.stream()
    .map(s -> s.toUpperCase());

 

정렬 - Sorted 

Stream은 정렬 시 sorted를 사용한다. default값은 오름차순이며

Comparator의 reverseOrder를 사용하여 내림차순 정렬도 가능하다.

아래는 코드 작성법이다.

// 오름차순 정렬
Stream<String> stream = fruits.stream().sorted();
  
// 내림차순 정렬
Stream<String> stream = fruits.stream().sorted(Comparator.reverseOrder());

 

중복 제거 - Distinct

중복 제거시에는 distinct를 사용한다.

Stream<String> stream = fruits.stream().distinct();

 


 

 

3. 결과(최종 연산)

아래의 메서드들을 이용해 원하는 출력을 얻을 수 있다.

 

 

값 계산

최솟값, 최댓값 등을 출력하기 위한 최종 연산들이 있다. SQL과 같은 명칭을 사용한다.

  • 최대값: max
  • 최소값: min
  • 총합: sum
  • 평균: average
  • 포함된 요소의 개수: count
                List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

                // 총합
                int sum = numbers.stream().reduce(0, (a, b) -> a + b);
                System.out.println("합: " + sum);
                
                
                // ....

                // 포함된 요소의 갯수
                long count = numbers.stream().count();
                System.out.println("갯수: " + count);

 

 

collect

collect는 Stream의 데이터를 다른 종류의 결과로 컬렉션 또는 다른 형태의 결과로 반환하는 메소드이다.

Collector 인터페이스를 매개변수로 받으며,

스트림 요소를 수집하는 데 사용되는 메소드(자료형, 초기화, 추가, 병합 등)를 정의한다.

 

  • collect() : 스트림의 최종연산, Collector 인터페이스를 매개변수로 받음
  • Collector : collect 파라미터
  • Collectors : 자주 사용되는 Collector를 제공

 

Collectors.to ~ ()

결과값을 원하는 자료구조로 반환하는 Collecters.

흔히 사용되는 컬렉션 형태인 `List`, `Set`, `Map` 등으로 반환하는 데 유용하다

// 결과값을 List로 반환하는 예시
List<String> fruitsList = fruits.stream()
	.collect(Collectors.toList());
    
// 결과값을 Map으로 반환하는 예시
Map<String, Integer> fruitsNameToPriceMap = fruits.stream()
	.collect(Collectors.toMap(fruits::getName, fruits::getPrice));

 

Collectors.joining()

Stream에서 작업한 결과를 1개의 String으로 출력할 때 사용한다.

총 3개의 인자를 받을 수 있는데, delimiter, prefix, suffix 순으로 값을 넣을 수 있다.

  • delimiter : 각 요소 중간에 들어가 요소를 구분시켜주는 구분자
  • prefix : 결과 맨 앞에 붙는 문자
  • suffix : 결과 맨 뒤에 붙는 문자
// String 사이에 ',' 을 넣고 맨 앞에는 '<', 맨 뒤에는 '>'를 넣음
String listToString = productList.stream()
  	.map(Product::getName)
  	.collect(Collectors.joining(", ", "<", ">"));

 

 

Collectors.summingInt(),  Collectors.averagingInt() ..

Stream에서 결과의 합, 평균값 등을 구할 때 사용하며

Collectors.averagingInt(): 평균값

Collectors.summingInt(): 합한 값

Collectors.summarizingInt(): 모든 값(개수, 합계, 평균, 최소, 최대)

  • 개수 getCount()
  • 합계 getSum()
  • 평균 getAverage()
  • 최소 getMin()
  • 최대 getMax()
Double averageAmount = productList.stream()
	.collect(Collectors.averagingInt(Product::getAmount));

Integer summingAmount = productList.stream()
	.collect(Collectors.summingInt(Product::getAmount));

IntSummaryStatistics statistics = productList.stream()
    .collect(Collectors.summarizingInt(Product::getAmount));

//IntSummaryStatistics {count=5, sum=86, min=13, average=17.200000, max=23}

 

 

Collectors.groupingBy()

함수형 인터페이스 Function을 사용해서 특정 값을 기준으로 Stream 내의 요소들을 그룹핑 한다.

Stream에서 작업한 결과를 원하는 특정 그룹으로 묶어주며, 반환타입은 Map이다.

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'}]}
 */

 

Collectors.partitioningBy()

조건에 따라 두 개의 그룹으로 분할하여 `Map`으로 수집하는 `Collector`를 반환한다.

예를 들어 제품의 갯수가 15보드 큰 경우와 그렇지 않은 경우를 나누고자 한다면 다음과 같이 코드를 작성할 수 있다.

 

Map<Boolean, List<Product>> mapPartitioned = productList.stream()
	.collect(Collectors.partitioningBy(p -> p.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'}]}
 */

 

 

 

Match

Stream의 요소들이 특정한 조건을 충족하는지 검사하고 싶은 경우 사용한다.

검사 결과는 boolean으로 반환한다. match 함수에는 크게 다음의 3가지가 있다.

  • anyMatch: 1개의 요소라도 해당 조건을 만족하는가
  • allMatch: 모든 요소가 해당 조건을 만족하는가
  • nonMatch: 모든 요소가 해당 조건을 만족하지 않는가

예를 들어 다음과 같은 예시 코드가 있다고 할 때, 아래의 경우 모두 true를 반환하게 된다.

 

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

boolean anyMatch = names.stream()
    .anyMatch(name -> name.contains("a"));
boolean allMatch = names.stream()
    .allMatch(name -> name.length() > 3);
boolean noneMatch = names.stream()
    .noneMatch(name -> name.endsWith("s"));

 

 

forEach

Stream의 요소들을 대상으로 어떤 특정한 연산을 수행하고 싶은 경우에는 forEach 함수를 이용할 수 있다. 앞에서 살펴본 비슷한 함수로 peek()가 있다. peek()는 중간 연산으로써 실제 요소들에 영향을 주지 않은 채로 작업을 진행하고, Stream을 반환하는 함수였다. 하지만 forEach()는 최종 연산으로써 실제 요소들에 영향을 줄 수 있으며, 반환값이 존재하지 않는다. 예를 들어 요소들을 출력하기를 원할 때 다음과 같이 forEach를 사용할 수 있다.

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

 

 

'Java' 카테고리의 다른 글

오버로딩(Overloading)과 오버라이딩(Overriding)  (0) 2023.06.09
모던 자바 알아보기(람다, 스트림, Optional)  (0) 2023.05.30
쓰레드의 상태  (0) 2023.05.30
동기화(synchronized)  (0) 2023.05.30
쓰레드 제어 메소드  (0) 2023.05.29