Java8 In Action

[Part2] Java8 In action - Chapter5 - 3

신나게개발썰 2022. 7. 29. 13:37
반응형

해당 내용은 Java8 In Action 책을 요약 및 정리한 내용입니다.

좀 더 자세히 알고 싶으신 분들은 책을 사서 읽어보심을 추천드립니다.!

5.5 실전 연습

Entity

Trader.java

package Part2.Chapter5.Chapter5_5_5.entity;

public class Trader {
    private final String name;
    private final String city;

    public Trader(String name, String city) {
        this.name = name;
        this.city = city;
    }

    public String getName() {
        return this.name;
    }

    public String getCity() {
        return this.city;
    }

    public String toString() {
        return "Trader : " + this.name + " in " + this.city;
    }
}

Transaction.java

package Part2.Chapter5.Chapter5_5_5.entity;

public class Transaction {
    private final Trader trader;
    private final int year;
    private final int value;

    public Transaction(Trader trader, int year, int value) {
        this.trader = trader;
        this.year = year;
        this.value = value;        
    }

    public Trader getTrader() {
        return this.trader; 
    }

    public int getYear() {
        return this.year;
    }

    public int getValue() {
        return this.value;
    }

    public String toString() {
        return "{" + this.trader
            + ", year : " + this.year
            + ", value : " + this.value  
            + "}";
    }
}

Main

package Part2.Chapter5.Chapter5_5_5;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

import Part2.Chapter5.Chapter5_5_5.entity.Trader;
import Part2.Chapter5.Chapter5_5_5.entity.Transaction;

/*
 * 5.5 실전 연습
 */
public class Main_5_5 {

