군만두의 IT 공부 일지

[스터디4] 08. 스트림 요소 처리 본문

프로그래밍/Java

[스터디4] 08. 스트림 요소 처리

mandus 2025. 3. 21. 17:25

목차

    17장. 스트림 요소 처리

    17.1 스트림이란?

    컬렉션 및 배열에 저장된 요소를 반복 처리하기 위해서는 for 문을 이용하거나 Iterator(반복자)를 이용했음.

    List<String> list = ...;
    for(int i=0; i<list.size(); i++) {
        String item = list.get(i);
        // item 처리
    }
    Set<String> set = ...;
    Iterator<String> iterator = set.iterator();
    while(iterator.hasNext()) {
        String item = iterator.next();
        // 요소 처리
    }

    Java 8부터는 컬렉션 및 배열의 요소를 반복 처리하기 위해 스트림(Stream)을 사용할 수 있다.

    Stream<String> stream = list.stream();
    Stream.forEach( item -> //item 처리
        );

    List 컬렉션의 stream() 메소드로 Stream 객체를 얻고, forEach() 메소드로 요소를 어떻게 처리할지를 람다식으로 제공한다.

    Stream과 Iterator의 차이점

    1. 내부 반복자이므로 처리 속도가 빠르고 병렬 처리에 효율적이다.
    2. 람다식으로 다양한 요소 처리를 정의할 수 있다.
    3. 중간 처리와 최종 처리를 수행하도록 파이프 라인을 형성할 수 있다.

    17.2 내부 반복자

    • 외부 반복자: for 문과 Iterator는 컬렉션의 요소를 컬렉션 바깥쪽으로 반복해서 가져와 처리한다.
      • 컬렉션의 요소를 외부로 가져오는 코드와 처리하는 코드를 모두 개발자 코드가 가지고 있어야 한다.
    • 내부 반복자: 스트림은 요소 처리 방법을 컬렉션 내부로 주입시켜서 요소를 반복 처리한다.
      • 개발자 코드에서 제공한 데이터 처리 코드(람다식)를 가지고 컬렉션 내부에서 요소를 반복 처리한다.
      • 멀티 코어 CPU를 최대한 활용하기 위해 요소들을 분배시켜 병렬 작업을 할 수 있어 외부 반복자보다는 효율적으로 요소를 반복시킬 수 있다.

    17.3 중간 처리와 최종 처리

    • 스트림 파이프라인(pipelines)
      • 스트림은 하나 이상 연결될 수 있다.
      • 컬렉션의 오리지널 스트림 뒤에 필터링 중간 스트림이 연결될 수 있고, 그 뒤에 매핑 중간 스트림이 연결될 수 있다.

    오리지널 스트림과 집계 처리 사이의 중간 스트림들은 최종 처리를 위해 요소를 걸러내거나(필터링), 요소를 변환시키거나(매핑), 정렬하는 작업을 수행한다. 최종 처리는 중간 처리에서 정제된 요소들을 반복하거나, 집계(카운팅, 총합, 평균) 작업을 수행한다.

    // Student 스트림
    Stream<Student> studentStream = list.stream();
    
    // score 스트림
    IntStream scoreStream = studentStream.mapToInt(student -> student.getScore());	// Student 객체를 getScore() 메소드의 리턴값으로 매핑
    
    // 평균 계산
    double avg = scoreStream.average().getAsDouble();
    // 메소드 체이닝 패턴 이용하여 위 코드 간결하게 작성
    double avg = list.stream()
        .mapToInt(student -> student.getScore())
        .average()
        .getAsDouble();
    • 스트림 파이프라인으로 구성할 때 주의할 점
      • 파이프라인의 맨 끝에는 반드시 최종 처리 부분이 있어야 한다. 최종 처리가 없으면 오리지널 및 중간 처리 스트림은 동작하지 않는다.

    17.4 리소스로부터 스트림 얻기

    java.util.stream 패키지에는 다양한 스트림 인터페이스가 있다.

    • Stream: 객체 요소를 처리하는 스트림
    • IntStream, LongStream, DoubleStream: 각각 기본 타입인 int, long, double 요소를 처리하는 스트림

    ▲ BaseStream 인터페이스를 부모로 한 자식 인터페이스의 상속 관계

     

    리턴 타입 메소드(매개변수) 리소스
    Stream<T> java.util.Collection.stream()
    java.util.Collection.parallelStream()
    List 컬렉션
    Set 컬렉션
    Stream<T>
    IntStream
    LongStream
    DoubleStream
    Arrays.stream(T[]), Stream.of(T[])
    Arrays.stream(int[]), IntStream.of(int[])
    Arrays.stream(long[]), LongStream.of(long[])
    Arrays.stream(double[]), DoubleStream.of(double[])
    배열
    IntStream IntStream.range(int, int)
    IntStream.rangeClosed(int, int)
    int 범위
    LongStream LongStream.range(long, long)
    LongStream.rangeClosed(long, long)
    long 범위
    Stream<Path> Files.list(Path) 디렉토리
    Stream<String> Files.lines(Path, Charset) 텍스트 파일
    DoubleStream
    IntStream
    LongStream
    Random.doubles(...)
    Random.ints()
    Random.longs()
    랜덤 수

    컬렉션으로부터 스트림 얻기

    java.util.Collection 인터페이스는 스트림과 parallelStream() 메소드를 가지고 있기 때문에 자식 인터페이스인 List와 Set 인터페이스를 구현한 모든 컬렉션에서 객체 스트림을 얻을 수 있다.

    public class StreamExample {
        public static void main(String[] args) {
            // List 컬렉션 생성
            List<Product> list = new ArrayList<>();
            for(int i = 1; i <= 5; i++) {
                Product product = new Product("상품" + i, "멋진 회사", (int)(10000 * Math.random()));
                list.add(product);
            }
    
            // 객체 스트림 얻기
            Stream<Product> stream = list.stream();
            stream.forEach(p -> System.out.println(p));
        }
    }

    배열로부터 스트림 얻기

    java.util.Arrays 클래스를 이용하면 다양한 종류의 배열로부터 스트림을 얻을 수 있다.

    public class StreamExample {
        public static void main(String[] args) {
            String[] strArray = { "홍길동", "신용권", "김미나" };
            Stream<String> strStream = Arrays.stream(strArray);
            strStream.forEach(item -> System.out.print(item + ","));
            System.out.println();
    
            int[] intArray = { 1, 2, 3, 4, 5 };
            IntStream intStream = Arrays.stream(intArray);
            intStream.forEach(item -> System.out.print(item + ","));
            System.out.println();
        }
    }

    숫자 범위로부터 스트림 얻기

    IntStream 또는 LongStream의 정적 메소드인 range()와 rangeClosed() 메소드를 이용하면 특정 범위의 정수 스트림을 얻을 수 있다.

    • 첫 번째 매개값: 시작 수
    • 두 번째 매개값: 끝 수
    • 끝 수를 포함하지 않으면 range(), 포함하면 rangeClosed() 사용
    public class StreamExample {
        public static int sum;
    
        public static void main(String[] args) {
            IntStream stream = IntStream.rangeClosed(1, 100);
            stream.forEach(a -> sum += a);
            System.out.println("총합: " + sum);
        }
    }

    파일로부터 스트림 얻기

    java.nio.file.Files의 lines() 메소드를 이용하면 텍스트 파일의 행 단위 스트림을 얻을 수 있다. data.txt 파일을 한 행씩 읽고 상품 정보를 출력하기 위해 Files의 lines() 메소드를 이용한다.

    public class StreamExample {
        public static void main(String[] args) throws Exception {
            Path path = Paths.get(StreamExample.class.getResource("data.txt").toURI());	// data.txt 파일의 경로(Path) 객체 얻기
            Stream<String> stream = Files.lines(path, Charset.defaultCharset());	// Path로부터 파일을 열고 한 행씩 읽으면서 문자열 스트림 생성, 기본 UTF-8 문자셋으로 읽음
            stream.forEach(line -> System.out.println(line));
            stream.close();
        }
    }

    17.5 요소 걸러내기(필터링)

    • 필터링: 요소를 걸러내는 중간 처리 기능
      • distinct() 메소드: 요소의 중복 제거한다.
      • equals() 메소드: 객체 스트림(Stream)일 경우 리턴값이 true이면 동일한 요소로 판단한다. IntStream, LongStream, DoubleStream은 같은 값일 경우 중복을 제거한다.
      • filter() 메소드: 매개값으로 주어진 Predicate가 true를 리턴하는 요소만 필터링한다.
    리턴 타입 메소드(매개변수) 설명
    Stream
    IntStream
    LongStream
    DoubleStream
    distinct() - 중복 제거
    filter(Predicate<T>)
    filter(IntPredicate)
    filter(LongPredicate)
    filter(DoublePredicate)
    - 조건 필터링
    - 매개 타입은 요소 타입에 따른 함수형 인터페이스이므로 람다식으로 작성 가능

    ▲ 필터링 메소드의 종류

     

    인터페이스 추상 메소드 설명
    Predicate<T> boolean test(T t) 객체 T를 조사
    IntPredicate boolean test(int value) int 값을 조사
    LongPredicate boolean test(long value) long 값을 조사
    DoublePredicate boolean test(double value)  

    ▲ Predicate의 종류

     

    Predicate<T>를 람다식으로 표현

    T -> { ... return true }
    또는
    T -> true;	// return 문만 있을 경우 중괄호와 return 키워드 생략 가능

    17.6 요소 변환(매핑)

    • 매핑(mapping): 스트림의 요소를 다른 요소로 변환하는 중간 처리 기능
    • 매핑 메소드: mapXxx(), asDoubleStream(), asLongStream(), boxed(), flatMapXxx() 등

    요소를 다른 요소로 변환

    • mapXxx() 메소드: 요소를 다른 요소로 변환한 새로운 스트림을 리턴한다.

    리턴 타입 메소드(매개변수) 요소 → 변환 요소
    Stream<R> map(Function<T, R>) T → R
    IntStream
    LongStream
    DoubleStream
    mapToInt(ToIntFunction<T>) T → int
    mapToLong(ToLongFunction<T>) T → long
    mapToDouble(ToDoubleFunction<T>) T → double
    Stream<U> mapToObj(IntFunction<U>) int → U
    mapToObj(LongFunction<U>) long → U
    mapToObj(DoubleFunction<U>) double → U
    DoubleStream
    DoubleStream
    IntStream
    LongStream
    mapToDouble(IntToDoubleFunction) int → double
    mapToDouble(LongToDoubleFunction) long → double
    mapToInt(DoubleToIntFunction) double → int
    mapToLong(DoubleToLongFunction) double → long

    ▲ mapXxx() 메소드의 종류

     

    인터페이스 추상 메소드 매개값 → 리턴값
    Function<T, R> R apply(T t) T → R
    IntFunction<R> R apply(int value) int → R
    LongFunction<R> R apply(long value) long → R
    DoubleFunction<R> R apply(double value) double → R
    ToIntFunction<T> int applyAsInt(T value) T → int
    ToLongFunction<T> long applyAsLong(T value) T → long
    ToDoubleFunction<T> double applyAsDouble(T value) T → double
    IntToLongFunction long applyAsLong(int value) int → long
    IntToDoubleFunction double applyAsDouble(int value) int → double
    LongToIntFunction int applyAsInt(long value) long → int
    LongToDoubleFunction double applyAsDouble(long value) long → double
    DoubleToIntFunction int applyAsInt(double value) double → int
    DoubleToLongFunction long applyAsLong(double value) double → long

    ▲ Function의 종류

     

    모든 Function은 매개값을 리턴값으로 매핑(변환)하는 applyXxx() 메소드를 가지고 있다. Function<T,R>을 람다식으로 표현하면 다음과 같다.

    T -> { ... return R; }
    또는
    T -> r;		// return 문만 있을 경우 중괄호와 return 키워드 생략 가능

    기본 타입 간의 변환이거나 기본 타입 요소를 래퍼(Wrapper) 객체 요소로 변환하려면 다음과 같은 간편화 메소드를 사용할 수도 있다.

    리턴 타입 메소드(매개변수) 설명
    LongStream asLongStream() int → long
    DoubleStream asDoubleStream() int → double
    long → double
    Stream<Integer>
    Stream<Long>
    Stream<Double>
    boxed() int → Integer
    long → Long
    double → Double

    요소를 복수 개의 요소로 변환

    • flatMapXxx() 메소드: 하나의 요소를 복수 개의 요소들로 변환한 새로운 스트림을 리턴한다.
    리턴 타입 메소드(매개변수) 요소 → 변환 요소
    Stream<R> flatMap(Function<T, stream<R>>) T → Stream<R>
    DoubleStream flatMap(DoubleFunction<DoubleStream>) double → DoubleStream
    IntStream flatMap(IntFunction<IntStream>) int → IntStream
    LongStream flatMap(LongFunction<LongStream>) long → LongStream
    DoubleStream flatMapToDouble(Function<T, Doublestream>) T → DoubleStream
    IntStream flatMapToInt(Function<T, Intstream>) T → IntStream
    LongStream flatMapToLong(Function<T, LongStream>) T → LongStream

    ▲ flatMap() 메소드의 종류

    17.7 요소 정렬

    • 정렬: 요소를 오름차순 또는 내림차순으로 정렬하는 중간 처리 기능
    리턴 타입 메소드(매개변수) 설명
    Stream<T> sorted() Comparable 요소를 정렬한 새로운 스트림 생성
    Stream sorted(Comparator<T>) 요소를 Comparator에 따라 정렬한 새 스트림 생성
    DoubleStream sorted() double 요소를 오름차순으로 정렬
    IntStream sorted() int 요소를 오름차순으로 정렬
    LongStream sorted() long 요소를 오름차순으로 정렬

    ▲ 요소를 정렬하는 메소드

    Comparable 구현 객체의 정렬

    스트림의 요소가 객체일 경우 객체가 Comparable을 구현하고 있어야 sorted() 메소드를 사용할 수 있다. 그렇지 않다면 ClassCastException이 발생한다.

    public class Xxx implements Comparable {
        ...
    }
    List<Xxx> list = new ArrayList<>();
    Stream<Xxx> stream = list.stream();
    Stream<Xxx> orderedStream = stream.sorted();

    Comparator를 이용한 정렬

    요소 객체가 Comparable을 구현하고 있지 않으면, 비교자를 제공하여 요소를 정렬시킬 수 있다.

    • 비교자: Comparator 인터페이스를 구현한 객체
    sorted((o1, o2) -> { ... })​

     

    이 글은 『이것이 자바다』 책을 학습한 내용을 정리한 것입니다.
    Comments