본문 바로가기

Java의 정석 : 3rd Edition

[Java의 정석 - 연습문제] Chapter14. 람다와 스트림(Lambda & Stream)

Java의 정석 : 3rd Edition, 2016을 개인 학습용으로 정리한 내용입니다.
"ppt 파일 자료, 연습문제"를 학습 용도로 배포할 수 있음을 고지하셨습니다.
저자님께 감사드립니다.

 

 

 

 

[14-1] 메서드를 람다식으로 변환하여 아래의 표를 완성하시오.

메서드 람다식
int max(int a, int b) {
    return a > b ? a : b;
}
(int a, int b) -> a > b ? a : b
int printVar(String name, int i) {
    System.out.println(name + "=" + i);
}
(a)
int square(int x) {
    return x * x;
}
(b)
int roll() {
    return (int)(Math.random() * 6);
}
(c)
int sumArr(int[] arr) {
    int sum = 0;
    for(int i : arr)
        sum += i;
    return sum;
}
(d)
int[] emptyArr() {
    return new int[]{};
}
(e)

 

답 :

(a) : (name, i) -> System.out.println(name + "=" + i)

(b) : x -> x * x

(c) : () -> (int) (Math.random() * 6)

(d) : (int[] arr) -> {
            int sum = 0;
            for(int i : arr)
                sum += i;
            return sum;
        }

(e) : () -> new int[]{}

 

메서드의 이름, 반환 타입 제거 후 {}앞에 ->를 추가한다(문장이 아닌 식이므로 ;를 붙이지 않는다.)

a : 람다식에 선언된 매개 변수의 타입은 추론 가능한 경우 생략 가능(어느 하나의 타입만 생략하는건 허용되지 않음)
b : 반환 값이 있다면 return문 생략 가능, 선언된 매개 변수가 하나뿐인 경우 () 생략 가능
c, e : 매개 변수가 없는 경우에는 빈 괄호()를 적어줘야 한다.

d : {}안의 문장이 한 문장인 경우 {} 생략 가능

 

 

 

 

[14-2] 람다식을 메서드 참조로 변환하여 표를 완성하시오. (변환이 불가능한 경우, '변환불가'라고 적어야함.)

람다식 메서드 참조
(String s) -> s.length()  
() -> new int[]{}  
arr -> Arrays.stream(arr)  
(String str1, String str2) -> str1.equals(str2)  
(a, b) -> Integer.compare(a, b)  
(String kind, int num) -> new Card(kind, num)  
(x) -> System.out.println(x)  
() -> Math.random()  
(str) -> str.toUpperCase()  
() -> new NullPointerException()  
(Optional opt) -> opt.get()  
(StringBuffer sb, String s) -> sb.append(s)  
(String s) -> System.out.println(s)  

 

답 :

1) String::length
2) int[]::new
3) Arrays::stream
4) String::equals
5) Integer::compare
6) Card::new
7) System.out::println
8) Math::random
9) String::toUpperCase
10) NullPointerException::new
11) Optional::get
12) StringBuffer::append
13) System.out::println

 

 

 

 

[14-3] 아래의 괄호안에 알맞은 함수형 인터페이스는?

(?) f;  // 함수형 인터페이스 타입의 참조 변수 f를 선언.
f = (int a, int b) -> a > b ? a : b;

a. Function

b. BiFunction

c. Predicate

d. IntBinaryOperator

e. IntFunction

 

답 : d

매개 변수가 두 개(int a, int b)라 하나의 매개 변수만 정의되어 있는 Function, Predicate, IntFunction는 적합하지 않다.

BiFunction는 두 개의 매개 변수를 갖지만, 기본형을 사용할 수 없다.(T 타입)

두 개의 매개 변수를 갖고, 기본형을 사용하는 IntBinaryOperator를 사용해야 한다.

+) 매개 변수 타입을 생략하면, BiFunction<Integer, Integer, Integer> or BinaryOperator<Integer>로도 적용 가능하다.

import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.IntBinaryOperator;