    static public void main(String[] args) {
        Trader raoul = new Trader("Raoul", "Cambridge");
        Trader mario = new Trader("Mario", "Milan");
        Trader alan = new Trader("Alan", "Cambridge");
        Trader brian = new Trader("Brian", "Cambridge");

        List<Transaction> transactions = Arrays.asList(
            new Transaction(brian, 2011, 300)
            , new Transaction(raoul, 2012, 1000)
            , new Transaction(raoul, 2011, 400)
            , new Transaction(mario, 2012, 700)
            , new Transaction(mario, 2012, 700)        
            , new Transaction(alan, 2012, 950));

        // 1. 2011년에 일어난 모든 트랜잭션을 찾아 값을 오른차순으로 정리하시오.
        System.out.println("1. 2011년에 일어난 모든 트랜잭션을 찾아 값을 오른차순으로 정리하시오.");
        transactions.stream()
            .filter((tran) -> tran.getYear() == 2011)
            .sorted((tran1, tran2) -> Integer.compare(tran1.getValue(), tran2.getValue()))
            .collect(Collectors.toList())
            .stream().forEach(tran -> System.out.println(tran.toString()) );

        System.out.println("[풀이] 1. 2011년에 일어난 모든 트랜잭션을 찾아 값을 오른차순으로 정리하시오.");
        transactions.stream()
            .filter((tran) -> tran.getYear() == 2011)
            .sorted(Comparator.comparing(Transaction::getValue))
            .collect(Collectors.toList())
            .stream().forEach(tran -> System.out.println(tran.toString()) );

        // 2. 거래자가 근무하는 모든 도시를 중복 없이 나열하시오.
        System.out.println("\\n2. 거래자가 근무하는 모든 도시를 중복 없이 나열하시오.");
        transactions.stream()            
            .map((tran) -> tran.getTrader().getCity())
            .distinct()
            .collect(Collectors.toList())
            .stream().forEach(tran -> System.out.println(tran.toString()) );

        System.out.println("[풀이] 2. 거래자가 근무하는 모든 도시를 중복 없이 나열하시오.");
        transactions.stream()
            .map(tran -> tran.getTrader().getCity())            
            .collect(Collectors.toSet())
            .stream().forEach(tran -> System.out.println(tran.toString()) );

        // 3. 케임브리지(cambridge)에서 근무하는 모든 거래자를 찾아서 이름순으로 정렬하시오.
        System.out.println("\\n3. 케임브리지(cambridge)에서 근무하는 모든 거래자를 찾아서 이름순으로 정렬하시오.");
        transactions.stream()
            .filter(transaction -> "cambridge".equals(transaction.getTrader().getCity().toLowerCase()) )
            .sorted((o1, o2) -> o1.getTrader().getName().compareTo(o2.getTrader().getName()))
            .map(tranName -> tranName.getTrader().getName())
            .distinct()
            .map(traderName -> {
                return transactions.stream()
                    .filter(tranNameFilter -> tranNameFilter.getTrader().getName().equals(traderName))
                    .sorted(Comparator.comparing(Transaction::getYear).reversed())
                    .limit(1)
                    .collect(Collectors.toList());
            })            
            .collect(Collectors.toList())
            .stream().forEach(System.out::println);

        System.out.println("[풀이] 3. 케임브리지(cambridge)에서 근무하는 모든 거래자를 찾아서 이름순으로 정렬하시오.");
        transactions.stream()
            .map(Transaction::getTrader)
            .filter(trader -> "Cambridge".equals(trader.getCity()))
            .distinct()
            .sorted(Comparator.comparing(Trader::getName))
            .collect(Collectors.toList())
            .stream().forEach(tran -> System.out.println(tran.toString()) );

        // 4. 모든 거래자의 이름을 알파벳순으로 정렬해서 반환하시오.
        System.out.println("\\n4. 모든 거래자의 이름을 알파벳순으로 정렬해서 반환하시오.");
        transactions.stream()
            .map(tran -> tran.getTrader().getName())
            .distinct()
            .sorted((o1, o2) -> o1.compareTo(o2))
            .map(traderName -> {
                return transactions.stream()
                    .filter(tranFilter -> traderName.equals(tranFilter.getTrader().getName()))
                    .sorted(Comparator.comparing(Transaction::getYear).reversed())
                    .limit(1)
                    .collect(Collectors.toList());
            })
            .collect(Collectors.toList())
            .forEach(System.out::println);

        System.out.println("[풀이-1] 4. 모든 거래자의 이름을 알파벳순으로 정렬해서 반환하시오.");
        System.out.println(transactions.stream()
            .map(tran -> tran.getTrader().getName())
            .distinct()
            .sorted()
            .reduce("", (n1, n2) -> n1 + n2) );
        /*
         * 각 반복 과정에서 모든 문자열을 반복적으로 연결해서 새로운 문자열 객체를 만든다.
         * 따라서 위 코드는 효율성이 부족하다. 이를 해결하기 위해 joining()을 이용하면 된다.
         */
        System.out.println("[풀이-2] 4. 모든 거래자의 이름을 알파벳순으로 정렬해서 반환하시오.");
        System.out.println(transactions.stream()
            .map(tran -> tran.getTrader().getName())
            .distinct()
            .sorted()
            .collect(Collectors.joining()) );        

        // 5. 밀라노(milan)에서 거래자가 있는가?
        System.out.println("\\n5. 밀라노(milan)에서 거래자가 있는가?");
        System.out.println(transactions.stream()
            .anyMatch((tran) -> "milan".equals(tran.getTrader().getCity().toLowerCase()) ));

        System.out.println("[풀이] 5. 밀라노(milan)에서 거래자가 있는가?");
        System.out.println(transactions.stream()
            .anyMatch((tran) -> "Milan".equals(tran.getTrader().getCity()) ));

        // 6. 케임브리지에 거주하는 모든 거래자의 모든 트랜잭션값을 출력하시오.
        System.out.println("\\n6. 케임브리지에 거주하는 모든 거래자의 모든 트랜잭션값을 출력하시오.");
        System.out.println(transactions.stream()
            .filter(tran -> "cambridge".equals(tran.getTrader().getCity().toLowerCase()) )
            .map(tran -> tran.getValue())
            .reduce(0, (tran1, tran2) -> tran1 + tran2));

        System.out.println("[풀이] 6. 케임브리지에 거주하는 모든 거래자의 모든 트랜잭션값을 출력하시오.");
        System.out.println(transactions.stream()
            .filter(tran -> "Cambridge".equals(tran.getTrader().getCity()))
            .map(Transaction::getValue)
            .reduce(0, Integer::sum));

        // 7. 전체 트랜잭션 중 최대값은 얼마인가?
        System.out.println("\\n7. 전체 트랜잭션 중 최대값은 얼마인가?");
        System.out.println(transactions.stream()
            .map(tran -> tran.getValue())
            .reduce(Integer::max).get() );

        System.out.println("[풀이] 7. 전체 트랜잭션 중 최대값은 얼마인가?");
        System.out.println(transactions.stream()
            .map(Transaction::getValue)
            .reduce(Integer::max).get() );

        // 8. 전체 트랜잭션 중 최소값은 얼마인가?
        System.out.println("\\n8. 전체 트랜잭션 중 최소값은 얼마인가?");
        System.out.println(transactions.stream()
            .map(tran -> tran.getValue())
            .reduce(Integer::min).get() );

        System.out.println("[풀이] 8. 전체 트랜잭션 중 최소값은 얼마인가?");
        System.out.println(transactions.stream()
            .reduce((t1, t2) -> 
                t1.getValue() < t2.getValue() ? t1 : t2
            ).get() );        
    }
}

