본문 바로가기
인프런/김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍

[인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 9. 스트림 API 2 - 기능

by hxxyeoniii 2025. 6. 17.

스트림 생성

스트림은 자바 8부터 추가된 기능으로 데이터 처리에 있어 간결하고 효율적인 코드 작성을 가능하게 해줌

특히 중간 연산과 최종 연산을 구분하며, 지연 연산을 통해 불필요한 연산을 최소화

 

 

 

CreateStreamMain.java : 스트림을 생성하는 대표적인 방법들을 코드로 알아보자

package stream.operation;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class CreateStreamMain {

    public static void main(String[] args) {
        System.out.println("1. 컬렉션으로부터 생성");
        List<String> list = List.of("a", "b", "c");
        Stream<String> stream1 = list.stream();
        stream1.forEach(System.out::println);

        System.out.println("2. 배열로부터 생성");
        String[] arr = {"a", "b", "c"};
        Stream<String> stream2 = Arrays.stream(arr);
        stream2.forEach(System.out::println);

        System.out.println("3. Stream.of() 사용");
        Stream<String> stream3 = Stream.of("a", "b", "c");
        stream3.forEach(System.out::println);

        System.out.println("4. 무한 스트림 생성 - iterate()");
        // iterate : 초기값과 다음값을 만드는 함수를 지정
        Stream<Integer> infiniteStream = Stream.iterate(0, n-> n+2); // 0, 2, 4, 6...
        infiniteStream.forEach(System.out::println); // 무한 출력
        infiniteStream.limit(3).forEach(System.out::println); // 0, 2, 4 만 출력

        System.out.println("5. 무한 스트림 생성 - generate()");
        // generate : Supplier를 사용해 무한 생성
        Stream<Double> randomStream = Stream.generate(Math::random);
        randomStream.limit(3).forEach(System.out::println);
    }
}

-> 컬렉션, 배열, Stream.of는 기본적으로 유한한 데이터 소스로부터 스트림 생성

-> iterate, generate는 별도 종료 조건이 없다면 무한히 데이터를 만들어내는 스트림 생성으로 필요한 만큼(limit)만 사용해야 함


중간 연산

스트림 파이프라인에서 데이터를 변환, 정렬 등을 하는 단계

 

 

 

FlatMap

: 중간 연산 중 하나

: map은 각 요소를 하나의 값으로 변환하지만 FlatMap은 각 요소를 스트림으로 변환한 뒤 그 결과를 하나의 스트림으로 평탄화해줌

 

 

 

MapVsFlatMapMain.java

package stream.operation;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class MapVsFlatMapMain {

    public static void main(String[] args) {
        List<List<Integer>> outerList = List.of(
                List.of(1,2),
                List.of(3,4),
                List.of(5,6)
        );
        System.out.println("outerList = " + outerList);

        ArrayList<Integer> forResult = new ArrayList<>();
        for(List<Integer> list : outerList) {
            for(Integer i : list) {
                forResult.add(i);
            }
        }
        System.out.println("forResult = " + forResult);

        // map
        List<Stream<Integer>> mapResult = outerList.stream()
                .map(list -> list.stream())
                .toList();
        System.out.println("mapResult = " + mapResult);

        // flatMap
        List<Integer> flatMapResult = outerList.stream()
                .flatMap(list -> list.stream())
                .toList();
        System.out.println("flatMapResult = " + flatMapResult);
    }
}

 

실행 결과

 

1) map

> 이중 구조가 그대로 유지

> 각 요소가 Stream 형태가 되므로 결과가 List<Stream<Integer>>가 됨

> java.util.stream.ReferencePipiline$.. 형태로 보임

 

2) flatMap

> flatMap 호출 시 list -> list.stream()이 호출되며 내부의 3개 List<Integer>를 Stream<Integer>로 변환

> flatMap은 Stream<Integer> 내부 값을 꺼내 외부 Stream에 포함

> 따라서 Stream<Integer>가 됨

 

=> flatMap은 중첩 구조를 일차원으로 펼치는 데 사용된다.


Optional 간단 설명

최종 연산을 시작하기 전 자바가 제공하는 Optional 클래스를 간단히 알아보자

 

Optional 클래스

> 내부에 하나의 값(value)을 가짐

> isPresent()를 통해 그 값이 있는지 없는지 확인할 수 있다.

> get()으로 내부의 값을 꺼낼 수 있다. 없다면 예외가 발생한다.

> Optional은 이름 그대로 필수가 아니라 옵션이라는 뜻 : 내부에 값이 있을 수도 있고 없을 수도 있다는 뜻

 

 

 

OptionalSimpleMain.java

package stream.operation;

import java.util.Optional;

public class OptionalSimpleMain {

    public static void main(String[] args) {
        Optional<Integer> optional1 = Optional.of(10);
        System.out.println("optional1 = " + optional1);

        // 값이 있는지 확인할 수 있는 안전한 메서드 제공
        if(optional1.isPresent()) {
            Integer i = optional1.get();
            System.out.println("i = " + i);
        }

        Optional<Object> optional2 = Optional.ofNullable(null);
        System.out.println("optional2 = " + optional2);
        if(optional2.isPresent()) {
            Object o = optional2.get();
            System.out.println("o = " + o);
        }

        // 값이 없는 Optional에서 get 호출 시 NoSuchElementException 발생
        Object o2 = optional2.get();
    }
}

 

실행 결과

-> Optional은 내부에 값을 담아두고 그 값의 null 여부를 체크하는 isPresent()와 같은 안전한 체크 메서드 제공

-> null 값으로 인한 오류를 방지하고 코드에서 "값이 없을 수도 있다"는 상황을 명시적으로 표현하기 위해 사용


최종 연산

스트림 파이프라인의 끝에 호출되어 실제 연산을 수행하고 결과를 만들어낸다.

최종연산이 실행된 후 스트림은 소모되어 더 이상 사용할 수 없다.

 

* reduce를 사용할 때 초깃값을 지정하면, 스트림이 비어 있어도 초깃값이 결과가 된다. 초깃값이 없으면 Optional을 반환한다.

* findFirst(), findAny()도 결과가 없을 수 있으므로 Optional을 통해 값 유무를 확인해야 한다.


기본형 특화 스트림

자바에서는 IntStream, LongStream, DoubleStream의 세가지 형태를 제공해 기본 자료형에 특화된 기능을 사용할 수 있다.

 

* 기본형 특화 스트림의 숫자 범위 생성 기능

1) range(int startInclusive, int endExclusive) : 시작값 이상 ~ 끝값 미만

-> IntStream.range(1, 5) -> [1, 2, 3, 4]

 

2) rangeClosed(int startInclusive, int endInclusive) : 시작값 이상 ~ 끝값 포함

-> IntStream.rangeClosed(1, 5) -> [1, 2, 3, 4, 5]

 

 

 

주요 기능 및 메서드

: 합계, 평균 등 자주 사용하는 연산을 편리한 메서드로 제공

: 타입 변환과 박싱/언박싱 메서드도 제공

 

 

 

성능 - 전통적 for문 vs 스트림 vs 기본형 특화 스트림

* 실제로는 데이터 양, 연산 종류, JVM 최적화 등에 따라 달라짐

 

> 전통적인 for문이 보통 가장 빠름

> 스트림보다 전통적 for문이 1.5 ~ 2배 정도 빠름

> 기본형 특화 스트림(IntStream 등)은 전통적 for문에 가까운 성능을 보여줌

 

실무에선?

극단적 성능이 필요한 경우가 아니라면, 코드의 가독성과 유지보수성을 위해 스트림 API를 사용하는 것이 보통 나음