Stream

kailyn·2024년 9월 9일

JAVA

목록 보기
16/17

1. Stream 개요

1. Stream 이란?

💡 스트림(Stream)은 자바 8 부터 추가된 기능으로, 컬렉션에 저장된 엘리먼트들을 하나씩 순회하면서 처리할 수 있는 기능이다. 람다식과 함께 사용할 수 있으며 컬렉션에 들어있는 데이터에 대한 처리를 간결하게 표현할 수 있다.

또한 내부 반복자를 사용하기 때문에 병렬처리가 쉽다는 장점이 있다.

2. Stream 이 필요한 이유

자바 8 이전에 배열 또는 컬렉션 다루는 방법은 for, foreach 를 사용하여 엘리먼트들을 꺼내서 다루는 방법이었다. 간단할 경우 상관 없지만 코드 양이 많아질수록 복잡성이 많이 올라가게 되었다.

스트림을 이용하면 배열 또는 컬렉션을 함수 여러 개를 사용해서 결과를 쉽게 얻을 수 있다.

스트림을 사용하면 람다식을 활용해 코드 양도 줄이고 간결하게 표현도 가능하다.

스트림의 또 하나의 장점은 병렬처리가 가능하다는 것이다.

3. Stream 특징

  1. 스트림은 원본을 변경하지 않는 읽기 전용이다.
  2. 스트림은 iterator 처럼 한번만 사용되고 사라진다. 필요하면 다시 스트림을 생성해야 한다.
  3. 최종 연산 전까지 중간 연산이 처리되지 않는다.
  4. 병렬 처리가 용이하다.

2. Stream 활용

스트림은 크게 생성, 가공, 결과 3가지로 구분된다.

  1. 생성 : 스트림 생성
  2. 가공 : 원하는 결과를 만들기 위한 필터링, 매핑 등의 작업
  3. 결과 : 최종 결과를 만들어 내는 작업
배열, 컬렉션 -> 스트림 생성 -> 매핑 -> 필터링 -> 결과
  • Stream을 활용한 코드
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    
    public class Application1 {
        public static void main(String[] args) {
    
            /* 수업목표. Stream에 대해 이해하고 활용할 수 있다. */
            /* 설명.
             *  Arrays.asList(): 매개변수로 요소들을 전달하면 List로 반환
             *  ArrayList<>(Collection 타입): Collection 타입을 ArrayList 객체로 생성할 때 쓰이는 생성자
            * */
            List<String> stringList = new ArrayList<>(Arrays.asList("hello", "world", "stream"));
    
            System.out.println("======== foreach");
            for(String str: stringList) {
                System.out.println(str);
            }
    
            System.out.println("======== stream");
            stringList.forEach(System.out::println);
            stringList.forEach(a -> System.out.println(a));
        }
    }
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    
    public class Application2 {
        public static void main(String[] args) {
    
            /* 수업목표. 스트림의 병렬처리에 대해 이해할 수 있다. */
            List<String> stringList =
                    new ArrayList<>(Arrays.asList("java", "oracle", "jdbc", "html", "css"));
    
            /* 설명. main 쓰레드에서 스트림을 사용하지 않고 확인 */
            System.out.println("========= foreach");
            for (String s: stringList) {
                System.out.println(s + " : " + Thread.currentThread().getName());
            }
    
            /* 설명. main 쓰레드에서 스트림을 사용하고 확인 */
            System.out.println("========= normal stream");
            stringList.forEach(Application2::print);                // stringList.'stream()'.forEach(Application2::print) 에서 stream() 생략
            stringList.forEach(s -> System.out.println(s + " : " + Thread.currentThread().getName()));
    
            /* 설명. 병렬 스트림 사용시 쓰레드 확인(속도가 상대적으로 더 몇 배 빠르다. 또한 기본 main쓰레드 외에 다른 쓰레드를 활용한다.) */
            System.out.println("========= parallel stream");
            stringList.parallelStream().forEach(Application2::print);
        }
    
        private static void print(String s) {
            System.out.println(s + " : " + Thread.currentThread().getName());
        }
    }

1. Stream 생성

자바에서는 자주 사용하는 배열과 컬렉션 객체에서 stream() 메소드를 지원한다.