public class Exercise14_3 {
    public static void main(String[] args) {
        // int
        IntBinaryOperator f = (int a, int b) -> a > b ? a : b;
        System.out.println("1 > 2 ? " + f.applyAsInt(1, 2));

        // Integer
        BinaryOperator<Integer> f1 = (a, b) -> a > b ? a : b;
        System.out.println("1 > 2 ? " + f1.apply(1, 2));

        BiFunction<Integer, Integer, Integer> f2 = (a, b) -> a > b ? a : b;
        System.out.println("1 > 2 ? " + f2.apply(1, 2));
    }
}
@FunctionalInterface
public interface IntFunction<R> {
    R apply(int value);
}
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}
@FunctionalInterface
public interface IntBinaryOperator {
    int applyAsInt(int left, int right);
}

 

 

 

 

[14-4] 문자열 배열 strArr의 모든 문자열의 길이를 더한 결과를 출력하시오.

String[] strArr = {"aaa", "bb", "c", "dddd"};
// sum : 10

 

답 :

import java.util.stream.IntStream;
import java.util.stream.Stream;

class Exercise14_4 {
    public static void main(String[] args) {
        String[] strArr = {"aaa", "bb", "c", "dddd"};
        Stream<String> strStream = Stream.of(strArr);  // String[] -> Stream<String>

        // mapToInt() : Stream<String> -> IntStream
        IntStream intStream = strStream.mapToInt(String::length);  // 메서드 참조
        // IntStream intStream = strStream.mapToInt(s -> s.length());  // 람다식
        
        int sum = intStream.sum();
        // int sum = strStream.mapToInt(String::length).sum();  // 한 문장으로 합친 것
        System.out.println("sum : " + sum);
    }
}
기본형 스트림으로 각 요소의 합을 구하는 방법
* reduce() : 초기값(identity)과 연산 방법(BinaryOperator)을 지정해줘야 함(복잡)
* sum() : 호출만 하면 스트림의 각 요소에 대한 합계를 구할 수 있음(편리)

1) Stream.of()를 이용하여 배열 or 컬렉션을 스트림으로 변환한다. String[] Stream<String>

    Stream은 데이터를 추상화하므로 어떤 데이터(배열 or 컬렉션 or 파일)든 같은 방식으로 읽고 쓸 수 있다.

2) mapToInt()를 통해 스트림을 기본형 스트림으로 변환한다. Stream<String>  IntStream

    문자열 스트림을 각 문자열의 길이를 갖는 정수 스트림으로 변환한다.

3) 기본형 스트림이 제공하는 sum()를 통해 스트림의 각 요소에 대한 합계를 구한다.

    기본형 스트림(IntStream)은 숫자를 다루는데 편리한 메서드(sum(), average(), max(), min())들을 제공한다.

기본형 스트림으로 변환하는 이유?
map()를 이용하면 스트림에 저장된 데이터를 원하는 필드만 뽑아내거나 특정 형태로 변환할 수 있다.
이를 통해 합계를 계산하려면 내부적으로 언박싱(Integer → int), 박싱(int → Integer) 과정을 거쳐야 함(비 효율적).
import java.util.stream.Stream;

class Exercise14_4_1 {
    public static void main(String[] args) {
        String[] strArr = {"aaa", "bb", "c", "dddd"};
        Stream<String> strStream = Stream.of(strArr);  // String[] -> Stream<String>

        // map() : Stream<String> -> Stream<Integer>
        Stream<Integer> integerStream = strStream.map(String::length);  // 메서드 참조
        // Stream<Integer> integerStream = strStream.map(s -> s.length());  // 람다식

        Integer sum = integerStream.reduce(0, Integer::sum);  // 메서드 참조
        // Integer sum = integerStream.reduce(0, (a, b) -> Integer.sum(a, b));  // 람다식

        System.out.println("sum : " + sum);
    }
}​

 

reduce()를 사용하는 방법

기본형 스트림에 reduce()를 적용하여 합계를 구하는 것도 가능하다.

import java.util.stream.IntStream;
import java.util.stream.Stream;

class Exercise14_4_2 {
    public static void main(String[] args) {
        String[] strArr = {"aaa", "bb", "c", "dddd"};
        Stream<String> strStream = Stream.of(strArr);  // String[] -> Stream<String>

        // mapToInt() : Stream<String> -> IntStream
        IntStream intStream = strStream.mapToInt(String::length);  // 메서드 참조

        int sum = intStream.reduce(0, (a, b) -> a + b);

        System.out.println("sum : " + sum);
    }
}

 

 

 

 