5.6 숫자형 스트림

Entity

Dish.java

package Part2.Chapter5.Chapter5_5_6.entity;

public class Dish {
    private final String name;
    private final boolean vegetarian;
    private final int calories;
    private Type type;

    public Dish(String name, boolean vegetarian, int calories, Type type) {    
        this.name = name;
        this.vegetarian = vegetarian;
        this.calories = calories;
        this.type = type;
    }

    public String getName() {
        return this.name;
    }

    public int getCalories() {
        return this.calories;
    }

    public Type getType() {
        return this.type;
    }

    public boolean isVegetarian() {
        return vegetarian;
    }

    @Override
    public String toString() {
        return this.name;
    }

    public enum Type {
        MEAT, FISH, OTHER
    }
}

Main

package Part2.Chapter5.Chapter5_5_6;

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

import Part2.Chapter5.Chapter5_5_4.entity.Dish;

/*
 * 5.6 숫자형 스트림
 * 
 * int calories = menu.stream()
 *         .map(Dish::getCalories)
 *         .reduce(0, Integer::sum);
 * 
 * 위 코드는 메뉴의 칼로리 합계를 구하는 코드이다.
 * 그러나 사실 위 코드에는 박싱 비용이 숨어있다.
 * 내부적으로 합계를 계산하기 전에 Integer를 기본형으로 언박싱해야 한다.
 * 
 * 숫자 스트림을 효율적으로 처리할 수 있게 기본형 특화 스트림(primitive stream specialzation)을 제공한다. 
 */
public class Main_5_6 {