이 메소드를 사용하면 스트림이 생성된다. 이 외 다양한 방법으로 스트림을 생성할 수도 있다.

1-1. 배열 스트림 생성

스트림을 생성할 배열을 생성한 후 stream() 사용

1-2. 컬렉션 스트림 생성

컬렉션도 마찬가지로 stream() 를 사용할 수 있다.

1-3. 비어 있는 스트림

비어있는 스트림도 생성할 수 있는데, 요소가 없을 때 사용하면 된다.

1-4. Stream.builder()

빌더를 사용해 직접 값을 넣을 수도 있다.

1-5. Stream().iterate()

iterate() 메소드를 사용하여 수열 형태로 스트림을 생성할 수 있다.

  • 배열, 컬렉션, builder, iterate를 활용한 코드
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Stream;
    
    public class Application1 {
        public static void main(String[] args) {
    
            /* 수업목표. 배열이나 컬렉션은 스트림을 이용할 수 있고 이를 이해해서 활용할 수 있다. */
            String[] sArr = new String[]{"java", "oracle", "jdbc"};
    
            /* 필기.
             *  Arrays.stream(배열): 배열 자료형을 stream 자료형으로 변환
            * */
            /* 설명. 1. 배열로 스트림 생성 */
            Stream<String> strStream1 = Arrays.stream(sArr);
            strStream1.forEach(System.out::println);
    //        strStream1.forEach(s -> System.out.println(s));
    
            System.out.println();           // 구분을 위한 단순 개행
    
            Stream<String> strStream2 = Arrays.stream(sArr, 0, 2);
            strStream2.forEach(System.out::println);
            System.out.println();
    
            /* 설명. 컬렉션으로 컬렉션 스트림 생성 */
            List<String> stringList = Arrays.asList("html", "css", "javascript");
    
            Stream<String> stringStream3 = stringList.stream();
            stringStream3.forEach(System.out::println);
            System.out.println();
    
            /* 설명. 3.
             *  Builder를 활용한 스트림 생성
             *  builder는 static<T>로 되어 있는 메소드이며, 호출시 타입 파라미터를 메소드 호출 방식으로 전달한다.
            * */
            Stream<String> builderStream = Stream.<String>builder()
                                                 .add("홍길동")
                                                 .add("유관순")
                                                 .add("윤봉길")
                                                 .build();
    
            builderStream.forEach(System.out::println);
    
            /* 설명. 4. iterator()를 활용하여 수열 형태의 스트림을 생성 */
            Stream<Integer> intStream =Stream.iterate(10, value -> value * 2)
                                             .limit(10);
            intStream.forEach(value -> System.out.println(value + " "));
        }
    }
    import java.util.Random;
    import java.util.regex.Pattern;
    import java.util.stream.IntStream;
    import java.util.stream.LongStream;
    import java.util.stream.Stream;
    
    public class Application2 {
        public static void main(String[] args) {
    
            /* 수업목표. 기본 타입 스트림 생성에 대해 이해하고 활용할 수 있다. */
            /* 필기.
             *  reange(시작값, 종료값): 시작값부터 1씩 증가하는 숫자로 종료값 직전까지 범위의 스트림 생성
             *  reangeClosed(시작값, 종료값): 시작값부터 1씩 증가하는 숫자로 종료값까지 포함한 스트림 생성
            * */
            IntStream intStream = IntStream.range(5, 10);
            intStream.forEach(value -> System.out.println(value + " "));
            System.out.println();
    
            LongStream longStream = LongStream.rangeClosed(5, 10);
            longStream.forEach(value -> System.out.println(value + " "));
            System.out.println();
    
            /* 필기.
             *  Wrapper 클래스 자료형의 스트림이 필요한 경우 boxing동 가능하다.
             *  doubles(개수): 난수를 활용한 DoubleStream을 개수만큼 생성하여 반환한다.
             *  boxed(): 기본 타입 스트림인 XXXStream을 박싱하여 Wrapper 타입의 Stream<XXX>로 반환한다.
            * */
            Stream<Double> doubleStream = new Random().doubles(5).boxed();
            doubleStream.forEach(value -> System.out.println(value + ""));
            System.out.println();
    
            /* 설명. 문자열을 split하여 stream으로 생성 */
            Stream<String> splitStream = Pattern.compile(", ").splitAsStream("html, css, javascript");
            splitStream.forEach(System.out::println);
    
        }
    }