[14-5] 문자열 배열 strArr의 문자열 중에서 가장 긴 것의 길이를 출력하시오.

String[] strArr = {"aaa", "bb", "c", "dddd"};
// 4

 

답 :

import java.util.stream.IntStream;
import java.util.stream.Stream;

class Exercise14_5 {
    public static void main(String[] args) {
        String[] strArr = {"aaa", "bb", "c", "dddd"};
        Stream<String> strStream = Stream.of(strArr);  // String[] -> Stream<String>

        IntStream intStream = strStream.mapToInt(String::length);  // Stream<String> -> IntStream
        int max = intStream.max().getAsInt();
        System.out.println("max : " + max);
    }
}

기본형 스트림이 제공하는 max()를 이용하면 길이가 가장 긴 요소(가장 큰 값)를 쉽게 구할 수 있다.

1) 배열을 기본형 스트림으로 변환한다. String[]Stream<String>IntStream

2) IntStream.max()를 통해 길이가 가장 긴 요소(가장 큰 값)을 max에 저장한다.

    max(), int()는 최종 연산의 결과를 Optional객체(T타입 or 기본형을 감싸는 래퍼 클래스)에 담아 반환한다.

    * 두 메서드는 초기값이 필요없는 대신 NullPointerException가 발생할 가능성이 있다.

    * Optional객체에 저장하면 NullPointerException가 발생하지 않는 보다 간결하고 안전한 코드를 작성할 수 있게 된다.

3) OptionalInt에 저장된 값을 꺼낼 때는 getAsInt()를 사용해야 한다.

    getAsInt()를 통해 OptionalInt에 저장된 값을 꺼내야 int 타입에 저장할 수 있다.

 

해설 : 스트림에 저장된 각 요소를 역순으로 정렬한 다음 가장 긴 문자열의 길이를 출력한다.

import java.util.Comparator;
import java.util.stream.Stream;

class Exercise14_5_1 {
    public static void main(String[] args) {
        String[] strArr = {"aaa", "bb", "c", "dddd"};
        Stream<String> strStream = Stream.of(strArr);  // String[] -> Stream<String>

        strStream.map(String::length)  // strStream.map(s -> s.length())
            .sorted(Comparator.reverseOrder())       // 역순 정렬(문자열 길이)
            .limit(1).forEach(System.out::println);  // 제일 긴 문자열의 길이 출력
    }
}

 

가장 긴 문자열을 길이가 아닌 문자열 자체로 출력하는 방법

sorted()에 정렬 기준(문자열 길이, 역순)을 제공한 다음 요소의 개수를 제한(limit(1))하면 가장 긴 문자열이 출력된다.

import java.util.Comparator;
import java.util.stream.Stream;

class Exercise14_5_2 {
    public static void main(String[] args) {
        String[] strArr = {"aaa", "bb", "c", "dddd"};
        Stream<String> strStream = Stream.of(strArr);  // String[] -> Stream<String>

        strStream
            .sorted(Comparator.comparingInt(String::length).reversed())
            .limit(1)
            .forEach(System.out::println);
    }
}

 

 

 

 

[14-6] 임의의 로또번호(1~45)를 정렬해서 출력하시오.

1
20
25
33
35
42

 

답 :

import java.util.Random;
import java.util.stream.IntStream;

class Exercise14_6 {
    public static void main(String[] args) {
        // IntStream intStream = new Random().ints(6, 1, 46);  // 6 : 스트림 크기 지정
        IntStream intStream = new Random().ints(1, 46);  // 범위 지정
        intStream
            .distinct()  // 중복 제거
            .limit(6)    // 개수 제한
            .sorted()    // 정렬
            .forEach(System.out::println);  // 출력
    }
}
Random클래스에는 난수 스트림을 타입별로 반환하는 인스턴스 변수 메서드(ints()longs()doubles())가 존재한다.
* 무한 스트림 : ints() 크기가 정해지지 않은 무한 스트림이 생성되므로 limit()를 같이 사용해야 한다.
* 유한 스트림 : ints(5) 매개 변수로 스트림의 크기를 지정했으므로 유한 스트림이 생성된다(limit() 불필요).

