빙응의 공부 블로그

[JAVA]함수형 프로그래밍 - Stream API 사용법 본문

JAVA

[JAVA]함수형 프로그래밍 - Stream API 사용법

빙응이 2023. 11. 30. 20:46

📝Stream의 사용 순서

Stream은 데이터를 처리하기 위해 다양한 연산들을 지원한다. 

이 과정은 3가지로 나뉘어져 있다.

Stream의 3단계 구조
  • 생성하기
  • 가공하기 
  • 결과 만들기 

📝1단계 - 생성하기

  • Stream을 생성하는 단계이다. 
  • Stream은 재사용이 불가능하므로, 결과 만들기가 끝나면 다시 생성해 주어야한다. 

Stream 연산은 Stream 객체를 기준으로 수행하기 때문에 먼저 컬렉션, 배열을 통해 Stream 객체를 생성해주어야 한다.

결과 만들기를 통해 연산이 끝나면 Stream이 닫히기 때문에 다시 만들어줘야 한다.

 

밑의 코드처럼 .stream을 통해 변환이 가능하다. 

컬렉션 스트림 변환

 

        // List 생성
        List<String> list = Arrays.asList("A", "B", "C");

        // List를 스트림으로 변환
        Stream<String> streamFromList = list.stream();
배열 스트림 변환
        // 배열 생성
        String[] array = {"A", "B", "C"};

        // 배열을 스트림으로 변환
        Stream<String> streamFromArray = Arrays.stream(array);

 

📝2단계 - 가공하기

  • 원본의 데이터를 별도의 데이터로 가공하기 위한 중간 연산이다. 
  • 연산 결과를 Stream으로 다시 반환하기 때문에 연속해서 중간 연산이 가능하다. 

가공하기 단계는 원본의 데이터를 별도의 데이터로 가공하여 새로운 스트림을 반환한다. 

Stream을 원하는 형태로 처리할 수 있음, 중간 연산의 반환 값이 Stream이기 때문에 필요한 만큼 가공이 가능하다.

 

필터링 - Filter

Filter는 Stream에서 조건에 맞는 데이터만을 정제하여 더 작은 컬렉션을 만들어 내는 연산이다.

기본 구조는 함수형 인터페이스의 Predicate(조건자)를 받고 있기 때문에, boolean을 반환하는 람다식을 작성하여

filter 함수를 구현한다. 

 

예를 들어 어떤 문자열에서 길이가 3이상인 문자열만 포함하도록 필터링해보자

Stream<String> stream = list.stream()
    .filter(name -> name.length() >=3);

 

데이터 변환 - Map

Map은 기존의 Stream 요소들을 변환하여 새로운 형태로 형성하는 연산이다.

특정한 형태로 변환하는데 주로 사용되며, 함수형 인터페이스는 function을 받고 있다.

 

예를 들어 어떤 문자열을 받고 문자열의 길이를 출력하는 기능을 만들어보자

// 각 단어의 길이를 맵핑하여 새로운 리스트 생성
List<Integer> wordLengths = words.stream()
              .map(String::length);
데이터 정렬 - Sorted

Stream의 요소들을 정렬하기 위해서는 sorted를 사용해야 한다.

기본적으로 오름차순 정렬이며, 파라미터로 Comparator.reversed()을 사용하면 내림차순이 가능하다.

 

또한 Comparator를 통해 커스텀으로 다양하게 정렬이 가능하다. 

// 오름차순 정렬
List<Integer> sortedNumbers = numbers.stream()
                                     .sorted()
                                     .collect(Collectors.toList());
// 길이로 내림차순 정렬
List<String> sortedWords = words.stream()
                               .sorted(Comparator.comparingInt(String::length).reversed())
                               .collect(Collectors.toList());

 

중복 제거 - Distinct

Stream의 요소들에 중복된 데이터가 존재하는 경우, 중복을 제거하기 위해 distinct를 사용할 수 있다. 

distinct는 중복된 데이터를 검사하기 위해 Object의 equals()와 hashCode를 사용하기 때문에 그에 따른 주의가 필요하다. 

 

List<String> fruits = Arrays.asList("apple", "banana", "orange", "apple", "grape", "banana");

// 중복 제거
List<String> distinctFruits = fruits.stream()
             .distinct()

 

특정 연산 수행 - Peek

Stream의 요소들을 대상으로 Stream에 영향을 주지 않고 특정 연산을 수행할 수 있다.

주요 특징은 다음과 같다.

  • 부수적인 효과 : 주요 스트림 파이프라인에 영향을 미치지 않으면서 요소를 확인하거나 로깅
  • 디버깅 용도: 스트림 요소를 중간 결과로 확인하거나 로깅함

 

예제로는 중간 요소를 출력하는 것이 제일 걸 맞는다.

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

 

 

 

📝3단계 - 결과 만들기

  • 가공된 데이터로부터 원하는 결과를 만들기 위한 최정 연산
  • Stream의 요소를 소모하기 때문에 1번만 처리 가능(Stream이 닫힌다.)
최대값/최소값/총합/평균/갯수

Stream의 요소들을 대상으로 수학적인 연산이 가능하다. 

주의할 점은 min, max, average는 Stream이 비어있는 경우 값을 특정할 수 없어 Optional을 사용해야 한다.

 

