ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JAVA] 스트림(Stream) 생성 / 중간연산 map,flatMap / 최종연산 reduce, collect / Optional (2)
    DEV/JAVA 2024. 3. 26. 16:16

    1. 스트림 만들기

    • 컬렉션 : Stream<T> Collection.stream()
      • ex : Stream<Integer> intStream = list.stream();
    • 배열
      • Stream<T> Stream.of(val)
      • Stream<T> Arrays.stream(val)
        • ex 1 : Stream<String> strStream = Stream.of( "a","b","c" );
        • ex 2 : Stream<String> strStream = Arrays.stream(new String[]{ "a","b","c" });
    • 특정 범위의 정수 : 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환
      • IntStream.range(int begin, int end) : end가 범위에 포함 X
      • IntStream.rangeClosed(int begin, int end): end가 범위에 포함
        • ex 1 : IntStream intstr = IntStream.range(1,5); //1,2,3,4
        • ex 2 : IntStream intstr = IntStream.rangeClosed(1,5); //1,2,3,4,5
    • 임의의 수 : Random클래스에 난수들로 이루어진 스트림을 반환하는 클래스가 존재
      • 아래 메서드들에 매개변수를 주지 않으면, 스트림의 크기가 정해지지 않은 무한 스트림을 반환하므로 limit()도 같이 사용하여 스트림의 크기를 제한해야한다.
      • IntStream ints()
      • LongStream longs()
      • DoubleStream doubles()
    IntStream intStr = new Random().ints(); //무한스트림
    intStr.limit(5).forEach(System.out::println); //5개 요소만 출력
    
    IntStream intStr2 = new Random().ints(5); //유한 스트림 : 크기가 5인 난수 스트림 반환
    
    //지정된 범위의 난수를 발생시키는 스트림을 반환하는 것도 가능
    //단, end 는 범위에 포함되지 않음
    IntStream intStr3 = new Random().ints(1, 10)
    • 람다식 - iterate(), generate() : 람다식을 매개변수로 받아서, 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성
      • static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
        • seed값으로 지정된 값부터 시작해서 람다식 f에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복
        • ex ) Stream<Integer> evenStream = Stream.iterate(0, n→n+2); //0,2,4,6, …
      • static <T> Stream<T> generate(Supplier<T> s)
        • iterate()와 같이 람다식에 의해 계산되는 값을 요소로하는 무한 스트림을 생성해서 반환하지만, 다른점은 이전 결과를 이용해서 다음 요소를 계산하지않음
        • 매개변수 타입은 Supplier<T>이므로 매개변수가 없는 람다식만 허용
        • ex ) Stream<Double> randomStream = Stream.generate(Math::random);
      • 두 메서드에 의해 생성된 스트림은 기본형 스트림 타입의 참조변수로 다룰 수 없다.
    • 파일 : list()는 지정된 디렉토리에 있는 파일의 목록을 소스로하는 스트림을 생성해서 반환한다.
      • Stream<Path> Files.list(Path dir)
    • 빈 스트림 : 요소가 하나도 없는 비어있는 스트림을 생성할 수 도 있다.
      • Stream emptyStream = Stream.empty();
    • 두 스트림의 연결 : concat() 메서드를 사용하면 같은 타입의 두 스트림을 하나로 연결할 수 있다.
      • Stream<String> strs = Stream.concat(strs1, strs2);

     

    2. 스트림의 중간 연산

    : 다양한 연산자 중 자세한 설명이 필요한 map, flatMap만 다룸

     

    - map() : 스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑아내거나 특정 형태로 변환해야 할때 사용

    ex) File 스트림에서 파일의 이름만 뽑기

    Stream<File> fileStream = Stream.of(new File("ex1.java"),new File("ex2.java"),
    				new File("ex.bak"),new File("ex1.txt"));
    									
    Stream<String> fileNameStream = fileStream.map(File::getName);

    * map은 중간연산이므로 연산결과는 스트림이다.

    * 하나의 스트림에 여러번 적용할 수 있다.

     

    - mapToInt(), mapToLong(), mapToDouble() : 스트림의 요소를 숫자로 변환하는 경우 기본형 스트림으로 변환하는 것이 더 유용할 수 있음.

     

    * 기본형 스트림은 숫자를 다루는데 편리한 메서드를 제공

    • int sum() : 스트림 모든 요소의 총합
    • OptionalDouble average() : sum() / (double) count()
    • OptionalInt max() : 스트림 요소 중 가장 큰 값
    • OptionalInt min() :스트림 요소 중 가장 작은 값
    • 해당 메서드들은 최종 연산이기 때문에 호출 후에 스트림이 닫힌다.

    →  하나의 스트림에서 sum() , average()를 연속으로 호출 할 수 없음. 그래서 제공하는 summaryStatistics() 메서드

    IntSummaryStatistics stat = scoreStream.summaryStatistics();
    long totalCount = stat.getCount();
    long totalScore = stat.getSum();
    double avgScore = stat.getAverage();
    int min = stat.getMin();
    int max = stat.getMax();
    

     

    * IntStreamStream<T>로 변환 시 mapToObj() 사용

    * IntStreamStream<Integer>로 변환 시 boxed() 사용

     

    - flatMap() : Stream<T[]>Stream<T>로 변환

    Stream<String[]> strArrStream = Stream.of(
    					new String[]{"abc","def","ghi"},
    					new String[]{"ABC","GHI","JKLMN"}
    );
    
    //flatMap
    Stream<String> strStream = strArrStream.flatMap(Arrays::stream);
    
    //일반 map()사용시 스트림의 스트림 형태로 변환 됨
    Stream<Stream<String>> strStream2 = strArrStream.map(Arrays::stream);

     

    3. Optional<T>와 OptionalInt

    • Optional<T>는 지네릭 클래스로 ‘T타입의 객체’를 감싸는 래퍼클래스. 그래서 Optional타입의 객체에는 모든 타입의 참조변수를 담을 수 있다.
    • 최종연산의 결과를 Optional객체에 담아서 반환하는 것이다.

    → Optional에서 정의된 메서드를 통해서 널 체크를 위한 if문 없이도 NullPointerException이 발생하지 않는 간결하고 안전한 코드를 작성할 수 있게 해줌

     

     

    * Optional객체 생성하기

    • of() 또는 ofNullable() 사용
    • 참조변수의 값이 null일 가능성이 있으면 ofNullable() 사용 (NullPointerException 방지)
    • 초기화empty() 사용
    • 객체의 값을 가져올 때는 get(), 값이 null일 것을 대비해서 orElse()로 대체할 값 지정 가능
    • orElse()의 변형으로 람다식을 지정할 수 있는 orElseGet(), 예외를 발생시키는 orElseThrow() 존재
    • isPresent()는 Optional 객체에 값이 null이면 false, 아니면 true 반환
    • ifPresent()는 값이 있으면 주어진 람다식을 실행, 없으면 아무 일도 하지않음
    /* 생성 */
    String str = "abc";
    Optional<String> optVal = Optional.of(str);
    
    /* Null일 경우 */
    Optional<String> optVal = Optional.of(null); // NullPointerException 발생
    Optional<String> optVal = Optional.ofNullable(null);
    
    /* 초기화 */
    Optional<String> optVal = null; //권장하지않음
    Optional<String> optVal = Optional.empty();
    
    /* 값 가져오기 */
    String str1 = optVal.get(); //저장 값 반환. null이면 예외발생
    String str2 = optVal.orElse(""); //값이 null일때, "" 반환
    
    /* orElse() 변형 */
    String str3 = optVal.orElseGet(String::new); //String 객체 생성
    String str4 = optVal.orElseThrow(NullPointerExcetopn::new); //NPE 발생
    
    /* isPresent(), ifPresent() */
    //기존의 null 체크
    if(str != null) {
    	System.out.println(str);
    }
    
    //isPresent() 사용
    if(Optional.ofNullable(str).isPresent()) {
    	System.out.println(str);
    }
    
    //ifPresent() 사용
    Optional.ofNullable(str).ifPresent(System.out::println);

     

    * Stream클래스에 정의된 메서드 중에 Optional<T>를 반환하는 메서드

    • findAny()
    • findFirst()
    • max(Comparator)
    • min(Comparator)
    • reduce()

    → 기본형 스트림과 다르게, max()와 min()의 매개변수로 Comparator 보냄

     

    * 기본형 스트림에는 Optional도 기본형을 값으로 하는 OptionalInt, OptionalLong, OptionalDouble을 반환한다.

    아래는 InputStream에 정의된 메서드이다.

    • OptionalInt findAny()
    • OptionalInt findFirst()
    • OptionalInt reduce()
    • OptionalInt max()
    • OptionalInt min()
    • OptionalDouble average()

     

    * 기본형 int의 기본값은 0이다. 아무런 값도 갖지 않는 OptionalInt에 저장되는 값도 0이다.

    아래 두 객체는 같을까 ?

    OptionalInt opt = OptionalInt.of(0);
    OptionalInt opt2 = OptionalInt.empty();

    저장된 값이 없는 것과 0이 저장된 것은 isPresent로 구분이 가능하다.

    • opt.isPresent(); → true
    • opt2.isPresent(); → false
    • opt.getAsInt(); → 0
      • OptionalInt[0]
    • opt2.getAsInt(); → NoSuchElementException 발생
      • OptionalInt.empty
    • opt.equals(opt2); → false
    • Optional객체에 null을 저장하면 비어있는 것과 동일하게 취급한다.
    Optional<String> opt = Optional.ofNullable(null);
    Optional<String> opt2 = Optional.empty();
    
    System.out.println(opt.equals(opt2)); //true
    

     

    4. 스트림의 최종 연산

    : 다양한 연산자 중 자세한 설명이 필요한 reduce, collect만 다룸

     

    - reduce() : 스트림의 요소를 줄여나가면서 연산을 수행하고 최종결과를 반환. 처음 두 요소를 가지고 연산한 결과를 가지고 그 다음 요소와 연산한다. 이 과정에서 스트림 요소를 하나씩 소모하게 되며 모든 요소를 소모하게 되면 그 결과를 반환한다.

    • Optional<T> reduce(BinaryOperator<T> accumulator)
    • 초기 값을 갖는 reduce()도 있음 : 초기값과 스트림의 첫 번째 요소로 연산 시작
      • T reduce(T identity, BinaryOperator<T> accumulator)
      • U reduce(U identity, BiFunction<U,T,U> accumulator, BinaryOperator<U> combiner)
        →  combiner는 병렬 스트림에 의해 처리된 결과를 합칠 때 사용

    ex) 최종연산의  count(), sum() 등은 다음과 같이 내부적으로 reduce()를 이용해 만들어진것.

    //Stream<Integer>
    int count = intStream.reduce(0, (a,b) -> a + 1);
    int sum = intStream.reduce(0, (a,b) -> a + b);
    int max = intStream.reduce(Integer.MIN_VALUE, (a,b) -> a>b ? a:b);
    int min = intStream.reduce(Integer.MAX_VALUE, (a,b) -> a<b ? a:b);
    
    /* max와 min의 경우 초기값이 필요 없음.
    다음과 같이 매개변수가 하나짜리인 reduce를 사용하는 것이 나음.
    변수 intStream의 타입이 기본형 IntStream인 경우 사용 */
    OptionalInt max = intStream.reduce( (a,b) -> a > b ? a : b);
    OptionalInt min = intStream.reduce( (a,b) -> a < b ? a : b);
    
    //위 람다식을 메서드 참조로 변환
    OptionalInt max = intStream.reduce(Integer::max);
    OptionalInt min = intStream.reduce(Integer::min);

     

    - collect() : 스트림의 요소를 수집하는 연산. 리듀싱(reduce())과 유사.

    collect()가 스트림의 요소를 수집하려면 어떻게 수집할 것인가에 대한 방법이 정의되어있어야하는데 이것이 바로 컬렉터(collector)이다.

    collect() :  스트림의 최종연산. 매개변수로 컬렉터를 필요로 함.
    Collector : 인터페이스. 컬렉터는 이 인터페이스를 구현해야함.
    Collectors : 클래스. static 메서드로 미리 작성된 컬렉터를 제공.

     

    • Object collect(Collector collector)
    • Object collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)

     

    * 스트림을 컬렉션과 배열로 변환

    :  스트림의 모든 요소를 컬렉션에 수집하려면, Collectors 클래스의 toList() 같은 메서드를 사용하면된다.

    List나 Set이 아닌 특정 컬렉션을 지정하려면 toCollection()에 해당 컬렉션의 생성자 참조를 매개변수로 넣는다.

    - toList(), toSet(), toMap(), toCollection(), toArray()

    //toList() 사용하여 수집
    List<String> names = studentStream.map(Student::getName).collect(Collectors.toList());
    
    //toCollection으로 ArrayList 수집
    ArrayList<String> list = names.stream().collect(Collectors.toCollection(ArrayList::new));
    
    //toMap() : key - 학번, value - 학생 객체 지정
    Map<String,Student> map = studentStream.collect(Collectors.toMap(s->s.getHakbun(), s->s));
    
    /* toArray() : 스트림에 저장된 요소 T[]타입의 배열로 반환
    단, 해당 타입의 생성자 참조를 매개변수로 지정해줘야함. default : Object[] */
    Student[] stuNames = studentStream.toArray(Student[]::new); //OK
    Student[] stuNames = studentStream.toArray(); //Error
    Object[] stuNames = studentStream.toArray(); //OK

     

    * 통계

    : 최종연산에서 제공하는 통계 정보를 collect()를 사용해서 구하기. 주로 groupingBy()와 함께 사용할 때 필요

    - counting(), summingInt(), averagingInt(), maxBy(), minBy()

    // 최종연산 count() 메서드
    long count = studentStream.count();
    // collect()에서 제공하는 counting()
    long count = studentStream.collect(Collectors.counting());

     

    * 리듀싱

    : 리듀싱도 마찬가지로 collect()로 사용이 가능하다

    - reducing()

    IntStream intStream = new Random().ints(1,46).distinct().limit(6);
    
    // 최종연산 reduce()
    OptionalInt max = intStream.reduce(Integer::max);
    
    // collect()의 reducing()
    Optional<Integer> max = intStream.boxed().collect(Collectors.reducing(Integer::max));

     

    * 문자열 결합

    : 문자열 스트림의 모든 요소를 하나의 문자열로 연결해서 반환. 구분자, 접두사, 접미사 지정 가능

    스트림의 요소가 String이나 StringBuffer 처럼 CharSequence의 자손인 경우에만 결합이 가능

    - joining()

    String studentNames = studentStream.map(Student::getName).collect(Collectors.joining());
    String studentNames = studentStream.map(Student::getName).collect(Collectors.joining(","));
    String studentNames = studentStream.map(Student::getName).collect(Collectors.joining(",","[","]"));

     

    * 그룹화와 분할

    : 그룹화는 스트림의 요소를 특정기준으로 그룹화하는 것을 의미. 분할은 스트림의 요소를 두 가지, 지정된 조건에 일치하는 그룹과 일치하지 않는 그룹으로의 분할을 의미

    - groupingBy(), partitioningBy()

    - groupingBy()는 스트림의 요소를 Function으로, partitioningBy()는 Predicate로 분류

    - 스트림을 두 개의 그룹으로 나누어야한다면 partitioningBy(), 그 외는 groupingBy()

    - 그룹화와 분할의 결과는 Map에 담겨 반환됨

     

    ex ) Student.class : 학생 클래스 정의

    class Student {
        String name;
        boolean isMale;
        int hak;
        int ban;
        int score;
    
        Student(String name, boolean isMale, int hak, int ban, int score) {
            this.name = name;
            this.isMale = isMale;
            this.hak = hak;
            this.ban = ban;
            this.score = score;
        }
    
        public String getName() {
            return name;
        }
    
        public boolean isMale() {
            return isMale;
        }
    
        public int getHak() {
            return hak;
        }
    
        public int getBan() {
            return ban;
        }
    
        public int getScore() {
            return score;
        }
    
        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", isMale=" + isMale +
                    ", hak=" + hak +
                    ", ban=" + ban +
                    ", score=" + score +
                    '}';
        }
    }

     

    partitioningBy()에 의한 분류

    Student[] students = {
    		new Student("김자바", false, 1,2,200),
            new Student("이자바", true, 2,2,100),
            new Student("박자바", false, 3,3,300),
            new Student("최자바", true, 1,4,200)
    };
    
    
    
    //1. 분할 : 성별로 기본 분할 후 각각 리스트에 담기
    Map<Boolean, List<Student>> stuBySex = Stream.of(students).collect(partitioningBy(Student::isMale));
    
    List<Student> maleStudent = stuBySex.get(true);
    List<Student> femaleStudent = stuBySex.get(false);
    
    
    //2. 분할 + 통계 : counting() 메서드로 남학생,여학생 수 구하기
    Map<Boolean, Long> stuNumBySex = Stream.of(students).collect(partitioningBy(Student::isMale, counting()));
    System.out.println("남학생 수 : " + stuNumBySex.get(true));
    System.out.println("여학생 수 : " + stuNumBySex.get(false));
    
    //3. 분할 + 통계 : maxBy() 메서드로 남학생, 여학생 각각 성적 1등 구하기
    Map<Boolean, Optional<Student>> topScoreBySex = Stream.of(students).collect(partitioningBy(Student::isMale, maxBy(comparingInt(Student::getScore))));
    System.out.println("남학생 1등 : " + topScoreBySex.get(true));
    System.out.println("여학생 1등 : " + topScoreBySex.get(false));
    
    //3-1. 같은 결과 Optional이 아닌 Student객체 얻고 싶을 때 collectingAndThen()과 Optional::get 사용
    Map<Boolean, Student> topScoreBySex2 = Stream.of(students).collect(partitioningBy(Student::isMale, collectingAndThen(maxBy(comparingInt(Student::getScore)), Optional::get)));
    System.out.println("남학생 1등 : " + topScoreBySex2.get(true));
    System.out.println("여학생 1등 : " + topScoreBySex2.get(false));
    
    
    /*4. 다중 분할
    1) 성별로 분할 (남/여)
    2) 성적으로 분할 (불합격/합격)
    */
    Map<Boolean, Map<Boolean, List<Student>>> failedStuBySex = Stream.of(students).collect(partitioningBy(Student::isMale, // 1. 성별로 분할
            partitioningBy(s -> s.getScore() < 150))); //2. 성적으로 분할
    List<Student> failedMaleStudent = failedStuBySex.get(true).get(true); //남자 불합격자
    List<Student> failedFemaleStudent = failedStuBySex.get(false).get(true); //여자 불합격자

     

    결과

    남학생 수 : 2
    여학생 수 : 2
    남학생 1등 : Optional[Student{name='최자바', isMale=true, hak=1, ban=4, score=200}]
    여학생 1등 : Optional[Student{name='박자바', isMale=false, hak=3, ban=3, score=300}]
    남학생 1등 : Student{name='최자바', isMale=true, hak=1, ban=4, score=200}
    여학생 1등 : Student{name='박자바', isMale=false, hak=3, ban=3, score=300}

     

     

    groupingBy()에 의한 분류

    //1. 학생을 반별로 그룹화
    Map<Integer, List<Student>> stuByBan = Stream.of(students).collect(groupingBy(Student::getBan, toList())); //toList생략 가능
    
    System.out.println("===반별 그룹화===");
    for(List<Student> ban : stuByBan.values()) {
    	for(Student s : ban) {
        	System.out.println(s);
        }
    }
    
    /*2. 다중 그룹화
        1) 학년별 그룹화
        2) 반별 그룹화
    */
    Map<Integer, Map<Integer, List<Student>>> stuByHakAndBan = Stream.of(students).collect(groupingBy(Student::getHak,
            groupingBy(Student::getBan)));
    
    System.out.println("===다중 그룹화 1.학년 / 2.반 ===");
    for(Map<Integer, List<Student>> hak : stuByHakAndBan.values()) {
    	for(List<Student> ban : hak.values()) {
        		for(Student s : ban) {
            		System.out.println(s);
            	}
        	}
    }

     

    결과

    ===반별 그룹화===
    Student{name='김자바', isMale=false, hak=1, ban=2, score=200}
    Student{name='이자바', isMale=true, hak=2, ban=2, score=100}
    Student{name='박자바', isMale=false, hak=3, ban=3, score=300}
    Student{name='최자바', isMale=true, hak=1, ban=4, score=200}
    ===다중 그룹화 1.학년 / 2.반 ===
    Student{name='김자바', isMale=false, hak=1, ban=2, score=200}
    Student{name='최자바', isMale=true, hak=1, ban=4, score=200}
    Student{name='이자바', isMale=true, hak=2, ban=2, score=100}
    Student{name='박자바', isMale=false, hak=3, ban=3, score=300}

     

    댓글

Designed by Tistory.