1) Random.ints()를 이용하여 난수 스트림을 생성한다.

    Random.ints()는 지정된 범위 내의 임의의 정수를 요소로 하는 무한 스트림을 반환한다.

    스트림의 범위 : Integer.MIN_VALUE <= ints() <= Integer.MAX_VALUE

    end로 지정한 값은 범위에 포함되지 않으므로 45를 범위에 포함시키려면 46을 지정해야 한다.

2) 매개 변수로 스트림의 크기를 지정하지 않았으므로 limit()를 통해 출력할 스트림의 개수(크기)를 지정해줘야 한다.

    스트림의 크기를 지정하지 않으면 무한 스트림이 생성된다.

    매개 변수로 스트림의 크기를 지정하면 limit()를 사용하지 않아도 된다.

 

 

 

 

[14-7] 두 개의 주사위를 굴려서 나온 눈의 합이 6인 경우를 모두 출력하시오.

[Hint] 배열을 사용하시오.

[1, 5]
[2, 4]
[3, 3]
[4, 2]
[5, 1]

 

답 :

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

class Exercise14_7 {
    public static void main(String[] args) {
        // rangeClosed() : 지정된 범위를 갖는 연속된 정수 스트림 생성(end 포함)
        // boxed() : IntStream -> Stream<Integer>
        Stream<Integer> dice = IntStream.rangeClosed(1, 6).boxed();  // 주사위 생성

        // map() : Stream<Integer> -> Stream<Stream<int[]>>
        // flatMap() : Stream<Integer> -> Stream<int[]>
        // dice.map(i -> Stream.of(1, 2, 3, 4, 5, 6).map(i2 -> new int[]{i, i2}))
        dice.flatMap(i -> Stream.of(1, 2, 3, 4, 5, 6).map(i2 -> new int[]{i, i2}))
            .filter(iArr -> iArr[0] + iArr[1] == 6)
            .forEach(iArr -> System.out.print(Arrays.toString(iArr)));
    }
}

1) 1~6를 범위로 갖는 스트림(주사위)을 생성한다.

  1-1) IntStream or LongStream에는 지정된 범위를 갖는 정수 스트림을 생성하는 메서드가 정의되어 있다.

          range(int begin, int end) : 종료 값(end)을 포함하지 않은 정수 스트림을 생성한다.

          rangeClosed(int begin, int end) : 종료 값(end)을 포함하는 정수 스트림을 생성한다.

  1-2) 기본형 스트림에 배열을 저장할 수 없기 때문에 객체 스트림으로 변환해야 한다.

          boxed() : 기본형 스트림(IntStream)을 객체 스트림(Stream<Integer>)으로 변환한다.

          mapToInt() : Stream<Integer>IntStream  (객체 스트림 → 기본형 스트림)
          boxed()      : IntStream Stream<Integer>  (기본형 스트림 → 객체 스트림)

2) flatMap()를 통해 두 주사위에 대한 작업(필터링, 출력)을 수행한다.

    flatMap() : dice의 요소 IntegerStream<int[]>로 변환하여 결과로 Stream<int[]>를 반환한다(평면화).

    map()      : dice의 요소 IntegerStream<int[]>로 변환하여 결과로 Stream<Stream<int[]>>를 반환한다(중첩).

flatMap()의 변환 과정
flatMap()의 변환 과정
map()의 변환 과정
map()의 변환 과정

  2-1) Stream.of() : 가변 매개 변수(variable parameter)를 전달 받아 스트림을 생성한다.

          i : IntStream.rangeClosed(1, 6) (dice의 각 요소)

          i2 : Stream.of(1, 2, 3, 4, 5, 6) (가변 인자로 전달한 스트림의 각 요소)

  2-2) map()를 통해 ii2를 배열 [i, i2] 형태로 변환한다.

         map() : 스트림에 저장된 데이터를 원하는 값만 뽑아내거나 특정 형태로 변환해야 할 때 사용한다.

  2-3) filter()를 통해 [i, i2] 형태로 변환된 두 주사위의 합이 6인지 확인한다.

         filter() : 조건에 맞는 요소로 구성된 새로운 스트림을 반환한다(스트림 반환 = 중간 연산 = 여러번 사용 가능).

                     조건에 맞지 않는 요소를 걸러내는 역할.

  2-4) forEach()를 통해 두 주사위의 합이 6인 배열의 요소(Arrays.toString())를 출력한다.

         forEach() : 스트림의 각 요소를 소비하며 람다식(동작)을 수행한다(최종 연산).

                           반환 타입이 void이므로 주로 스트림의 요소를 출력하는 용도로 사용한다.

 