1-6. 스트림 생성 - 기본 타입

스트림을 제네릭을 사용하지 않고 기본 타입으로 스트림을 생성할 수 있다. (range()rangeClosed() 는 범위 차이다.)

제네릭을 사용하지 않기 때문에 불필요한 오토박싱도 일어나지 않는다.

필요한 경우에는 boxed() 를 사용해 박싱을 할 수도 있다.

2. Stream 가공

스트림에 있는 데이터에 대해 내가 원하는 결과를 만들기 위해서 중간 처리 작업을 가공이라고 한다.

스트림에서는 데이터를 가공할 수 있는 메소드들을 제공하는데, 해당 메소드들은 Stream을 전달받아 Stream을 반환하므로 연속해서 메소드를 연결할 수 있다. 또한 Stream 가공할 때에 필터-맵(filter-map) 기반 API를 사용하기 때문에 지연연산을 통해 성능 최적화가 가능하다.

상세 역할메소드
필터링filter(), distinct()
변환map(), flatMap()
제한limit(), skip()
정렬sorted()
결과 확인peek()

2-1. Filter

필터(Filter)는 스트림에서 특정 데이터만 걸러내는 메소드이다. 매개변수로 받는 Predicateboolean을 리턴하는 함수형 인터페이스이다.

  • Filter를 활용한 코드
    import java.util.stream.IntStream;
    
    public class Application1 {
        public static void main(String[] args) {
    
            /* 수업목표. 스트림의 중계연산 중 하나인 filter에 대해 이해하고 사용할 수 있다. */
            /* 설명. 필터(filter)는 스트림에서 특정 데이터만 걸러내는 메소드이다. */
            IntStream intStream = IntStream.range(0, 10);
            intStream.filter(i -> (i % 2) == 0)
                     .forEach(i -> System.out.print(i + " "));
        }
    }

2-2. Map

map() 메소드는 값을 가공할 수 있는 람다식을 매개변수로 받는다.

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

스트림에 들어있는 데이터를 특정 람다식을 통해 데이터를 가공하고 새로운 스트림에 담아주는 역할을 한다.

  • Map을 활용한 코드
    import java.util.stream.IntStream;
    
    public class Application2 {
        public static void main(String[] args) {
    
            /* 수업목표. 스트림의 중계연산 중 하나인 map에 대해 이해하고 사용할 수 있다. */
            /* 설명. 맵(map)은 스트림에 들어있는 데이터를 람다식으로 가공하고 새로운 스트림에 담아주는 메소드이다. */
            IntStream intStream = IntStream.range(1, 10);
            intStream.filter(i -> i % 2 == 0)           // boolean 반환형으로 람다식 작성(predicate)
                     .map(i -> i * 5)                   // 요소를 반환하는 형태로 람다식 작성(operator)
                     .forEach(result -> System.out.print(result + " "));    // 반환형 없는 람다식 작성(consumer)
        }
    }

2-3. flatMap

flatMap()map() 과 비슷하지만 조금 더 복잡한 작업을 하는데 확인해보자.

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

flatMap() 는 중첩 구조를 한 단계 제거하고 단일 컬렉션으로 만들어준다. 이러한 작업을 플래트닝(flattening)이라고한다.

  • flatMap을 활용한 코드
    import java.util.Arrays;
    import java.util.Collection;
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class Applcation3 {
        public static void main(String[] args) {
    
            /* 수업목표. 스트림의 중계연산중 하나인 flatMap에 대해 이해하고 사용할 수 있다. */
            /* 필기.
             *  Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
             *  flatMap()는 중첩 구조를 한 단계 제거하고 단일 컬렉션으로 만들어 준다.
             *  이러한 작업을 플래트닝(flattening)이라고 한다.
            * */
            List<List<String>> list = Arrays.asList(
                    Arrays.asList("JAVA", "SPRING", "SPRINGBOOT"),
                    Arrays.asList("java", "spring", "springboot")
            );
            System.out.println("list = " + list);
    
            List<String> flatList = list.stream().flatMap(Collection::stream)
                                                 .collect(Collectors.toList());
            System.out.println("flatList = " + flatList);
        }
    }