    public static void main(String[] args) {
        List<Dish> menu = Arrays.asList(
            new Dish("pork", false, 800, Dish.Type.MEAT)    
            , new Dish("beef", false, 700, Dish.Type.MEAT)                
            , new Dish("chicken", false, 400, Dish.Type.MEAT)
            , new Dish("french fries", true, 530, Dish.Type.OTHER)
            , new Dish("rice", true, 350, Dish.Type.OTHER)
            , new Dish("season fruit", true, 120, Dish.Type.OTHER)
            , new Dish("pizza", true, 550, Dish.Type.OTHER)
            , new Dish("prawns", false, 300, Dish.Type.FISH)
            , new Dish("salmon", false, 450, Dish.Type.FISH));

        /*
         * 5.6.1 기본형 특화 스트림
         * 
         * 자바 8에서는 3가지 기본형 특화 스트림을 제공한다.
         * 박싱 비용을 피할 수 있도록 'int에 특화된 IntStream', 'double에 특화된 DoubleStream', 'long 요소에 특화된 LongStream'
         * 을 제공한다. 
         * 각각의 인터페이스는 숫자 스트림의 합계를 계산할 수 있는 sum, 최대값 요소를 검색하는 max 같이 자주 사용하는 숫자 관련
         * 리듀싱 연산 수행 메서드를 제공한다.
         * 또한 필요할 때 다시 객체 스트림으로 복원하는 기능도 제공한다.
         * 특화 스트림은 오직 박싱과정에서 일어나는 효율성과 관련 있으며 스트림에 추가 기능을 제공하진 않는다는 사실을 기억하자.          
         */

        /*
         * mapToInt 메서드는 각 요리에서 모든 칼로리(Integer 형식)를 추출한 다음에 IntStream을 반환한다.
         * (Stream<Integer>가 아님)
         * 따라서 IntStream 인터페이스에서 제공하는 sum 메서드를 사용할 수 있다.
         * sum 메서드는 스트림이 비어있으면 기본값 0을 반환한다.
         * IntStream은 max, min, average 등 다양한 유틸리티 메서드를 지원한다.
         */
        System.out.println("5.6.1 기본형 특화 스트림 - mapToInt : " + menu.stream()
            .mapToInt(Dish::getCalories)
            .sum());

        /*
         * 객체 스트림으로 복원하기
         * 
         * boxed 메서드를 이용하면 특화 스트림을 일반 스트림으로 변환할 수 있다.
         */
        // 스트림을 숫자 스트림으로 변환.
        IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
        System.out.println("[IntStream] 객체 스트림으로 복원하기 : " + intStream);

        // 숫자 스트림에서 boxed 메서드를 이용해서 스트림으로 변환.
        Stream<Integer> streamInteger = menu.stream().mapToInt(Dish::getCalories).boxed();
        System.out.println("[Stream<Integer>] 객체 스트림으로 복원하기 : " + streamInteger);

        /*
         * 기본값 : OptionalInt
         * 
         * 합계 예제에서는 0이라는 기본값이 있어 별 문제가 없었다.
         * 하지만 IntStream에서 최대값을 찾을 때는 0이라는 기본값 때문에 잘못된 결과가 도출될 수 있다.
         * 스트림에 요소가 없는 상황과 실제 최대값이 0인 상황을 어떻게 구별할 수 있을까?
         * Optional을 Integer, String 등의 레퍼런스 형식으로 파라미터화할 수 있다.
         * 또한 OptionalInt, OptionalDouble, OptionalLong 세 가지 기본형 특화 스트림 버전도 제공한다. 
         */
        OptionalInt optionalInt = menu.stream().mapToInt(Dish::getCalories).max();
        // 최대 값이 없는 상황에 orElse 메서드를 이용해서 명시적으로 기본값을 정의 할 수 있다.
        System.out.println("기본값 : OptionalInt : " + optionalInt.orElse(1));

        /*
         * 5.6.2 숫자 범위
         * 
         * 특정 범위의 숫자를 이용해야 하는 상황이 자주 발생한다.
         * 예를 들어 1부터 100사이의 숫자를 생성하려한다고 가정하자.
         * 자바 8의 IntStream, LongStream에서는 range와 rangeClosed라는 두 가지 정적 메서드를 제공한다.
         * 두 메서드 모두 첫 번째 인수로 시작값을 두 번째 인수로 종료값을 갖는다.
         * 다만 range 메서드는 종료값이 결과에 포함되지 않는다.
         */
        System.out.println("5.6.2 숫자 범위 - range(0 ~ 10)");
        IntStream.range(0, 10).forEach(System.out::println);

        System.out.println("5.6.2 숫자 범위 - rangeClosed(0 ~ 10)");
        IntStream.rangeClosed(0, 10).forEach(System.out::println);

        /*
         * 5.6.3 숫자 스트림 활용 : 피타고라스 수
         */
        Stream<int[]> pythagoreanTriples = IntStream.rangeClosed(1, 100)
            .boxed()
            .flatMap(a -> {
                return IntStream.rangeClosed(a, 100)
                    .filter(b -> Math.sqrt(a * a + b * b) % 1 == 0)
                    .mapToObj(b -> new int[] {a, b, (int)Math.sqrt(a * a + b *b) })
                    .filter(t -> t[2] % 1 == 0);
            });

        System.out.println("5.6.2 숫자 스트림 활용 : 피타고라스 수");
        pythagoreanTriples
            .limit(5)
            .forEach(t -> System.out.println(t[0] + ", " + t[1] + ", " + t[2]));
    }

}

5.7 스트림 만들기

File

Main_5_7.txt

이거슨 첫 번째 라인 입니다.
이거슨 두 번재 라인 입니다.

Main

package Part2.Chapter5.Chapter5_5_7;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.function.IntSupplier;
import java.util.stream.IntStream;
import java.util.stream.Stream;

/*
 * 5.7 스트림 만들기
 */
public class Main_5_7 {