가변 인자로 스트림을 생성하는 이유(미 해결)
두 개의 주사위 중 하나는 왜 가변 인자로 생성해야 할까?
Stream<Integer>(dice2)로 map() → filter() → forEach()를 한 번 수행하고 나면 스트림이 닫히게 된다.
forEach()는 최종 연산이라 단 한번만 수행되기 때문이다.
아직 정확한 이유를 파악하지 못했다.
Q. 최종 연산을 두 번 호출한게 아닌데 왜 스트림이 닫히게 되는 거지?
Q. Stream<Integer>에 저장된 요소를 스트림 내부에서 꺼내올 수 없기 때문일까?
import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.Stream;

class Exercise14_7_1 {
    public static void main(String[] args) {
        Stream<Integer> dice = IntStream.rangeClosed(1, 6).boxed();  // 주사위 생성
        Stream<Integer> dice2 = IntStream.rangeClosed(1, 6).boxed();  // 주사위 생성

        // dice.flatMap(i -> Stream.of(1, 2, 3, 4, 5, 6).map(i2 -> new int[]{i, i2}))
        dice.flatMap(i -> dice2.map(i2 -> new int[]{i, i2}))
            .filter(iArr -> iArr[0] + iArr[1] == 6)
            .forEach(iArr -> System.out.print(Arrays.toString(iArr)));  // stream has already been operated upon or closed
    }
}
스트림의 동작 순서
수평적 구조가 아닌 수직적 구조로 순회한다.
* 수평적 구조 : 모든 데이터에 대해 filter 작업을 진행한 후 forEach를 실행한다.
* 수직적 구조 : 각각의 데이터가 filter 작업을 거쳐 forEach를 실행한다.

 

 

 

 

[14-8] 다음은 불합격(150점 미만)한 학생의 수를 남자와 여자로 구별하여 출력하는 프로그램이다.

(1)에 알맞은 코드를 넣어 완성하시오.

import java.util.Map;
import java.util.function.*;
import java.util.stream.Stream;

import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.partitioningBy;

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;
    }

    String getName() { return name; }
    boolean isMale() { return isMale; }
    int getHak() { return hak; }
    int getBan() { return ban; }
    int getScore() { return score; }

    @Override
    public String toString() {
        return String.format("[%s, %s, %d학년 %d반, %3d점]", name, isMale ? "남" : "여", hak, ban, score);
    }

    // groupingBy()에서 사용
    enum Level { HIGH, MID, LOW }  // 등급 : 상, 중, 하
}

class Exercise14_8 {
    public static void main(String[] args) {
        Student[] stuArr = {
            new Student("나자바", true, 1, 1, 300),
            new Student("김지미", false, 1, 1, 250),
            new Student("김자바", true, 1, 1, 200),
            new Student("이지미", false, 1, 2, 150),
            new Student("남자바", true, 1, 2, 100),
            new Student("안지미", false, 1, 2, 50),
            new Student("황지미", false, 1, 3, 100),
            new Student("강지미", false, 1, 3, 150),
            new Student("이자바", true, 1, 3, 200),

            new Student("나자바", true, 2, 1, 300),
            new Student("김지미", false, 2, 1, 250),
            new Student("김자바", true, 2, 1, 200),
            new Student("이지미", false, 2, 2, 150),
            new Student("남자바", true, 2, 2, 100),
            new Student("안지미", false, 2, 2, 50),
            new Student("황지미", false, 2, 3, 100),
            new Student("강지미", false, 2, 3, 150),
            new Student("이자바", true, 2, 3, 200)
        };

        // (1) 알맞은 코드를 넣으시오.
        Map<Boolean, Map<Boolean, Long>> failedStuBySex =

        long failedMaleStuNum = failedStuBySex.get(true).get(true);
        long failedFemaleStuNum = failedStuBySex.get(false).get(true);

        System.out.println("불합격[남자] : " + failedMaleStuNum + "명");
        System.out.println("불합격[여자] : " + failedFemaleStuNum + "명");
    }
}
// 불합격[남자] : 2명
// 불합격[여자] : 4명

 

