코딩 공부/Java

[Java] Stream이란?

ballqs 2024. 7. 29. 18:42

Stream이란?

Java 8부터 추가된 기술로 람다를 활용해 배열과 컬렉션을 간단하게 처리할 수 있는 기술이다. Java 8 이전에는 데이터의 요소를 관리하려면 반복문을 이용해서 하는 방법이였으나 Stream이 생긴 이후에는 함수 여러 개를 조합하여 결과를 필터링하여 가공된 결과를 얻을수 있게 되었고 이를 람다 표현식으로 사용했기에 가독성까지 챙길 수 있게 되었다.


Stream의 특징

  • Stream은 데이터를 변경하지 않는다.
  • Stream은 일회용이다.
  • Stream은 작업을 내부 반복으로 처리한다.

Stream의 흐름

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

흐름 : 전체 -> 맵핑 -> 필터링1 -> 필터링2 -> 결과 만들기 -> 결과물


Stream의 종류

생성하기

  • 배열 , 컬렉션
  • Stream.builder() / Stream.generate() / Stream.iterate()
  • 기본 타입형

가공하기

  • Filtering
  • Mapping
  • Sorting
  • 기타 연산

결과 만들기

  • Calculating
  • Reduction
  • Collecting
  • Matching
  • Iterating

Stream 사용

배열 스트림

String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);

 

컬렉션 스트림

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

 

Stream.builder()

builder를 사용하면 스트림에 직접적으로 원하는 값을 넣을 수 있다. 
마지막에 build 메소드로 스트림을 리턴합니다.

Stream<String> builderStream = Stream.<String>builder()
                                .add("Eric").add("Elena").add("Java")
                                .build(); // [Eric, Elena, Java]

 

Stream.generate()

generate 메소드를 이용하면 Supplier<T>에 해당하는 람다로 값을 넣을 수 있다. 
Supplier<T>는 인자는 없고 리턴값만 있는 함수형 인터페이스

public static<T> Stream<T> generate(Supplier<T> s) { ... }

 

이때 생성되는 스트림은 크기가 정해져 있지 않아 최대 크기를 제한해야 함

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

 

Stream.iterate()

iterate 메소드를 이용하면 초기값과 해당 값을 다루는 람다를 이용해서 스트림에 들어갈 요소를 만든다. 
이방법 또한 크기가 정해져 있지 않아서 최대 크기를 제한해야 함

Stream<Integer> iteratedStream = Stream.iterate(30, n -> n + 2).limit(5); // [30, 32, 34, 36, 38]

 

 

기본 타입형

제네릭을 사용하면 리스트나 배열을 이용해서 기본 타입 스트림을 생성할 수 있다. 하지만 제네릭을 사용하지 않고 직접적으로 해당 타입의 스트림을 다룰 수도 있다. range와 rangeClosed는 범위의 차이이다.

IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4]
LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5]

 

 

Filtering

Filter는 스트림 내 요소들을 하나씩 평가해서 걸러내는 작업이다.

List<String> names = Arrays.asList("Eric", "Elena", "Java");
Stream<String> stream = 
  names.stream()
  .filter(name -> name.contains("a"));
// [Elena, Java]

 

Mapping

Map은 스트림 내 요소들을 하나씩 특정 값으로 변환해주는 작업이다

// Map을 통해 대문자 만들기
List<String> names = Arrays.asList("Eric", "Elena", "Java");
Stream<String> stream = 
  names.stream()
  .map(String::toUpperCase);
// [ERIC, ELENA, JAVA]

 

Sorting

오름차순 내림차순으로 정렬해주는 작업이다.

// 오름차순 , 내림차순
Stream<String> stream = list.stream()
	.sorted() // [a,b,c] 오름차순 정렬
    .sorted(Comparator.reverseOrder()) 	// [c,b,a] (내림차순)

// 문자열 길이 기준 정렬
List<String> list = Arrays.asList("a","ab","abc");
Stream<String> stream = list.stream()
	.sorted(Comparator.comparingInt(String::length)) // [abc,ab,a]

 

기타 연산