    public static void main(String[] args) throws UnsupportedEncodingException, URISyntaxException {
        /*
         * 5.7.1 값으로 스트림 만들기
         * 
         * 임의의 수를 인수로 받는 정적 메서드 Stream.of를 이용해서 스트림을 만들 수 있다.
         */
        Stream<String> streamString = Stream.of("Java 8", "Lambdas", "In ", "Action");
        System.out.println("5.7.1 값으로 스트림 만들기");
        streamString.map(String::toUpperCase).forEach(System.out::println);

        /*
         * 5.7.2 배열로 스트림 만들기
         * 
         * 배열을 인수로 받는 정적 메서드 Array.stream을 이용해서 스트림을 만들 수 있다.
         */
        int[] numbers = {2, 3, 5, 7, 11, 13};
        System.out.println("5.7.2 배열로 스트림 만들기 : " + Arrays.stream(numbers).sum());

        /*
         * 5.7.3 파일로 스트림 만들기
         *
         * 파일을 처리하는 등의 I/O 연산에 사용하는 자바의 NIO API(비블록 I/O)도 스트림 API를
         * 활용할 수 있도록 업데이트 되었다. 
         * java.nio.file.Files의 많은 정적 메서드가 스트림을 반환한다.
         */
        long uniquoWords = 0;        
        String fileName = URLDecoder.decode(Main_5_7.class.getResource("").getPath() + "Main_5_7.txt", "UTF-8")
                .replaceFirst("^/(.:/)", "$1");

        try (
            Stream<String> lines = 
            Files.lines(Paths.get(fileName), Charset.defaultCharset()) ) {

            uniquoWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
                .distinct().count();                

            System.out.println("5.7.3 파일로 스트림 만들기 : " + uniquoWords);
        } catch(IOException e) {
            e.printStackTrace();
        }

        /*
         * 5.7.4 함수로 무한 스트림 만들기
         * 
         * 스트림 API는 함수에서 스트림을 만들 수 있는 두 개의 정적 메서드 Stream.iterate와 Stream.generate를
         * 제공한다.
         * 두 연산을 이용해서 무한 스트림(infinite stream), 즉 고정된 컬렉션에서 고정된 크기의 스트림이 아닌
         * 크기가 고정되지 않는 스트림을 만들 수 있다.        
         * iterate와 generate에서 만든 스트림은 요청할때마다 주어진 함수를 이용해서 값을 만든다.
         * 보통 무한한 값을 출력하지 않도록 limit(n)함수를 함께 연결해서 사용한다.
         */

        /*
         * iterate
         * 
         * iterate는 요청할 때마다 값을 생산할 수 있으며 끝이 없으므로 무한 스트림(infinite stream)을 만든다.
         * 이러한 스트림을 언바운드 스트림(unbounded stream)이라고 표현한다.
         * 이런 특징이 스트림과 컬렉션의 가장 큰 차이점이다. 예제에서는 limit 메서드를 이용해서 스트림의 크기를
         * 명시적으로 처음 10개의 짝수로 제한한다. 그리고 최종 연산인 forEach를 호출해서 스트림을 소비하고
         * 개별 요소를 출력한다.
         */
        System.out.println("5.7.4 함수로 무한 스트림 만들기 - iterate");
        Stream.iterate(0, n -> n + 2)
            .limit(10)
            .forEach(System.out::println);

        /*
         * generate
         * 
         * generate도 요구할 때 값을 계산하는 무한 스트림을 만들 수 있다.
         * 하지만 iterate와 달리 생산된 각 값을 연속적으로 계산하지 않는다.
         */
        System.out.println("5.7.4 함수로 무한 스트림 만들기 - generate");
        Stream.generate(Math::random)
            .limit(5)
            .forEach(System.out::println);

        /* 우리가 사용한 공급자(supplier)는 상태가 없는 메서드, 즉 나중에 계산에 사용할
         * 어떤 값도 저장해두지 않는다.
         * 하지만 공급자에 꼭 상태가 없어야 하는 것은 아니다. 공급자가 상태를 저장한 다음에
         * 스트림의 다음 값을 만들 때 상태를 고칠 수 있다.
         * 여기서 중요한 점은 병렬 코드에서는 공급자에 상태가 있으면 안전하지 않다는 것이다.
         * 
         * 아래 코드는 IntSupplier 인스턴스를 만들었다.
         * 만들어진 객체는 기존 피보나치 요소와 두 인스턴스 변수에 어떤 피보나치 요소가
         * 들어있는지 추적하므로 "가변(mutable) 상태 객체"다.
         * getAsInt를 호출하면 객체 상태가 바뀌며 새로운 값을 생산한다.
         * iterate를 사용했을 때는 각 과정에서 새로운 값을 생성하면서도 기존 상태를 바꾸지 않는
         * 순수한 "불변(immutable)" 상태를 유지했다.
         * (iterate 피보나치는 Quiz_5_7에서 확인.)
         * 
         * 스트림을 병렬로 처리하면서 올바른 결과를 얻으려면 "불변 상태 기법"을 고수해야 한다.
         */
        IntSupplier fib = new IntSupplier() {
            /*
             * 추적 대상 변수들 - 위 설명에서 나온 "상태"를 뜻하기도 한다.
             */
            private int prev = 0;
            private int curr = 1;

            @Override
            public int getAsInt() {
                int oldPrev = this.prev;
                int nextVal = this.prev + this.curr;

                this.prev = this.curr;
                this.curr = nextVal;

                return oldPrev;
            }
        };

        System.out.println("5.7.4 함수로 무한 스트림 만들기 - generate");
        IntStream.generate(fib).limit(10).forEach(t -> System.out.print(t + ", "));

    }

}