2-4. Sorting

스트림에 있는 데이터들을 정렬할 때는 sorted() 메소드를 사용한다.

Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);

sorted() 은 인자가 없이도 호출이 가능한데, 인자가 없으면 오름차순으로 자동 정렬된다. 별도의 비교 로직을 구현하고 싶다면 comparator를 인자로 넘겨주면 된다.

  • sorting을 활용한 코드
    import java.util.List;
    import java.util.stream.Collectors;
    import java.util.stream.IntStream;
    
    public class Applicationn4 {
        public static void main(String[] args) {
    
            /* 수업목표. 스트림의 중계연산 중 하나인 sorted에 대해 이해하고 사용할 수 있다. */
            List<Integer> integerList = IntStream.of(5, 10, 99, 2, 1, 35)
                                                 .boxed()
                                                 .sorted()
                                                 .collect(Collectors.toList());
    
            System.out.println("정렬된 Integer List: " + integerList);
        }
    }

3. Stream 결과

가공된 스트림을 통해 이제는 결과를 만들어 내는 작업이 필요하다.

이 과정은 데이터를 필터링하고 가공한 뒤에 출력하기 위해서 진행하는 작업이다.

3-1. Calculating

스트림에서는 다양한 메소드들을 제공하는데, 먼저 최소/최대/총합/평균 등 과 같은 결과를 알아보자.

만약 스트림이 비어 있으면 count 와 sum은 0을 출력할 것이다.

  • calculating을 이용한 코드
    import java.util.OptionalInt;
    import java.util.stream.IntStream;
    
    public class Application1 {
        public static void main(String[] args) {
    
            /* 수업목표. 스트림의 최종연산 중 하나인 calculating에 대해 이해하고 사용할 수 있다. */
            long count = IntStream.range(1, 10).count();
            long sum = IntStream.range(1, 10).sum();
    
            System.out.println("count = " + count);
            System.out.println("sum = " + sum);
    
            /* 설명. OptionalInt는 결과 없음을 나타내야 하는 명확한 요구가 있는 메소드 반환 형식으로 사용하기 위한 타입이다. */
            OptionalInt max = IntStream.range(1, 10).max();
    //        OptionalInt max = IntStream.range(1, 1).max();   // OptionalInt.empty 형태로 나오고 기본자료형에서 존재하지 않음을 나타내기 위함을 알 수 있다.
            OptionalInt min = IntStream.range(1, 10).min();
    
            System.out.println("max = " + max);
            System.out.println("min = " + min);
    
            int oddSum = IntStream.range(1, 10)
                                  .filter(i -> i % 2 == 1)
                                  .sum();
    
            System.out.println("oddSum = " + oddSum);
        }
    }

3-2. Reduction

reduce() 라는 메소드는 스트림에 있는 데이터들의 총합을 계산해준다. reduce() 파라미터에 따라 3가지 종류가 있다.

  • Reduction을 활용한 코드
    // 1개 (accumulator)
    Optional<T> reduce(BinaryOperator<T> accumulator);
    
    // 2개 (identity)
    T reduce(T identity, BinaryOperator<T> accumulator);
    
    // 3개 (combiner)
    <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
    public static void main(String[] args) {
    
        // 인자가 1개일 경우
        OptionalInt reduceOneParam = IntStream.range(1, 4)     // 1, 2, 3
                                        .reduce((a, b) -> {
                                            return Integer.sum(a, b);
                                        });
    
        System.out.println("reduceOneParam = " + reduceOneParam.getAsInt());
        
        // 인자가 2개일 경우
        int reduceTwoParam = IntStream.range(1, 4)      // 1, 2, 3
                .reduce(100, Integer::sum);
    
        System.out.println("reduceTwoParam = " + reduceTwoParam);
        
        // 인자가 3개일 경우
        Integer reduceThreeParam = Stream.of(1, 2, 3, 4, 5, 6 ,7 ,8 ,9 ,10)
                .reduce(100,
                        Integer::sum,
                        (x, y) -> x + y
                );
    
        System.out.println("reduceThreeParam = " + reduceThreeParam);
    }
    • 실행결과
      reduceOneParam = 6
      reduceTwoParam = 106
      reduceThreeParam = 155