답 : partitioningBy()를 통해 성별로 분할 한 다음 한번 더 성적으로 분할하여 불합격한 학생의 수를 구한다.

Map<Boolean, Map<Boolean, Long>> failedStuBySex = Stream.of(stuArr)
    .collect(
        partitioningBy(
            Student::isMale,              // 성별로 분할(남/녀)
            partitioningBy(
                s -> s.getScore() < 150,  // 성적으로 분할(불합격/합격)
                counting()                // 스트림에 저장된 요소의 개수 반환
            )
        )
    );
partitioningBy() : 분할(2분할). 스트림의 요소를 두 가지(조건 충족 or 조건 미충족)로 분할할 때 사용한다.
       : 스트림의 요소를 Predicate로 분류한다.
groupingBy()
: 그룹화(n분할). 스트림의 요소를 특정 기준으로 그룹화할 때 사용한다.
                      : 스트림의 요소를 Function으로 분류한다.

1) Stream.of() : 배열을 분할(2분할 or 특정 기준)하기 위해 스트림으로 변환한다.

2) partitioningBy(Student::isMale) : 스트림의 요소를 성별(남(true)/여(false))로 분류한다.

3) partitioningBy(s -> s.getScore() < 150) : 스트림의 요소를 성적(불합격(true)/합격(false))으로 분류한다.

    counting() : Collectors클래스의 정적(static) 메서드. 스트림에 저장된 요소의 개수를 반환한다.

 

 

 

 

[14-9] 다음은 각 반별 총점을 학년 별로 나누어 출력하는 프로그램이다.

(1)에 알맞은 코드를 넣어 완성하시오.

package Exercise.Exercise14;

import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.stream.Collectors.*;

// Student : [14-8]에 정의된 클래스 사용
class Exercise14_9 {
    public static void main(String[] args) {
        Student[] stuArr = {
            new Student("나자바", true, 1, 1, 300),
            new Student("김지미", false, 1, 1, 250),
            new Student("김자바", true, 1, 1, 200),
            new Student("이지미", false, 1, 2, 150),
            new Student("남자바", true, 1, 2, 100),
            new Student("안지미", false, 1, 2, 50),
            new Student("황지미", false, 1, 3, 100),
            new Student("강지미", false, 1, 3, 150),
            new Student("이자바", true, 1, 3, 200),

            new Student("나자바", true, 2, 1, 300),
            new Student("김지미", false, 2, 1, 250),
            new Student("김자바", true, 2, 1, 200),
            new Student("이지미", false, 2, 2, 150),
            new Student("남자바", true, 2, 2, 100),
            new Student("안지미", false, 2, 2, 50),
            new Student("황지미", false, 2, 3, 100),
            new Student("강지미", false, 2, 3, 150),
            new Student("이자바", true, 2, 3, 200)
        };

        // (1) 알맞은 코드를 넣으시오.
        Map<Integer, Map<Integer, Long>> totalScoreByHakAndBan =

        for(Object e : totalScoreByHakAndBan.entrySet()) {
            System.out.println(e);
        }
    }
}

 

답 : groupingBy()를 통해 학년별로 분할한 다음, 한번 더 반별로 분할하여 summingLong()를 통해 반 별 총점을 구한다.

Map<Integer, Map<Integer, Long>> totalScoreByHakAndBan = Stream.of(stuArr)
    .collect(
        groupingBy(
            Student::getHak,      // 학년별로 분할
            groupingBy(
                Student::getBan,  // 반별로 분할
                summingLong(Student::getScore)  // 요약 연산(합계)
            )
        )
    );

1) Stream.of() : 배열을 분할(2분할 or 특정 기준)하기 위해 스트림으로 변환한다.

2) groupingBy(Student::getHak) : 스트림의 요소를 학년별로 분류한다(2개 이상일 수 있으므로 groupingBy()가 적합)

3) groupingBy(Student::getBan) : 스트림의 요소를 반별로 분류한다(2개 이상일 수 있으므로 groupingBy()가 적합)

    summingLong(Student::getScore) : Collectors클래스의 정적(static) 메서드. (반 별)학생 점수의 합계를 반환한다.

                                                           : averagingInt()는 평균을 반환한다.