Quiz_5_7

Quiz_5_7.java

package Part2.Chapter5.Chapter5_5_7.Quiz;

import java.util.stream.Stream;

public class Quiz_5_7 {

    public static void main(String[] args) {
        /*
         * 퀴즈 5-4 피보나치수열 집합
         */
        Stream.iterate(new int[] {0, 1}, t -> new int[] {t[1], t[0] + t[1]})
            .limit(20)
            .map(t -> t[0])
            .forEach(t -> System.out.print(t + ", ")); 
    }

}

스트림 활용

  • 데이터를 어떻게 처리할지는 스트림 API가 관리하므로 편리하게 데이터 관련 작업을 할 수 있다.
  • 스트림 API 내부적으로 다양한 최적화가 이루어질 수 있다.
  • 내부 반복뿐 아니라 코드를 병렬로 실행할지 여부도 결정할 수 있다. 이러한 일은 순차적인 반복을 단일 스레드로 구현했기 때문에 외부 반복으로는 불가능하다.

요약

  • 스트림 API를 이용하면 복잡한 데이터 처리 질의를 표현할 수 있다.
  • filter, distinct, skip, limit 메서드로 스트림을 필터링하거나 자를 수 있다.
  • map, flatMap 메서드로 스트림의 요소를 추출하거나 변환할 수 있다.
  • findFirst, findAny 메서드로 스트림의 요소를 검색할 수 있다. allMatch, noneMatch, anyMatch 메서드를 이용해서 주어진 프레디케이트와 일치하는 요소를 스트림에서 검색할 수 있다.
  • 이들 메서드는 쇼트서킷(short-circuit), 즉 결과를 찾는 즉시 반환하며, 전체 스트림을 처리하지는 않는다.
  • reduce 메서드로 스트림의 모든 요소를 반복 조합하며 값을 도출할 수 있다. 예를 들어 reduce로 스트림의 최대값이나 모든 요소의 합계를 계산할 수 있다.
  • filter, map 등은 상태를 저장하지 않는 "상태 없는 연산(stateless operation)"이다 reduce 같은 연산은 값을 계산하는 데 필요한 상태를 저장한다. sorted, distinct 등의 메서드는 새로운 스트림을 반환하기 앞서 스트림의 모든 요소를 버퍼에 저장해야 한다. 이런 메서드를 "상태 있는 연산(stateful operation)"이라고 부른다.
  • IntStream, DoubleStream, LongStream은 기본형 특화 스트림이다. 이들 연산은 각각의 기본형에 맞게 특화되어 있다.
  • 컬렉션뿐 아니라, 값, 배열, 파일, iterate와 generate 같은 메서드로도 스트림을 만들 수 있다.
  • 크기가 정해지지 않는 스트림을 무한 스트림이라고 한다.
반응형