3-3. Collecting

collect() 는 Collector 타입을 받아서 처리하는데, 해당 메소드를 통해 컬렉션을 출력으로 받을 수 있다.

collect() 메소드는 Collector 객체에서 제공하는 정적 메소드를 사용할 수 있다.

public class Member {

    private String memberId;
    private String memberName;

    public Member() {
    }

    public Member(String memberId, String memberName) {
        this.memberId = memberId;
        this.memberName = memberName;
    }

    public String getMemberId() {
        return memberId;
    }

    public void setMemberId(String memberId) {
        this.memberId = memberId;
    }

    public String getMemberName() {
        return memberName;
    }

    public void setMemberName(String memberName) {
        this.memberName = memberName;
    }

    @Override
    public String toString() {
        return "Member{" +
                "memberId='" + memberId + '\'' +
                ", memberName='" + memberName + '\'' +
                '}';
    }
}
public static void main(String[] args) {

    List<Member> memberList = Arrays.asList(
            new Member("test01", "testName01"),
            new Member("test02", "testName02"),
            new Member("test03", "testName03")
    );
}
  1. Collectors.toList()

    스트림 작업 결과를 리스트로 반환해주는 메소드이다.

    public static void main(String[] args) {
    
        List<Member> memberList = Arrays.asList(
                new Member("test01", "testName01"),
                new Member("test02", "testName02"),
                new Member("test03", "testName03")
        );
        
        List<String> collectorCollection = memberList.stream()
                                                    .map(Member::getMemberName)
                                                    .collect(Collectors.toList());
    
        System.out.println("collectorCollection = " + collectorCollection); 
    }
    • 실행결과
      collectorCollection = [testName01, testName02, testName03]
  1. Collectors.joining()

    스트림의 작업 결과를 String 타입으로 이어 붙인다.

    세개의 인자를 받을 수 있다. 각 인자는 delimiter(구분자), prefix(맨 앞 문자), suffix(맨 뒤 문자) 이다.

    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class Application3 {
    
    	public static void main(String[] args) {
    
    		/* 설명. joining()는 하나의 요소들을 합쳐서 하나의 문자열로 바꿔주는 메소드이다. */
    	    String str = memberList
    	    				.stream()
    		                .map(Member::getMemberName)
    		                .collect(Collectors.joining());
    
    	    System.out.println("str = " + str);
    	    
    	
    	}
    }
    • 실행결과
      str = testName01testName02testName03
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class Application3 {
    
    	public static void main(String[] args) {
        
    	    String str2 = memberList
    			    		.stream()
    		                .map(Member::getMemberName)
    		                .collect(Collectors.joining(" || ", "**", "**"));
    
    	    System.out.println("str2 = " + str2);
    	}
    }
    • 실행결과
      str = **testName01 || testName02 || testName03**

3-4. Matching

Matching 은 Predicate 를 인자로 받아 조건을 만족하는 엘리먼트가 있는지 확인하고 boolean 으로 리턴해준다.

boolean anyMatch(Predicate<? super T> predicate);     // 하나라도 조건을 만족하는 값이 있는지
boolean allMatch(Predicate<? super T> predicate);     // 모든 조건을 만족하는지
boolean noneMatch(Predicate<? super T> predicate);    // 모든 조건을 만족하지 않는지
public static void main(String[] args) {

    List<String> stringList = Arrays.asList("Java", "Spring", "SpringBoot");

    boolean anyMatch = stringList.stream()
                                .anyMatch(str -> str.contains("p"));
    boolean allMatch = stringList.stream()
                                .allMatch(str -> str.length() > 4);
    boolean noneMatch = stringList.stream()
                                .noneMatch(str -> str.contains("c"));

    System.out.println("anyMatch = " + anyMatch);
    System.out.println("allMatch = " + allMatch);
    System.out.println("noneMatch = " + noneMatch);
}
  • 실행결과
    anyMatch = true
    allMatch = false
    noneMatch = true

문자열인지 확인: true
문자열인지 확인: false

profile
나는 할 수 있다...!

0개의 댓글