Stream<String> stream = list.stream()
    .distinct() // 중복 제거
    .limit(max) // 최대 크기 제한
    .skip(n)    // 앞에서부터 n개 skip하기
    .peek(System.out::println) // 중간 작업결과 확인

 

Calculating

최소, 최대, 합, 평균 등 기본형 타입으로 결과를 만들어낸다.

// 배열없이 스트림을 만들어서 보여주는 예제
long count = IntStream.of(1, 3, 5, 7, 9).count();
long sum = LongStream.of(1, 3, 5, 7, 9).sum();

// 평균 , 최소 , 최대의 경우 스트림이 비어있으면 표현불가하기 때문에 아래와 같음
OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min();
OptionalInt max = IntStream.of(1, 3, 5, 7, 9).max();
OptionalInt avg = IntStream.of(1, 3, 5, 7, 9).average();

// 배열로 보여주는 예제
int[] arr = {1, 2, 5, 6};
arr.stream().min();

 

Reduction

스트림은 reduce라는 메소드를 이용해서 결과를 만들어냅니다.

reduce는 총 3개까지의 파라미터를 받을수 있습니다.

    accumulator : 각 요소를 처리하는 계산 로직. 각 요소가 올 때마다 중간 결과를 생성하는 로직.

    identity : 계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴.

    combiner : 병렬(parallel) 스트림에서 나눠 계산한 결과를 하나로 합치는 동작하는 로직

먼저 인자가 하나만 있을때 인자 2개를 받아 같은 타입의 결과를 반환하는 함수형 인터페이스입니다

OptionalInt reduced = 
  IntStream.range(1, 4) // [1, 2, 3]
  .reduce((a, b) -> {
    return Integer.sum(a, b);
  }); // 6을 리턴(1 + 2 + 3)

 

이번엔 두 개의 인자를 받는 경우! 여기서 10은 초기값이고 스트림 내 값을 더해서 결과는 16

int reducedTwoParams = 
  IntStream.range(1, 4) // [1, 2, 3]
  .reduce(10, Integer::sum); // method reference

 

마지막으로 세 개의 인자를 받는 경우

Integer reducedParams = Stream.of(1, 2, 3)
  .reduce(10, // identity
          Integer::sum, // accumulator
          (a, b) -> {
            System.out.println("combiner was called");
            return a + b;
          });

결과는 다음과 같이 36이 나온다.

먼저 accumulator 는 총 세 번 동작한다.

초기값 10에 각 스트림 값을 더한 세 개의 값(10 + 1 = 11, 10 + 2 = 12, 10 + 3 = 13)을 계산하며. Combiner 는 identity 와 accumulator 를 가지고 여러 쓰레드에서 나눠 계산한 결과를 합치는 역할이다.

12 + 13 = 25, 25 + 11 = 36 이렇게 두 번 호출된다.

 

Collecting

스트림 요소를 원하는 자료형으로 변환

  • Collectors.toList() : 스트림에서 작업한 결과를 담은 리스트로 반환
  • Collectors.joining() : 스트림에서 작업한 결과를 하나의 스트링으로 이어 붙일 수 있음
  • Collectors.averageingInt() : 숫자 값의 평균
  • Collectors.summingInt() : 숫자 값의 합
  • 등등 많지만 여기까지...  부족한건 키워드를 통해 검색하자

 

Matching

매칭은 조건식 람다 Predicate 를 받아서 해당 조건을 만족하는 요소가 있는지 체크한 결과를 리턴합니다. 다음과 같은 세 가지 메소드가 있습니다.

  • 하나라도 조건을 만족하는 요소가 있는지(anyMatch)
  • 모두 조건을 만족하는지(allMatch)
  • 모두 조건을 만족하지 않는지(noneMatch)
boolean anyMatch(Predicate<? super T> predicate);
boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);

// 데이터
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

 

Iterating

foreach는 요소를 돌면서 실행되는 최종 작업이다. 보통 System.out.println 메소드를 넘겨서 결과를 출력할 때 사용하곤 한다.

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

 

마무리

이렇게 나열해도 아직 한참 남은게 Stream이라 볼수있다. 알고리즘 문제 풀면서 자주 볼것 같은 것들만 찾아서 작성해두었다.