// 최대값
Optional<Integer> max = numbers.stream().max(Integer::compare);
System.out.println("최대값: " + max.orElse(null));

// 최소값
Optional<Integer> min = numbers.stream().min(Integer::compare);
System.out.println("최소값: " + min.orElse(null));

// 총합
int sum = numbers.stream().map(Integer::intValue).sum();
System.out.println("총합: " + sum);

// 평균
OptionalDouble average = numbers.stream().map(Integer::intValue).average();
System.out.println("평균: " + average.orElse(null));

// 갯수
long count = numbers.stream().count();
System.out.println("갯수: " + count);

 

데이터 수집 - collect

Stream의 요소들을 List나 Set, Map, 등 다른 종류의 결과로 수집하고 싶은 경우에는 collect 함수를 이용할 수 있다. 

일반적으로 collec 함수는 어떻게 Stream의 요소를 수집할 것인가를 정의한 Collector 타입을 인자로 받아 처리한다.

 

1. 요소 수집 - toList()

Stream에서 작업한 결과를 원하는 컬렉션으로 받을 수 있다. 아래 예제에서는 그 변환을 다루고 있다.

        // List로 요소 수집
        List<String> collectedList = words.stream()
                .collect(Collectors.toList());
                
        // Set으로 요소 수집 (중복 제거)
        Set<String> collectedSet = words.stream()
                .collect(Collectors.toSet());

 

 

2. 이어붙이기 - joining()

Stream에서 작업한 결과를 1개의 String으로 이어붙이기를 원하는 경우 사용한다. 

  • Collectors.joining(delimiter, prefix, suffix)
    • delimiter : 각 요소 중간에 들어가는 요소로 구분자이다.
    • prefix : 결과 맨 앞에 붙는 문자
    • suffix : 결과 맨 뒤에 붙는 문자.
        List<String> words = Arrays.asList("apple", "banana", "orange");

        // 문자열로 결합 (접두사와 접미사 추가)
        String result = words.stream()
                .collect(Collectors.joining(", ", "[", "]"));
                
        //출력 [apple, banana, orange]

 

3. 특정 값 기준 그룹화 - groupingBy()

Stream에서 작업한 결과를 특정 그룹으로 묶기를 원할 때 사용한다. 

결과는 Map으로 반환받는다. 

 

List<String> words = Arrays.asList("apple", "banana", "orange", "avocado", "blueberry");

// 길이를 기준으로 그룹화
Map<Integer, List<String>> groupedByLength = words.stream()
            .collect(Collectors.groupingBy(String::length));
            
 //{5=[apple], 6=[banana, orange], 7=[avocado], 9=[blueberry]}

 

4. 조건 기준 그룹화 - partitioningBy()

Stream에서 특정 조건에 따라 그룹화할 때 사용한다.

 

List<String> words = Arrays.asList("apple", "banana", "orange", "avocado", "blueberry");

// 길이가 6 이상인 문자열과 아닌 문자열로 나누기
Map<Boolean, List<String>> partitionedResult = words.stream()
     .collect(Collectors.partitioningBy(s -> s.length() >= 6));
     
     
    // {false=[apple, orange], true=[banana, avocado, blueberry]}

 

 

조건 검사 - Match

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

크게 3가지가 사용 방법이 있다.

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

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

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// 하나 이상의 요소가 3보다 큰지 검사
boolean anyGreaterThanThree = numbers.stream()
                .anyMatch(num -> num > 3);

// 모든 요소가 0보다 큰지 검사
boolean allGreaterThanZero = numbers.stream()
                .allMatch(num -> num > 0);

// 모든 요소가 10보다 작지 않은지 검사
boolean noneLessThanTen = numbers.stream()
                .noneMatch(num -> num < 10);

 

 

특정 연산 수행 - forEach

Stream의 요소들을 대상으로 어떤 특정한 연산을 수행하고 싶은 경우 forEach 함수를 이용할 수 있다.

 

List<String> fruits = Arrays.asList("apple", "banana", "orange");

// 스트림의 각 요소에 대해 동작을 실행
fruits.stream().forEach(fruit -> System.out.println("Fruit: " + fruit));

 

 


 

📝정리 

Steam API는 3단계 구조(생성하기, 가공하기, 결과 만들기)로 이루어져 있다.

각 단계의 주요 메소드는 다음과 같다.

메소드 용도
1단계 - 생성하기 
Stream() 컬렉션, 배열을 통한 Stream 생성
2단계 - 가공하기
filter() 특정 조건에 맞게 필터링한다.
map() 기존 Stream 요소를 변환하여 새로운 형태로 생성
sorted() Stream 요소를 정렬한다.
distinct() hashCode,equals를 통해 중복 데이터를 제거한다.
peek() Stream에 영향을 주지 않고 특정 연산을 수행한다.
3단계 - 결과 만들기 
max(), min(), sum(), average(), count() 수학적 연산으로 결과 출력 
(단, max,min,average는 값 특정이 불가능해 Optional 사용)
collect() 요소 수집, 이어붙이기, 그룹화를 하여 컬렉션으로 바꿔준다.
anyMatch(), allMatch(), nonMatch() 특정한 조건을 충족하는지 검사한다. 
forEach() 어떤 특정한 연산을 수행하고 싶은 경우 사용한다.

 


✔참조 사이트