Java - Lambda & Stream

idkwhattodo·2024년 4월 10일
0

Java 문법

목록 보기
18/18
post-thumbnail

1. 함수형 언어

  • 함수형 언어는 객체 지향 언어와 같이 비절차형 언어인데, 간단하게 말하자면 “함수” 자체가 하나의 객체가 되어 사용되는 것
  • 동일한 입력 값을 넣었을 때 항상 동일한 리턴 값을 반환하며, 외부의 영향을 받지 않음

2. 람다식 (Lambda Expression)

  • 함수(메소드)를 간단한 식으로 표현하는 방법
  • 메소드 이름과 반환 값이 생략된다는 의미로 익명 함수라고도 불림
  • 람다식은 변수처럼 사용할 수 있는데, 메소드의 매개변수로 전달되는 것도 가능함과 동시에 메소드의 결과로 반환될 수도 있음

3. 람다식 작성 예시

3-1. 기존 함수 ⇒ 람다식

  • 메소드의 이름과 반환타입을 제거한다
  • 선언부와 몸통 {} 사이에 → 를 추가한다
  • 끝에 세미콜론(;)을 삭제한다
// 함수 1
int max(int a, int b) {
	return a > b ? a : b;
}

// 람다식 1
(int a, int b) -> return a > b ? a : b

// 함수 2
int sumArr(int[] arr) {
	int sum = 0;
	for (int i : arr) {
		sum += i ;
	}
	reutrn sum ;
}

// 람다식 2
(int[] arr) -> {
	int sum = 0;
	for (int i : arr) {
		sum += i;
	}
	reutrn sum;
}
  • 만약 매개변수가 하나일 때는 선언부의 괄호도 생략할 수 있음
    // 함수 3
    int square(int x) {
    	return x * x;
    }
    
    // 람다식 3
    x -> x * x
  • 구현부 내 문장이 한 줄일 때는 중괄호 생략(블록해제)도 가능
    void printProductName(String product, int i) {
    	System.out.println(prouct + " " + i);
    }
    
    (product, i) -> System.out.println(prouct + " " + i)
    • 만일, 구현부 내 문장이 한 줄이라도 그 한 줄이 return 문인 경우에는 중괄호 생략이 불가능함

4. 람다식 참조 변수

  • 람다식을 익명 함수라고 부른다고 했지만, 엄밀히 따지자면 익명 객체라고 하는 것이 맞음 (람다식은 선언과 생성을 동시에 하는 이름이 없는 일회용 클래스이기 때문)
    (a, b) -> a > b ? a : b
    
    /**
     * 익명 객체 구현
     */
    // Object로 만들고 참조 변수 타입도 Object로 하면 에러가 발생함
    new Object() {
    	int max(int a, int b) {
    		return a > b ? a : b;
    	}
    }
    
    // 참조 변수 대입 => 실패
    Object obj = new Object() {
    	int max(int a, int b) {
    		return a > b ? a : b;
    	}
    }
    • 위와 같이 new 연산자를 통해 익명 객체를 선언 및 생성하고 참조 변수에도 동일하게 그 클래스 타입을 지정해줘야 하지만, Object와 같이 이미 존재하는 상위 클래스나 다른 클래스에는 해당 이름의 메소드를 가지고 있지 않기 때문에 사용할 수 없음
  • 익명 객체인 람다식을 참조 변수를 통해 사용하려면 함수형 인터페이스를 사용해야 함

5. 함수형 인터페이스

  • 단 “하나”의 추상 메소드만 선언된 인터페이스
  • @FunctionalInterface : 해당 인터페이스는 단 하나의 추상 메소드 선언만 가능한 함수형 인터페이스기 때문에, 두 개 이상의 추상 메소드가 선언되지는 않았는지 컴파일러가 확인해줌
  • 즉, 함수형 인터페이스를 통해 이름이 없는 익명 객체인 람다식을 이름의 역할을 할 수 있도록 만들어줌
// 예시 1
@FunctionalInterface
interface MyFunction {
	// public abstract는 생략 가능
	public abstract int max(int a, int b);
}

// 인터페이스를 구현한 함수 1
My function f = new MyFunction() {
	public int max(int a, int b) {
		return a > b ? a : b;
	}
};
int mx = f.max(10, 15);

// 인터페이스를 구현한 람다식 1
// 아래의 세미콜론은 참조변수 대입을 하기 때문에 적어야 함
My function f = (int a, int b) -> a > b ? a : b; 
int mx = f.max(10, 15) ;
  • 함수형 인터페이스는 단 하나의 추상 메소드만 있기 때문에 위와 같이 이름없이 람다식으로 간단하게 사용이 가능함
    • 단, 메소드의 선언부가 일치하기 때문에 대체할 수 있는 것 (선언부(매개 변수 타입, 갯수) 뿐만 아니라 반환 값도 일치함)
// 예시 2
@FunctionalInterface
interface Comparator<T> {
	int compare(T o1, T o2);
}

List<String> list = Arrays.asList("abc", "aaa", "bbb", "ddd", "aaa");

// 익명 객체 생성
Collections.sort(list, new Comparator<String>() {
	public int compare(String s1, String s2) {
		return s2.compareTo(s1);
	}
});

// 람다식으로 변환
Collections.sort(list, (s1, s2) -> s2.compareTo(s1));

6. 함수형 인터페이스 타입의 매개 변수 & 반환 타입

6-1. 함수형 인터페이스를 메소드의 매개 변수 타입으로 활용하는 방법

  • 매개 변수로 함수형 인터페이스 타입 객체가 들어와 해당 함수 내에서 람다식을 사용하는 방식
@FunctionalInterface
interface MyFunction {
	void myMethod();
}
...
void aMethod(MyFunction f) {
	f.myMethod();
}
...
MyFunction f = () -> System.out.println("람다식을 이용한 myMethod");
aMethod(f);
  • 매개 변수로 들어갈 참조 변수에 람다식을 구현하고, 함수형 인터페이스 타입 객체를 매개 변수로 받는 일반적인 메소드의 인자로 위의 참조 변수만 적어주면 람다식을 통해 처리된 결과가 출력됨
  • 위의 코드는 아래와 같이 참조 변수 없이도 표현이 가능함
    aMethod(() -> System.out.println("람다식을 이용한 myMethod"));

6-2. 메소드의 반환 타입을 함수형 인터페이스로 지정하는 방법

MyFunction myMethod() {
	MyFunction f = () -> {};
	return f;
}
...
  • 위와 같이 return 값으로 내부에서 생성한 함수형 인터페이스 타입 참조 변수를 반환함
  • 위의 코드는 아래와 같이 참조 변수 없이도 표현이 가능함
    MyFunction myMethod() {
    	return () -> {};
    }

7. java.util.function 패키지

  • 자주 사용되는 다양한 함수형 인터페이스를 제공하는 패키지로, 이를 이용하면 새로운 함수형 인터페이스를 만들어 구현하는 불필요한 작업을 하지 않아도 됨
  • 덕분에 함수형 인터페이스에 정의된 메소드 이름도 통일되고, 재사용성과 유지보수 측면에서도 장점을 가져갈 수 있음

7-1. 자주 쓰이는 가장 기본적인 함수형 인터페이스

7-1-1. java.lang.Runnable

  • 매개 변수 X, 반환 값 X
  • 메소드 : void run()

7-1-2. Supplier

  • 매개 변수 X, 반환 값 O
  • 메소드 : T get()

7-1-3. Consumer

  • 매개 변수 O, 반환 값 X
  • 메소드 : void accept(T t)

7-1-4. Function<T, R>

  • 매개 변수 O, 반환 값 O (일반적인 함수)
  • 메소드 : R apply(T t)

7-2. 매개 변수가 2개인 함수형 인터페이스

7-2-1. BiConsumer<T, U>

  • 두 개의 매개 변수만 있고 반환 값 X
  • 메소드 : void accept(T t, U u)

7-2-2. BiFunction<T, U, R>

  • 두 개의 매개 변수 & 하나의 결과 반환
  • 메소드 : R apply(T t, U u)

7-2-3. BiPredicate

  • 두 개의 매개 변수 & 하나의 boolean 타입 결과 반환
  • 메소드 : boolean test(T t, U u)

7-3. 매개 변수 타입이 반환 값 타입과 같은 함수형 인터페이스

7-3-1. UnaryOperator

  • 두 개의 매개 변수만 있고 반환 값 X
  • 메소드 : T apply(T t)
  • Function의 자손
@FunctionalInterface
public interface UnaryOperator<T> extend Function<T, T> {
	static <T> UnaryOperator<T> identity() {
		return t -> t;
	}
}

7-3-2. BinaryOperator

  • 두 개의 매개 변수 & 하나의 결과 반환
  • 메소드 : T apply(T t, T t)
  • BiFunction의 자손
  • 참고) 제네릭 메소드
    • static에 제네릭이 붙어 있는 메소드를 제너릭 메소드라 함

      public class Member<T> {
      	static T getName(T name) {
      		return name;
      	}
      }
    • 위의 코드와 같이 해당 클래스 타입 객체를 매개 변수로 받거나, 반환 타입이 해당 클래스 타입이라면, 인스턴스가 생성되기 전까지 T의 타입이 무엇인지 정해지지 않기 때문에 오류가 발생함

      public class Member<T> {
      	static <T> getName(T name) {
      		return name;
      	}
      }
    • 따라서 위와 같이 작성해야하며, 코드 상으로는 class의 제네릭 와 static 메소드의 가 같아 보일 수 있지만 완전히 다른 별개의 제네릭 타입으로, 해당 클래스가 인스턴스화가 되지 않더라도 먼저 메모리에 올라갈 수 있도록 분리시켜 지정하는 방법임

8. Predicate 결합

8-1. Predicate 인터페이스

  • boolean 타입을 반환하는 조건식으로 사용되는 함수형 인터페이스를 Predicate 인터페이스라 함
  • Predicate 인터페이스는 &&(and), ||(or), !(not) 과 같은 논리 연산자로 결합할 수 있는데, 사용법은 아래와 같음
    Predicate<Integer> p = i -> i < 100 ;
    Predicate<Integer> q = i -> i < 200 ;
    Predicate<Integer> r = i -> i % 2 == 0 ;
    Predicate<Integer> notP = i -> p.negate() ; // i >= 100
    
    Predicate<Integer> all = notP.and(q.or(r)) ; // 100 <= i && (i < 200 || i%2 == 0 )
    Predicate<Integer> all2 = notP.and(q).or(r); // 100 <= i && i < 200 || i%2 == 0
    System.out.println(all.test(150)) ; // true
    System.out.println(all.test(2)) ; // false
    System.out.println(all2.test(2)) ; // true
  • Predicate 인터페이스의 static 메소드인 isEqual()은 두 대상을 비교하는 Predicate를 만들 때 사용하며, Predicate 인터페이스 타입의 참조 변수에 미리 고정 값을 지정해 놓고, 또 다른 비교 대상을 test()의 매개 변수로 지정하여 비교함
    String str1 = "abc";
    Predicate<String> p = Predicate.isEqual(str1);
    
    System.out.println(p.test("def")); // false
    // 위의 코드를 한 줄로 줄인 것, str1.equals("def")와 같음
    boolean result = Predicate.isEqual(str1).test("def");
    System.out.println(result);

8-2. 두 개 이상의 함수형 인터페이스 구현체 연결

  • andThen() ⇒ (F.I 1).andThen(F.I 2) ⇒ F.I 1 → F.I 2
  • compose() ⇒ (F.I 1).compose(F.I 2) ⇒ F.I 2 → F.I 1 (andThen() 역순 배치)
  • 단, 한 인터페이스의 입력 값, 매개 변수 타입과 다른 하나의 인터페이스 반환 값 타입이 동일해야 함
    // 입력값 : String / 반환값 : Integer
    Function<String, Integer> f = (s) -> Integer.parseInt(s, 16) 
    // 입력값 : Integer / 반환값 : String
    Function<Integer, String> g = (i) -> Integer.toBinaryString(i) 
    
    // 두 함수를 하나의 함수로 결합
    Function<String, String> h = f.andThen(g) ; 
    // andThen 역순 == g.andThen(f)
    Function<Integer, Integer> rh = f.compose(g) ; 

9. F.I 메소드 (+ Collection Framework)

InterfaceMethod설명
Collectionboolean removeIf()조건에 맞는 요소 삭제
Listvoid replaceAll(UnaryOperator operator)List 내 모든 요소 변환 & 대체
Iterablevoid forEach(Consumer action)모든 요소에 작업(action) 수행
MapV compute(K Key, BiFunction<K, V, V> f) _ 람다식지정된 키의 값에 작업(f) 수행
V computeIfAbsent(K key, Function<K, V> f) _ 람다식키가 없으면, 값에 작업(f) 수행 & 추가
V computeIfPresent(K key, BiFunction<K, V, V> f) _ 람다식지정 키가 있으면, 값에 작업(f) 수행
V merge(K key, V value, BiFunction<V, V, V> f)모든 요소에 병합 작업(f) 수행
void forEach(BiConsumer<K, V> action)모든 요소에 작업(action) 수행
void replaceAll(BiFunction<K, V, V> f)모든 요소에 치환작업(f) 수행
  • compute로 시작하는 메소드 : Map의 value를 변환하는 작업
  • merge() 메소드 : Map을 병합하는 작업

10. 메소드 참조 (Method Reference)

10-1. 기본 메소드 참조

  • 하나의 메소드만 호출하는 람다식을 더 간단하게 표현할 수 있음
    • 클래스 이름::메소드 이름
    • 참조변수 이름::메소드 이름

10-1-1. static 메소드 참조

  • (x) → ClassName.method(x) ⇒ ClassName::method
// 기존 메소드
Integer chgInt(String s) {
	return Integer.parseInt(s);
}

// 람다식
Function<String, Integer> f = (String s) -> Intger.parseInt(s);

// 메소드 참조
Function<String, Integer> f = Integer::parseInt;
  • 위의 코드에서 참조 변수 f는 Function<String, Integer> 타입을 통해 이미 String 타입이라는 것을 알 수 있기 때문에, 람다식의 (String s)는 필요가 없어짐

10-1-2. 인스턴스 메소드 참조

  • (obj.x) → obj.method(x) ⇒ ClassName::method

10-1-3. 특정 객체 메소드 참조

  • (x) → obj.method(x) ⇒ obj::method
  • 참고로 특정 객체 메소드 참조는 자주 사용하지 않음

10-2. 생성자 메소드 참조

  • 생성자는 매개 변수 없이 하나의 인스턴스를 만들어 줄 수도 있기 때문에 Supplier 인터페이스 사용이 가능함
    // 람다식 생성자
    Supplier<MyClass> s = () -> new MyClass();
    
    // 생성자 메소드 참조
    Supplier<MyClass> s = MyClass::new;
  • 매개 변수가 필요한 생성자의 경우, 생성자의 매개 변수와 반환되는 객체 타입이 인터페이스 제네릭을 통해 입력되어 있기 때문에 그대로 인터페이스를 활용하면 됨
    // 매개 변수가 1개인 경우
    Function<Integer, MyClass> f = (i) -> new MyClass(i); // 람다식
    Function<Integer, MyClass> f = MyClass::new; // 생성자 메소드
    
    // 매개 변수가 2개인 경우
    BiFunction<Integer, String, MyClass> f2 = (i, s) -> new MyClass(i, s); // 람다식
    BiFunction<Integer, String, MyClass> f2 = MyClass::new; // 생성자 메소드
  • 생성자 메소드 참조 방법으로 배열을 생성할 땐, 제네릭에 입력 값 Integer가 있기 때문에 매개 변수를 굳이 입력할 필요 없이 int[]를 앞에 붙여주면 됨
    Function<Integer, int[]> arrF = x -> new int[x]; // 람다식
    Function<Integer, int[]> arrF = int[]::new; // 생성자 메소드
    
    // String 배열
    Function<Integer, String[]> arrF2 = String[]::new;

11. 스트림(Stream)

11-1. 스트림(Stream)이란?

  • 다양한 데이터 소스(Collections, Arrays)를 표준화된 방법으로 다루기 위한 것
  • 스트림은 데이터 소스를 추상화하고 다루는데 자주 사용되는 메소드들을 정의해 놓았음
  • 추상화를 통해 데이터 소스가 무엇이든 같은 방식으로 다룰 수 있기 때문에 완전한 통일, 완전한 표준화를 이뤄낼 수 있었으며, 코드의 재사용성이 높아졌음
// 컬렉션
List<Integer> list = Arrays.asList(1,2,3,4,5); 
// 컬렉션 -> Stream
Stream<Integer> intStream = list.stream(); 
// 배열 -> Stream
Stream<String> strStream = Stream.of(new String[]{"a", "b", "c"}); 

// 람다식 -> Stream
Stream<Integer> evenStream = Stream.iterate(0, n -> n + 2) ; 
// 메서드 참조 : 람다식 -> Stream
Stream<Double> randomStream = Stream.generate(Math::random) ; 

// Int형 최적화 Stream : IntStream
IntStream intStream = new Random().ints(5) ; // 난수 스트림

11-2. Stream 작업 처리 3단계

  • Stream 만들기 ⇒ 중간 연산(내부 반복 작업) 0~n번 ⇒ 최종 연산(결과 출력) 0~1번
  • 중간 연산 : [연산 결과] 스트림 O ⇒ 반복적으로 적용 가능
  • 최종 연산 : [연산 결과] 스트림 X ⇒ 단 한 번만 적용 가능
  • 스트림은 일회성이며, 소모됨
/**
 * distinct => 중복 제거
 * limit => ~개 까지만
 * sorted => 정렬
 *
 * forEach => 각 요소마다
 */
stream.distinct().limit(5).sorted().forEach(System.out::println);
//    |----------중간연산----------||----------최종연산----------|

11-3. Stream 특징

11-3-1. 스트림은 데이터 소스를 변경하지 않는다

  • Read Only (SQL SELECT와 비슷)
  • 적용되는 메소드는 변경 메소드가 아닌 주로 조회/취급 조건을 거는데 사용되는 메소드
List<Integer> list = Arrays.asList(3, 1, 5, 4, 2);
// 정렬해서 새로운 List에 저장
List<Integer> sortedList = list.stream().sorted().collect(Collections.toList());

System.out.println(list); // [3,1,5,4,2] => 변경되지 않음
System.out.println(sortedList); // [1,2,3,4,5]

11-3-2. 스트림은 일회용이다

  • 스트림은 Iterator 처럼 일회용으로, 필요하면 Stream을 다시 생성해야 함

11-3-3. 스트림은 내부 반복으로 작업 처리를 한다

  • 내부 반복이란, 단순하게 for문이나 while문처럼 블록을 활용해서 반복시키는 것이 아닌, forEach와 같이 반복문을 내부에 숨긴 메소드들을 통해 간략하게 반복문을 처리하는 것
  • for문이나 while문에 비해 성능은 떨어지지만, 스트림을 사용하면 코드가 더 간결해짐

11-4. 지연된 연산

  • 스트림은 중간 연산과 최종 연산 단계를 거치는데, 지연된 연산이란 의미는 중간 연산 과정에서 각 연산을 하나씩 독립적으로 수행하는 것이 아니라, 최종 연산이 수행되어야 비로소 해당 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모된다는 것

11-5. Stream & IntStream

  • 기본적으로 요소의 타입이 T인 스트림(Stream)으로 사용되는데, 이 T에는 기본형이 아닌 참조형만 사용 가능함
int[] intArr = {1, 2, 3};
Stream<Integer> intStream = Arrays.stream(intArr);
  • 위의 코드를 보면, int였던 1, 2, 3의 값이 new Integer(1), new Integer(2), new Integer(3)인 참조형으로 바뀜 (오토박싱)
  • 그렇기 때문에 이런 오토박싱 & 언박싱으로 인한 비효율적인 처리를 방지하기 위해 기본형 스트림을 제공함
    • IntStream, LongStream, DoubleStream 등
  • 기본형 스트림은 Stream보다 효율적이며, 해당 기본형 값을 처리하는 유용한 메소드들을 제공함 (sum(), average() 등)

11-6. 병렬 Stream

  • Multi Thread를 통해 병렬 작업 처리를 수행하기 위한 목적
  • Java 자체 내부적으로 fork & join 프레임워크를 이용하여 자동적으로 연산을 병렬로 수행하는데, Stream에 parallel() 메소드를 호출하여 사용할 수 있음
    • parallel() 취소 메소드 ⇒ sequential()
  • 기본적으로 Stream은 병렬이 아니며, 병렬 Stream 사용 시 새로운 Stream을 만드는 것이 아니라 속성을 변경하는 것

12. Stream 사용

  • Stream의 데이터 소스가 될 수 있는 대상은 배열, 컬렉션, 임의의 난수, 람다식 등이 있음

12-1. Collection

  • Collection에는 stream() 메소드가 정의되어 있기 때문에 자손인 List나 Set을 구현한 Collection 클래스들은 stream() 메소드를 통해 Stream을 생성할 수 있음
// 가변인자(배열의 길이를 정하지 않고)로 List 만들기 위한 .asList
List<Integer> list = Arrays.asList(1,2,3,4,5) ; 

//  List -> Stream
Stream<Integer> intStream = list.stream() ; 

12-2. Arrays

  • 배열을 데이터 소스로 하는 Stream을 생성하는 메소드는 Stream과 Arrays에 static 메소드로 정의되어 있음
    // Stream
    Stream<T> Stream.of(T...values) // 가변
    Stream<T> Stream.of(T[])
    
    // Arrays
    Stream<T> Arrays.stream(T[])
    // index로 원하는 구간의 값들만으로 stream 생성 가능
    Stream<T> Arrays.stream(T[] array, int startInclusive, int endExclusive) 
    
    // int[] 는 불가능 -> 쓰고싶으면 IntStream 사용
    Integer[] intArr = {1, 2, 3, 4, 5}; 
    // intArr가 참조형(Integer)이여야 가능
    Stream<Integer> intStream = Arrays.stream(intArr) ; 
    
    // String(문자열) Stream 만들기
    Stream<String> strStream = Stream.of("a", "b", "c")
    Stream<String> strStream = Stream.of(new String[]{"a", "b", "c"});
    Stream<String> strStream = Arrays.stream(new String[]{"a", "b", "c"});
    Stream<String> strStream = Arrays.stream(new String[]{"a", "b", "c"}, 0, 3);
  • 참조형이 아닌 기본형 배열을 데이터 소스로 하는 Stream
    // 기본형 IntStream
    IntStream IntStream.of(int...values)
    IntStream IntStream.of(int[])
    IntStream Arrays.stream(int[])
    IntStream Arrays.stream(int[] array, int startInclusive, int endExclusive) 
    
    // IntStream 뿐만 아니라 LongStream과 DoubleStream도 위 방법과 동일

12-3. 난수(Random) & 범위(Range)

  • 난수를 생성하는 Random 클래스에는 각 타입별 난수로 이루어진 기본형 스트림을 반환하는 메소드를 포함하고 있음
    • IntStream ints()
    • LongStream longs()
    • DoubleStream doubles()
  • Ramdom 클래스의 Stream 메소드들은 크기가 정해지지 않은 무한 스트림을 반환하기 때문에 limit() 라는 스트림 개수 지정 메소드를 사용해서 유한 스트림으로 만들어줘야 함
    IntStream intStream = new Random().ints(); // 무한 스트림
    intStream.limit(5).forEach(System.out::println); // 임의 난수 5개 출력(유한 스트림)
  • limit()를 사용하지 않고 유한 스트림을 만드려면 난수 스트림 생성 메소드에 매개 변수로 크기를 지정해주면 됨
    IntStream ints(long streamSize)
    LongStream longs(long streamSize)
    DoubleStream doubles(long streamSize)
    
    // 매개 변수를 활용한 유한 스트림 
    IntStream intStream = new Random().ints(5); // 매개 변수 5를 통해 유한 스트림으로 만듬
    intStream.forEach(System.out::println);
  • 스트림 난수의 범위
    • Integer.MIN_VALUE <= ints() <= Integer.MAX_VALUE
    • Long.MIN_VALUE <= longs() <= Long.MAX_VALUE
    • 0.0 <= doubles() < 1.0
  • 지정된 범위의 난수를 요소로 갖는 스트림을 생성하는 메소드
    // 무한 스트림
    IntStream ints(int begin, int end) // begin <= ints() < end
    LongStream longs(long begin, long end) // begin <= longs() < end
    DoubleStream doubles(double begin, double end) // begin <= doubles() < end
    
    // 유한 스트림
    IntStream ints(long streamSize, int begin, int end) // begin <= ints() < end
    LongStream longs(long streamSize, long begin, long end) // begin <= longs() < end
    DoubleStream doubles(long streamSize, double begin, double end) // begin <= doubles() < end
  • Stream에서도 특정 범위를 지정할 때 사용하는 range()를 사용하면 특정 범위의 정수 요소를 갖게됨
    IntStream IntStream.range(int begin, int end) // end 값 포함 X
    IntStream IntStream.rangeClosed(int begin, int end) // end 값 포함 O
    
    IntStream int1 = IntStream.range(1, 5); // 1, 2, 3, 4
    IntStream int2 = IntStream.rangeClosed(1, 5); // 1, 2, 3, 4, 5

12-4. 람다식(lambda)

  • Stream에서 람다식을 매개 변수로 받기 위해 iterate()와 generate() 메소드를 사용하며, 이 두 메소드는 매개 변수로 받은 람다식에 의해 계산되는 값들을 요소로하는 무한 스트림을 생성함
    static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
    static <T> Stream<T> generate(Supplier<T> s)

12-4-1. iterate()

  • Seed부터 시작해서 람다식 f에 의해 계산된 결과를 다시 Seed로 지정하여 계산을 반복함
// 0 -> 2, 2 -> 4, 4 -> 6 ... (무한 스트림)
Stream<Integer> evenStream = Stream.iterate(0, n -> 2);

12-4-2. generate()

  • iterate()와 같이 람다식에 의해 계산되는 값을 요소로하는 무한 스트림을 생성하지만, iterator()와 달리 이전 결과를 통해 다음 요소를 계산하지 않음
  • 또한, 매개 변수인 람다식의 타입이 Supplier로, 매개 변수가 없는 람다식 인터페이스임

12-4-3. iterate()와 generate()의 주의점

  • iterate()와 generate()에 의해 생성된 스트림은 기본형 스트림 타입 참조 변수로 다룰 수 없음
  • 기본형 스트림으로 사용하려면 mapToInt(), mapToLong(), mapToDouble() 등을 사용해야 함
  • 기본형 스트림을 일반적인 제네릭 타입 지정 스트림으로 사용하려면 오토박싱을 해주는 boxed()를 사용함

12-4-4. boxed()

  • boxed()는 원시 타입에 대한 스트림 지원을 클래스 타입으로 전환해줌 (오토박싱)
    • ex) IntStream ⇒ Stream
    • Collection에는 int를 담지 못하기 때문에, Integer로 변환하여 List와 같이 담기 위해 수행
  • int[] ⇒ Integer[]
    int[] num = {3, 4, 5};
    
    // int[] => IntStream
    int[] num = Arrays.stream(num);
    
    // IntStream => Stream<Integer>
    Stream<Intger> boxed = stream.boxed();
    
    // Stream<Intger> => Integer[] 1
    Integer[] result = boxed.toArray(Integer[]::new);
    
    // int[] => IntStream => Stream<Integer> => Integer[] oneLine
    Integer[] result = Arrays.stream(num)
    												 .boxed()
    												 .toArray(Integer[]::new);
  • int[] ⇒ List
    // Arrays.stream 사용
    List<Integer> list1 = Arrays.stream(arr) // int[] => IntStream
    												.boxed() // IntStream => Stream<Integer>
    													.collect(Collectors.toList()); // Stream<Integer> => List<Integer>
    
    // IntStream.of 사용
    List<Integer> list2 = IntStream.of(arr) // int[] => IntStream
    												.boxed() // IntStream => Stream<Integer>
    													.collect(Collectors).toList()); // Stream<Integer> => List<Integer>
  • List ⇒ int[]
    // 방법 1 : 직접 int[] 생성해서 넣기
    int[] arr1 = new int[list.size()]
    for (int i=0; i<list.size(); i+=)
    	arr1[i] = list.get(i).intValue();
    
    // 방법 2 : 람다식 
    int[] arr2 = list.stream()
    								 .mapToInt(i -> i)
     								 .toArray();
    
    // 방법 3 : 메소드 참조
    int[] arr3 = list.stream()
    								 .mapToInt(Integer::intValue)
     								 .toArray();
  • int[] ⇒ Set
    int[] ints = {1, 2, 3, 4};
    
    // 방법 1 : HashSet 인자에 List<Integer> 넣기
    Set<Integer> set = new HashSet<>(Arrays.stream(ints) // int[] => IntStream
    																			 .boxed() // IntStream => Stream<Integer>
    																			 .collect(Collectors.toList())); // Stream<Integer> => Set<Integer>
    
    // 방법 2 : stream으로 한 줄에 작성
    Set<Integer> set = Arrays.stream(ints) // int[] => IntStream
    												 .boxed() // IntStream => Stream<Integer>
    											   .collect(Collectors.toList()); // Stream<Integer> => Set<Integer>
    
    // 방법 3 : int[] => Integer[]
    HashSet<Integer> hashSet = IntStream.of(ints) // int[] => IntStream
    																		.boxed() // IntStream => Stream<Integer>
    																		.collect(Collectors.toCollection(HashSet::new)); // Stream<Integer> => HashSet<Integer>
    • 참고) Set & HashSet
      • Set
        • 자바 컬렉션 프레임워크의 인터페이스 중 하나로, 중복을 허용하지 않는 요소들의 모임을 나타냄
        • 순서가 없으며, 요소에 대한 접근은 인덱스를 사용하는 것이 아닌 iterator를 통해 이뤄짐
        • Set을 구현한 구현체가 HashSet, LinkedHashSet, TreeSet
      • HashSet
        • Set 인터페이스를 구현한 클래스 중 하나로, 중복된 요소를 허용하지 않으며 순서가 없음
        • 해시 테이블을 사용하여 요소를 저장하기 때문에, 요소에 빠르게 접근할 수 있음

12-5. Files & Empty Stream

  • java.nio.file.Files는 파일을 다루는 유용한 메소드들을 제공함
    • Files.list() : 지정 디렉토리에 있는 파일 목록을 소스로 하는 스트림을 생성하여 반환
      Stream<Path> Files.list(Path dir)
    • Files.lines() : 한 파일의 한 행(line)을 소스로 하는 스트림을 생성하여 반환 (파일 뿐만 아니라 다른 입력 대상으로부터도 가능)
      Stream<String> Files.lines(Path path)
      Stream<String> Files.lines(Path path, Charset cs)
      Stream<String> lines() // BufferedReader 클래스의 메소드
  • 요소가 하나도 없는 비어있는 스트림의 경우 연산 수행 결과가 없는데, null 보다는 빈 스트림을 반환해야함
    Stream emptyStream = Stream.empty(); // 비어있는 스트림 생성 및 반환
    long cnt = emptyStream.count(); // 0

13. Stream 연산

  • 스트림에 정의된 메소드 중에서 데이터 소스를 다루는 작업을 수행하는 것은 연산(Operation)이라고 하며, 중간 연산과 최종 연산 2개의 단계가 진행됨
    • 중간 연산 : 연산 결과가 스트림인 연산으로, 연속해서 중간 연산 가능
      • filter(), distinct(), sort(), limit() 등
    • 최종 연산 : 연산 결과가 스트림이 아닌 연산으로, 단 한번만 최종 연산 가능(스트림 소모(closed))
      • count(), forEach() 등

13-1. 중간 연산

  • 반환 값의 타입이 Stream인 연산

13-1-1. skip()

  • Stream 내 요소를 건너뛰는 중간 연산 메소드
Stream<T> skip(long n)

// 기본형 스트림에서의 skip
IntStream skip(long n)

13-1-2. limit()

  • 반환 값 / 결과 값으로 구성된 Stream에 있는 요소들을 원하는 갯수 만큼 가져오는 중간 연산 메소드
Stream<T> limit(long maxSize)

// 기본형 스트림에서의 limit
IntStream limit(long maxSize)

13-1-3. filter()

  • 주어진 조건에 맞는 요소만 가져오는 중간 연산 메소드
  • 다른 조건으로 여러번 filter 메소드 적용 가능
  • Predicate 인터페이스가 아닌 boolean 반환 람다식 사용 가능
Stream<T> filter(Predicate<? super T> predicate)

IntStream intStream = IntStream.rangeClosed(1, 10); // 1 ~ 10

intStream.filter(i -> i % 2 == 0).forEach(System.out::println); // 246810
intStream.filter(i -> i % 2 != 0 && i % 3 != 0).forEach(System.out::println); // 157
intStream.filter(i -> i % 2 != 0).filter(i -> i % 3 != 0).forEach(System.out::println); // 157

13-1-4. distinct()

  • Stream 내 중복된 요소들을 하나씩만 남겨놓고 중복 값을 제거하는 중간 연산 메소드
Stream<T> distinct()

IntStream intStream = IntStream.of(1,2,2,3,3,3,4,5,5,6);
intStream.distinct().forEach(System.out::print); // 123456

13-1-5. sorted()

  • Stream 요소들을 정렬하는 중간 연산 메소드
  • Comparator<? super T> comparator를 통해서 정렬 기준 적용 가능
    • 만일, Comparator를 지정하지 않으면 스트림의 요소 클래스에서 구현해놓은 Comparable 기준으로 정렬됨 (Comparable을 구현하지 않은 클래스 요소라면 comparator를 지정하지 않을 경우 예외 발생)

      Stream<T> sorted()
      Stream<T> sorted(Comparator<? super T> comparator)
  • 문자열 스트림 정렬
    // CASE_INSENSITIVE_ORDER 같은 경우는 미리 static 으로 구현되어 있음
    static Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();
    
    Stream<String> strStream = Stream.of("dd", "aaa", "CC", "cc", "b");
    
    /*
    최종 연산 forEach(System.out::print) 를 수행시켰다고 가정
    */
    // 기본 정렬 -> CCaaabccdd
    strStream.sorted() ; // String 클래스의 Comparable
    strStream.sorted(Comparator.naturalOrder()) ; // Comparator 클래스에 구현된 기본 정렬 기준 
    strStream.sorted((s1, s2) -> s1.compareTo(s2)) ; // 람다식
    strStream.sorted(String::compareTo) ;
    
    // 역순 정렬 -> ddccbaaaCC
    strStream.sorted(Comparator.reverseOrder())
    
    // 기본 정렬 (대소문자 구분 X) -> aaabCCccdd
    strStream.sorted(String.CASE_INSENSITIVE_ORDER)
    
    // 역순 정렬 (대소문자 구분 X) -> ddCCccbaa (주의, 대문자가 앞에 위치함)
    strStream.sorted(String.CASE_INSENSITIVE_ORDER.reversed())
    
    // 별도 기준 정렬 : comparing
    strStream.sorted(Comparator.comparing(String::length))
    strStream.sorted(Comparator.comparingInt(String::length))
    
    // 별도 기준 역순 정렬 : comparing().reversed()
    strStream.sorted(Comparator.comparing(String::length).reversed())
  • Comparator 메소드
    • 스트림의 요소가 만약 Comparable을 구현한 경우라면 매개 변수가 하나인 comparing을 사용하고, Comparable을 구현하지 않은 경우 추가적인 정렬 기준(Comparator)을 따로 지정해주어야 함
      comparing(Function<T, U> keyExtractor)
      comparing(Function<T, U> keyExtractor, Comparator<U> keyComparator)
    • 스트림과의 비교 대상이 기본형이라면 아래와 같이 오토박싱 & 언박싱 과정을 줄여 효율적으로 기본형 비교 대상을 제공할 수 있음
      comparingInt(ToIntFunction<T> keyExtractor)
      comparingLong(ToLongFunction<T> keyExtractor)
      comparingDouble(ToDoubleFunction<T> keyExtractor)
    • 만약 정렬 조건이 2개 이상인 경우, 정렬 조건 누적을 thenComparing() 메소드로 할 수 있음
      thenComparing(Comparator<T> other)
      thenComparing(Function<T, U> keyExtractor)
      thenComparing(Fucntion<T, U> keyExtractor, Comparator<U> keyComparator)
    • 구조 ⇒ sorted(Comparator.comparing().thenComparing().thenComparing()…thenComparing())

13-1-6. map()

  • 중간 연산 메소드들 중 가장 핵심적인 메소드로, 스트림 요소에 저장된 값 중에서 원하는 필드만 뽑아내거나 특정 형태 변환을 해야할 때 사용
  • 매개 변수로는 T타입 값을 R타입 값으로 변환 & 반환하는 함수를 지정해줘야 함
    // Stream<T> -> Stream<R>
    Stream<R> map(Function<? super T, ? extends R> mapper)
  • File 타입 값들 중, 파일 이름만 가져와 이름으로 구성된 Stream으로 변환
    Stream<File> fileStream = Stream.of(new File("F1.java"),
    																	  new File("F2.java"), 
    																		new File("F3.java"));
    
    // fileStream.map(File::getName).forEach(System.out::println) 로도 가능
    Stream<String> fileNameStream = fileStream.map(File::getName); // F1.javaF2.javaF3.java
    fileNameStream..forEach(System.out::println)

13-1-7. peek()

  • 연산 중간에 디버깅 용도로 많이 쓰이는 메소드
  • 중간 연산과 중간 연산 사이에 스트림을 소모하는 연산으로, 최종 연산을 사용하지 못하기 때문에 결과를 출력해서 결과를 확인하는 forEach()를 사용할 수 없음
  • peek() 함수는 스트림의 요소를 소모하지 않으므로 여러 번 사용이 가능함
Stream<File> fileStream = Stream.of( new File("F1.java"), new File("F1.bak"), 
									new File("F2.java"), new File("F3.txt"),
                                    new File("F3"), new File("F4.java"));

// 요소 하나씩 하나씩 처리
fileStream.map(File::getName)
		.filter(s -> s.indexOf('.') != -1) // 확장자가 없는 경우, 출력하지 못함
        .peek(s -> System.out.printf("filename = %s%n", s))
        .map(s -> s.substring(s.indexOf('.') + 1))
        .peek(s -> System.out.printf("extension = %s%n", s))
        .map(String::toUpperCase)
        .distinct() // 확장자가 중복되는 경우, 출력하지 못함
        .forEach(System.out::println);

/*
filename = F1.java
extension = java
JAVA
filename = F1.bak
extension = bak
BAK
filename = F2.java
extension = java
filename = F3.txt
extension = txt
TXT
filename = F4.java 
extension = java
*/

13-1-8. flatMap()

  • Stream의 Stream을 Stream으로 변환
  • Stream<String[]>은 스트림 > 배열 > 배열 내부 값의 구조로 이루어져 있는데, 각 배열의 내부 값들을 하나의 스트림으로 합병하여 Stream으로 만들고 싶을 때 사용
    • 만일 map() 메소드를 사용하면 기존 Stream<String[]>을 Stream<Stream>으로 밖에 만들 수 없음 (내부 값들이 합쳐진게 아닌 그냥 단순 스트림으로 묶어준 꼴)
      Stream<Stream<String>> strStrStream = 배열스트림.map(Arrays::stream)
    • 만일 각 배열들이 가진 요소들을 하나의 스트림으로 합병하고 싶다면, flatMap() 메소드를 사용하면 됨
      // Stream<String>의 스트림 반환
      Stream<String[]> 스트림.flatMap(Arrays::stream)
  • 보통 문자열 배열의 값들이 긴 문자열 한 줄 씩으로 배열을 구성하고 있다면, 각 한 줄을 단어로 나눠 각 줄의 단어들을 하나의 스트림으로 모아서 취급하고 싶을 때 사용하면 효과적
중간 연산설명
Stream distinct()중복 제거
Stream filter( Predicate predicate )조건에 맞는 요소만 필터링
Stream limit( long maxSize )스트림의 요소 제한 (잘라내기)
Stream skip( long n )스트림의 요소 일부 Skip
Stream peek( Consumer action )스트림의 요소 작업 수행, 보통 " 중간 결과를 볼 때 " 사용
Stream sorted()Stream sorted( Comparator comparator )스트림의 요소 정렬, Comparator로 정렬 기준
중간 연산설명
Streammap( Function<T, R> mapper )
DoubleStreammapToDouble( ToDoubleFunction mapper )
IntStreammapToInt( ToIntFunction mapper )
LongStreammapToLong( ToLongFunction mapper )
스트림 요소 반환
Streammap( Function<T, R> mapper )
DoubleStreammap( Function<T, R> mapper )
IntStreammap( Function<T, R> mapper )
LongStreammap( Function<T, R> mapper )

13-2. 최종 연산

  • 최종 연산은 스트림을 소모해서 결과를 만들어내고, 최종 연상이 완료되면 스트림이 닫히게 되기 때문에 스트림 당 단 1번만 사용할 수 있음
  • 중간 연산의 반환 값이 스트림이었던 것과는 달리, 최종 연산의 반환 값은 단일 값(int, boolean 등)이거나 스트림의 요소가 담긴 배열이나 컬렉션일 수 있음

13-2-1. forEach()

  • 반환 타입은 void이며, 주로 forEach(System.out::print)와 같이 요소 출력의 용도로 많이 쓰임
void forEach(Consumer<? super T> action)

13-2-2. forEachOrdered()

  • 병렬 스트림에서 사용하는 경우 순서를 보장해줌(스트림을 parallel()로 처리할 때 사용)
void forEachOrdered(Consumer<? super T> action)

13-2-3. 조건 검사

  • 매개 변수로 들어온 조건식에 대한 검사 결과를 반환하는 메소드
boolean allMatch(Predicate<? super T> Predicate) // 모든 요소 만족 => true
boolean anyMatch(Predicate<? super T> Predicate) // 하나라도 요소 만족 => true
boolean noneMatch(Predicate<? super T> Predicate) // 모든 요소 불만족 => true (<-> allMatch)

13-2-4. 조건에 일치하는 요소 찾기

  • 주로 Stream의 중간 연산인 filter()와 함께 사용함
  • findFirst()
    • 조건에 일치하는 첫 번째 요소 반환
    • 직렬 스트림(sequential())에서 쓰임
  • findAny()
    • 조건에 일치하는 아무 요소 하나 반환
    • 병렬 스트림(paraller())에서 쓰임
Optional<Student> result = stuStream.filter(s -> s.getTotalScore <= 100).findFirst();
Optional<Student> result = stuStream.parallel()
															.filter(s -> s.getTotalScore <= 100).findAny();

13-2-5. reduce()

  • 스트림의 요소를 하나씩 줄여가며 누적 연산을 수행
// 기본
Optional<T> reduce(BinaryOperator<T> accumulator)

/*
 * identity => 초기값
 * accumulator => 이전 연산 결과 & 스트림 요소에 수행할 연산
 * combiner => (병렬 스트림) 결과를 합치는데 사용할 연산
 */
T reduce(T identity, BinaryOperator<T> accumulator)

// 입력 값을 2개 받는 BinaryFunction F.I의 연산
// combiner => 합치기 연산
U reduce (U identity, BiFunction<U, T, U> accumulator, BinaryOperator<U> combiner)
  • identity 매개 변수를 입력하면 반환 타입이 identity 타입과 동일하여 Optional을 사용할 필요가 없지만, identity 매개 변수를 입력하지 않을 경우에는 null이 반환될 수 있기 때문에 결과를 담을 참조 변수 타입을 Optional로 지정해주어야 함
  • count(), sum(), max(), min()과 같은 메소드들 또한 모두 reduce() 메소드를 통해 연산을 활용하는 구조

13-2-6. Collect() & Collectors

  • Collect()는 Collector 인터페이스 구현 클래스 객체를 매개 변수로 받는 메소드로, reduce()와 유사한 스트림 요소 수집 연산
  • 다만, reduce()는 스트림 전체에 대해 Reducing할 때 사용하며, collect()는 요소 그룹별로 Reducing할 때 사용함
  • collect()는 정의된 Collector가 필요한데, Collector 인터페이스는 collect()가 스트림의 요소를 수집할 때 수집에 필요한 메소드를 정의해놓은 인터페이스임
    // T(요소) --> A에 누적(reducing) --> 결과 R로 변환&반환
    public interface Collector<T, A, R> {
    	// 누적할 곳 (ex. StringBuilder::new)
    	Supplier<A> supplier();
        
      // 누적 방법 (ex. (sb, s) -> sb.append(s))
      BiConsumer<A, T> accumulator();
        
      // 결합 방법(병렬) (ex. (sb1, sb2) -> sb1.append(sb2))
      BinaryOperator<A> combiner();
        
      // 최종 변환 (R타입으로) (ex. sb -> sb.toString())
      Function<A, R> finisher();
        
      // Collector 특성 Set 반환
      Set<Characteristics> characteristics() ; 
    
      ...
    }
  • Collector 인터페이스를 모두 직접 구현하기에는 어려우므로, 미리 작성된 다양한 종류의 컬렉터 반환 static 메소드가 Collectors 클래스에 존재함
    • 변환 : mapping(), toList(), toSet(), toMap(), toCollection() 등
    • 통계 : counting(), summingInt(), averagingInt(), maxBy(), minBy(), summarizingInt() 등
    • 문자열 결합 : joining()
    • Reducing : reducing()
    • 그룹화 & 분할 : groupingBy(), partitioningBy(), collectingAndThen()

13-2-7. 기능별 Collectors 클래스 메소드

  • improt static java.util.Stream.Collectors.*를 통해 Collectors.를 코드에서 생략해서 사용할 수 있음
  • 변환
    • 스트림 ⇒ 컬렉션
      • toList(), toSet(), toMap(), toCollection()

      • List나 Set과 같은 컬렉션은 위와 같이 사용하면 되지만, ArrayList나 LinkedList 등과 같이 특정 컬렉션을 지정할 때에는 toCollection() 메소드에 원하는 컬렉션의 생성자 참조를 매개 변수로 넣어주면 됨

        List<String> names = childrenStream.map(Child::getName)
        																	 .collect(Collectors.toList());
        
        // String 요소 List 객체 스트림 => ArrayList 컬렉션에 저장
        ArrayList<String> list = names.stream()
        													.collect(Collectors.toCollection(ArrayList::new));
      • toMap()은 key-value 쌍으로 저장해야 하므로, 연산 대상인 객체의 어떤 필드들을 키와 값으로 사용할지를 정해줘야 함

        // 주민번호 -> 키 & 객체 -> 값
        Map<String, Person> map = personStream
        											.collect(Collectors.toMap(p -> p.getRegId(), p -> p));
    • 스트림 ⇒ 배열
      • toArray()

      • 스트림의 저장 요소들을 배열로 변환

      • 주의할 점은 매개 변수로 해당 타입의 생성자 참조를 넣어줘야하며, 만약 넣지 않는다면 Object[] 타입이 반환되므로 배열을 담을 참조 변수 타입도 Object[]로 일치시켜줘야 함

        Child[] childrenNames = childrenStream.toArray(Child[]::new) ;
        
        // 에러 : Object[] 배열 반환 -> 불일치
        Child[] childrenNames = childrenStream.toArray() ; 
        // 매개 변수 타입과 반환 타입 일치
        Child[] childrenNames = (Child[])childrenStream.toArray() ;
        Object[] childreanNames = childrenStream.toArray() ;
  • 통계
    • counting(), summingInt(), averagingInt(), maxBy(), minBy(), summarizingInt() 등

    • 그룹화 메소드인 groupingBy() 메소드와 주로 함께 사용

      import static java.util.Stream.Collectors.* ;
      
      long count = stuStream.count();
      long count = stuStream.collect(counting()); // Collectors
      
      long totalScore = stuStream.mapToInt(Student::getTotalScore).sum();
      long totalScore = stuStream.collect(summingInt(Student::getTotalScore)); // Collectors
      
      Optional<Student> topStudent = stuStream.max(Comparator.compareInt(Student::getTotalScore));
      Optional<Student> topStudent = stuStream.collect(maxBy(Comparator.compareInt(Student::getTotalScore)));
      
      IntSummaryStatistics stat = stuStream.mapToInt(Student::getTotalScore).summaryStatistics();
      IntSummaryStatistics stat = stuStream.collect(summarizingInt(Student::getTotalScore));
  • 결합
    • joining()

    • 구분자 & 접두사 & 접미사 모두 지정 가능함

    • 단, 스트림의 요소가 String이나 StringBuffer처럼 CharSequence의 자손인 경우에만 결합이 가능하기 때문에, 요소가 문자열이 아닌 경우 map()을 통해 먼저 문자열로 변환해야함

      import static java.util.Stream.Collectors.* ;
      // 이름 : Kim, Jung, Choi, Park이 있다고 가정
      
      // 출력 >> KimJungChoiPark
      String stuNames = stuStream.map(Student::getName).collect(joining());
      
      // 출력 >> Kim,Jung,Choi,Park
      String stuNames = stuStream.map(Student::getName).collect(joining(","));
      
      // 출력 >> [Kim, Jung, Choi, Park]
      String stuNames = stuStream.map(Student::getName).collect(joining(",", "[", "]"));
  • 리듀싱(Reducing)
    /* collect() */
    Collector reducing(BinaryOperator<T> op) 
    
    Collector reducing(T identity, BinaryOperator<T> op) 
    // 변환 작업까지 필요한 경우
    Collector reducing(U identity, Function<T, U> mapper, BinaryOperator<U> op)
    
    /* reduce() */
    import static java.util.Stream.Collectors.* ;
    IntStream intStream = new Random().ints(1, 46).distinct().limit(6) ;
    
    // collect() 활용
    Optinal<Integer> max = intStream.boxed().collect(reducing(Integer::max));
    // reduce() 활용
    OptionalInt max = intStream.reduce(Integer::max) ; // 전체 대상 reducing 에 유리
    • 기본형 스트림인 IntStream에는 매개 변수가 3개인 collect()만 정의되어 있기 때문에, boxed()를 통해 Stream로 변환해야만 매개 변수 1개인 collect()를 쓸 수 있음
      long sum = intStream.reduce(0, (a,b) -> a + b); 
      long sum = intStream.boxed().collect(reducing(0, (a,b) -> a + b)); 
      
      int grandTotal = stuStream.map(Student::getTotalScore)
      													.reduce(0, Integer::sum); 
      // 초기값 설정, 변환 작업, 수행 작업 reducing 
      int grandTotal = stuStream
      								.collect(reducing(0, Student::getTotalScore, Integer::sum));
  • 그룹화 & 분할
    • 그룹화는 스트림의 요소를 특정 기준으로 그룹화하는 것이고, 분할은 스트림의 요소를 [조건 일치 그룹] / [조건 불일치 그룹]의 두 가지 그룹으로 분할하는 것
    • 반환 결과는 그룹화와 분할 모두 Map에 담겨 반환되지만, 그룹화는 특정 기준에 의해 N분할이 가능하고, 분할은 일치와 불일치의 2분할이 가능함
    • 분할
      Collector partitioningBy(Predicate predicate)
      Collector partitioningBy(Predicate predicate, Collector downstream)
      • 기본 분할
        // 기본 분할 -> 기준 적용 결과가 Key값에 해당하는 값들의 List
        Map<Boolean, List<Student>> stuBySex = stuStream.collect(partitioningBy(Student::isMale));
        
        List<Student> maleStudent = stuBySex.get(true) ; // 남학생 리스트
        List<Student> femaleStudent = stuBySex.get(false) ; // 여학생 리스트
      • 기본 분할 + 통계 정보
        /* 각 그룹의 학생 수 */
        Map<Boolean, Long> stuNumBySex = stuStream.collect(partitioningBy(Student::isMale, counting()));
        
        System.out.println("남학생 인원 : " + stuNumBySex.get(true)); // 남학생 인원
        System.out.println("여학생 인원 : " + stuNumBySex.get(false)); // 여학생 인원
        
        /* 각 성별 그룹에서의 성적 1등 */
        Map<Boolean, Optional<Student>> topScoreBySex = stuStream.collect( 
        									partitioningBy(Student::isMale, maxBy(comparingInt(Student::getScore))));
        
        System.out.println("남학생 1등 : " + topScoreBySex.get(true)); // 추출된 "Optional"객체의 toString() 결과
        System.out.println("여학생 1등 : " + topScoreBySex.get(false)); // 추출된 "Optional"객체의 toString() 결과
        
        /* 강 성별 그룹에서의 성적 1등 결과 값을 객체의 타입으로 반환 */ 
        Map<Boolean, Optional<Student>> topScoreBySex = stuStream.collect( 
        									partitioningBy(Student::isMale, 
                                            	collectingAndThen(maxBy(comparingInt(Student::getScore)), 
                                                						Optional::get)));
        
        System.out.println("남학생 1등 : " + topScoreBySex.get(true)); // 추출된 객체의 toString() 결과
        System.out.println("여학생 1등 : " + topScoreBySex.get(false)); // 추출된 객체의 toString() 결과
        • maxBy() 메소드의 반환 값은 Optional 타입이기 때문에, CollectingAndThen()을 활용해 해당 함수의 매개 변수로 maxBy() 메소드와 더불어 Optional::get을 두번째 매개 변수로 입력하면 해당 객체의 타입으로 반환할 수 있음
      • 다중 분할
        Map<Boolean, Map<Boolean, List<Student>>> failedStuBySex = stuStream.collect( 
        							partitioningBy(Student::isMale, 
                                            		partitioningBy(s -> s.getScore() < 100)));
        
        List<Student> failedMaleStudent = failedStuBySex.get(true).get(true); // "불합격" 남학생 리스트
        List<Student> failedFemaleStudent = failedStuBySex.get(false).get(true); //"불합격" 여학생 리스트
        • 분할을 두 단계 깊이에 걸쳐 수행하려면 Collector downstream 위치에서 다시 partioningBy()를 입력하면 됨
    • 그룹화
      Collector groupingBy(Function classifier)
      Collector groupingBy(Function classifier, Collector downstream)
      Collector groupingBy(Function classifier, Supplier mapFactory, Collector downstream)
      • groupingBy()를 사용하면 Grouping을 할 수 있는데, 기본적으로 Value 값은 List에 담지만, 다른 컬렉션으로 지정할 수 있음
      • getter 메소드를 통한 기본 Grouping (특정 도메인 값으로 나눠져 있는 필드 값을 기준으로 Grouping)
        Map<Integer, List<Student>> stuByBan = stuStream
        																.collect(groupingBy(Student::getBan));
                                        
        // HashSet 컬렉션으로 반환
        Map<Integer, HashSet<Student>> stuByBan = stuStream
        								.collect(groupingBy(Student::getBan, toCollection(HashSet::new));
      • if ~ else를 활용한 Grouping
        Map<Student.Level, Long> stuByLevel = stuStream
        								.collect(groupingBy( s -> {
        	                                				if(s.getScore() >= 200) 
        		                                        return Student.Level.HIGH;
        	                                        else if(s.getScore() >= 100)
        		                                        return Student.Level.MID;
        	                                        else
        	                                         	return Student.Level.LOW;
        	                                        }, counting()));
        
        List<Student> failedMaleStudent = failedStuBySex.get(true).get(true) ; // "불합격" 남학생 리스트
        List<Student> failedFemaleStudent = failedStuBySex.get(false).get(true) ; // "불합격" 여학생 리스트
      • 다중 Grouping
        // 학년별 그룹화 -> 반별 그룹화
        Map<Integer, Map<Integer, List<Student>>> stuByHakAndBan = stuStream
        											.collect(groupingBy(Student::getHak,
        			  											 groupingBy(Student::getBan)));
        
        // 각 학년의 각 반별 학생들 + 성적 등급별 그룹화 & 각 등급에 해당하는 학생 수
        Map<Integer, Map<Integer, Set<Student.Level>>> stuByHakAndBan = 
        			stuStream.collect(
                    	groupingBy( Student::getHak , 
                        	groupingBy(Student::getBan,
                            	mapping(s -> {
        					                         if(s.getScore() >= 200) 
        						                         return Student.Level.HIGH;
        					                         else if(s.getScore() >= 100)
        					                           return Student.Level.MID;
        					                         else
        					                           return Student.Level.LOW;
        					                         }, toSet()))));
        
        // 각 반의 1등을 해당 값 객체 타입으로 받기
        Map<Integer, Map<Integer, Student>> topStuByHakAndBan =
        							stuStream.collect(
        		              groupingBy(Student::getHak , 
        	                	groupingBy(Student::getBan,
        	                    collectingAndThen(
        	                      maxBy(comparingInt(Student::getScore)),
        	                        Optional::get))));
중간 연산설명

| void forEach( Consumer<? super T> action )
void forEachOrdered( Consumer<? super T> action ) : 순서 유지 (병렬 스트림 처리 시에 주로 이용) | 각 요소에 지정된 작업 수행 |
| long count() | 스트림 요소 개수 |
| Optional max( Comparator comparator )
Optional min( Comparator comparator ) | 최대 / 최솟값 |
| Optional findFirst() : 첫번째 요소 (직렬)
Optional findAny() : 아무거나 하나 (병렬, filter()와 자주 사용) | 스트림의 요소 하나 반환 |
| boolean allMatch( Predicate p ) : 모두 만족
boolean anyMatch( Predicate p ) : 하나라도 만족
boolean noneMatch( Predicate p ) : 모두 만족 X | 모든 요소가 주어진 조건 만족 여부 확인 |
| Object[] toArray()A[] toArray( IntFinction<A[]> generator ) | 스트림 요소 → 배열 반환 |
| 《 핵 심 》 | |
| Optional reduce( BinaryOperator accumulator )
T reduce( T identity, BinaryOperator accumulator )
U reduce( U identity, BiFunction<U, T, U> accumulator, BinaryOperator combiner ) | 요소를 하나씩 줄여가면서(Reducing) 계산 |
| R collect( Collector<T, A, R> collector )
R collect( Supplier supplier, BiConsumer<R, T> accumulator, BiConsumer<R, R> combiner ) | 스트림 요소 수집
주로 그룹화 / 분할 결과 컬렉션에 담아 반환할 때 사용 |

14. Optional

  • T타입 객체의 래퍼(Wrapper) 클래스로, 어떠한 타입이든 모든 객체를 담을 수 있음
  • null 값 또한 담아서 간접적으로 다룰 수 있음
    • NullPointerException 방지
    • null 체크를 위한 if문을 사용하지 않아도 됨

14-1. Optional 객체 생성

  • of()
    // "abc" 값을 가진 String 객체 1
    String str = "abc";
    
    // "abc" 값의 주소를 참조하는 String 객체를 가진 Optional 객체 1
    Optional<String> optVal = Optional.of(str);
    
    // "abc" 값의 String 객체를 가진 Optional 객체 2
    Optional<String> optVal = Optional.of("abc");
    
    // "abc" 값을 가진 새로운 String 객체를 가진 Optional 객체 3
    Optional<String> optVal = Optional.of(new String("abc"));
    • Optional은 Wrapper 클래스이기 때문에, 한 번 더 거쳐서 감싸는 형태 (단, null 값을 사용할 땐 of() 메소드를 사용하면 안됨
  • ofNullable()
    • 참조 변수의 값이 null일 가능성이 있을 경우, of() 대신 ofNullable()을 사용해야 함

      // "abc" 값을 가진 String 객체 1
      String str = "abc";
      
      Optional<String> optVal = Optional.ofNullable(str); // Optional객체로 Wrapping
      Optional<String> optVal = Optional.ofNullable(null);
      Optional<String> optVal = Optional.of(null); // NullPointerException 발생
    • Optional 타입의 참조 변수를 초기화할 때 null이 가능하긴 하나 바람직한 방법이 아니끼 때문에, Optional 타입 참조 변수를 초기화하려면 empty() 메소드를 사용해야 함

      • empty() 메소드는 제네릭 메소드로, 추정 가능하기 때문에 보통 생략할 수 있음

        Optional<String> optVal = null; // 가능하지만 권장하지 않음
        Optional<String> optStrVal = Optional.empty();
        Optional<Integer> optIntVal = Optional.empty();

14-2. Optional 객체의 값 가져오기

  • get()
    • 값이 null인 경우 NoSuchElementException이 발생하기 때문에 잘 사용하지 않음
  • orElse()
    • 값이 null인 경우, 매개 변수로 입력된 대체 값을 반환

      Optional<String> optVal = Optional.of("abc");
      String str1 = optVal.get(); // str1 = "abc"
      String str2 = optVal.orElse("EMPTY"); // str2 = "abc"
      
      Optional<String> optVal = Optional.ofNullable(null);
      String str1 = optVal.get(); // NoSuchElementException 예외
      String str2 = optVal.orElse("EMPTY"); // str2 = "EMPTY"
  • orElseGet()
    • 값이 null인 경우, 대체할 값을 반환하는 람다식 지정
  • orElseThrow()
    • 값이 null인 경우, 지정된 예외를 발생시키는 메소드
  • orElseGet(), orElseThrow() 둘 다 orElse() 메소드를 변형한 형태로, 반환 값만 있는 Supplier 함수 형식으로 매개 변수를 받음
    T orElseGet(Supplier<? extends T> other)
    T orElseGet(Supplier<? extends T> exceptionSupplier)
    
    Optional<String> optVal = Optional.ofNullable(null);
    // null 이면 () -> new String()과 동일하게 객체 생성
    String str3 = optVal.orElseGet(String::new);
    // NoSuchElementException가 아닌 NullPointerException 예외를 생성하여 throw
    String str4 = optVal.orElseThrow(NullPointerException::new);
  • isPresent()
    • Optional 객체의 값이 null이면 false, null이 아니면 true 반환

      if (Optional.ofNullable(str).isPresent()) {
      	system.out.println(str); // null이면 실행 X
      }
  • ifPresent()
    • 매개 변수로 Consumer 타입(반환 값이 없는 람다식)을 받을 수 있는데, 값이 있으면 람다식을 실행하고, 값이 없으면 아무 작업도 하지 않음

      // null이면 해당 코드 수행 X (반환 없음)
      Optional.ofNullabe(str).ifPresent(System.out::println);

14-3. OptionalInt & OptionalLong & OptionalDouble

  • 기본형 타입의 Wrapper 클래스로, Optional 보다 더 높은 성능 효율을 위해 사용
    public final class OptionalInt {
    	private final int value; // int 타입 기본형 참조 변수
    
    	// 기본 값이 비어있는지(기본 값 0), 아니면 정수 값 0인지 비교하기 위해 필요
    	private final boolean isPresent; //값이 저장되어 있으면 true
    	...
    }
  • get() 메소드
    • OptionalInt ⇒ int getAsInt()
    • OptionalLong ⇒ long getAsLong()
    • OptionalDouble ⇒ double getAsDouble()
    • 주의! Optional가 String인 경우 값이 비어있거나, 초기 값이거나, 초기화 했다면 null이지만, 기본형 int의 경우 기본 값이 0임
      • 진짜 값이 0인지, 아니면 비어있는 OptionalInt 객체를 나타내는 기본 값 0인지는 인스턴스 변수 isPresent를 사용하여 알 수 있음 (true인 경우 실제 값이 저장되어 있는 상태)

참고 : https://velog.io/@yummygyudon/JAVA-람다와-스트림-Lambda-Stream

15. Lambda & Stream 연습

15-1. 메서드를 람다식으로 변환

// 메서드
int max(int a, int b) {
	return a > b ? a : b;
}

// 람다식
// (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);
}

// 람다식
(String name, int i) -> System.out.println(name + "=" + i)
// 메서드
int square(int x) {
	return x * x;
}

// 람다식
// (int x) -> return x * x
(int x) -> x * x
// 메서드
int roll() {
	return (int)(Math.random() * 6);
}

// 람다식
// () ->	return (int)(Math.random() * 6);
() -> (int)(Math.random() * 6)
// 메서드
int sumArr(int[] arr) {
	int sum = 0;
	for(int i : arr) 
		sum += i;
	return sum;
}

// 람다식
(int[] arr) -> {
	int sum = 0;
	for(int i : arr) 
		sum += i;
	return sum;
}
// 메서드
int[] emptyArr() {
	return new int[]{};
}

// 람다식
() -> new int[]{}

15-2. 람다식을 메소드 참조로 변환

// 람다식
(String s) -> s.length()

// 메소드 참조
String::length
// 람다식
() -> new int[]{}

// 메소드 참조
int[]::new
// 람다식
arr -> Arrays.stream(arr)

// 메소드 참조
Arrays::stream
// 람다식
(String str1, String str2) -> strl.equals(str2)

// 메소드 참조
String::equals
// 람다식
(a, b) -> Integer.compare(a, b)

// 메소드 참조
Integer::compare
// 람다식
(String kind, int num) -> new Card(kind, num)

// 메소드 참조
Card::new
// 람다식
(x) -> System.out.println(x)

// 메소드 참조
System.out::println
// 람다식
() -> Math.random()

// 메소드 참조
Math::random
// 람다식
(str) -> str.toUpperCase()

// 메소드 참조
String::toUpperCase
// 람다식
() -> new NullPointerException()

// 메소드 참조
NullPointerException::new
// 람다식
(Optional opt) -> opt.get()

// 메소드 참조
Optional::get
// 람다식
(StringBuffer sb, String s) -> sb.append(s)

// 메소드 참조
StringBuffer::append
// 람다식
(String s) -> System.out.println(s)

// 메소드 참조
System.out::println

15-3. 괄호 안에 알맞은 함수형 인터페이스 찾기

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

// 정답 => BinaryOperator (매개 변수가 int 2개, 반환 값 int 1개)

/* 기본 함수형 인터페이스 */
// Supplier => 매개 변수 X, 반환 값 O
// Consumer => 매개 변수 O, 반환 값 X
// Function => 매개 변수 O, 반환 값 O (일반적)
// Predicate => 매개 변수 1개, boolean 타입 반환 값 1개

/* 매개 변수가 2개인 함수형 인터페이스 */
// BiConsumer => 매개 변수 2개, 반환 값 X
// BiFunction => 매개 변수 2개, 반환 값 1개
// BiPredicate => 매개 변수 2개, boolean 타입 반환 값 1개

/* 매개 변수 타입과 반환 값 타입이 같은 함수형 인터페이스 */
// UnaryOperator => 매개 변수 2개, 반환 값 X
// BinaryOperator => 매개 변수 2개, 반환 값 1개

15-4. 문자열 배열 strArr의 모든 문자열의 길이를 더한 결과 출력

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

// 실행결과 => sum = 10

/* 정답 */
// 스트림 생성
Stream<String> strStream = Stream.of(strArr);

// mapToInt를 통해 Stream<String>을 IntStream으로 변환하여 sum() 메소드 사용
// 람다식
int sum = strStream.mapToInt(s -> s.length()).sum();
// 메소드 참조
int sum = strStream.mapToInt(String::length).sum();

System.out.println(sum);

15-5. 문자열 배열 strArr의 문자열 중 가장 긴 것의 길이 출력

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

// 실행결과 => 4

/* 정답 */
Stream<String> strStream = Stream.of(strArr);

/* 가장 긴 문자열의 길이 출력
 * strStream.map(String::length) => 각 요소들의 길이로 형태 변환
 * .sorted(Comparator.reverseOrder()) => 각 요소들의 길이를 기준으로 내림차순 정렬
 * .limit(1).forEach(System.out::println) => 정렬된 요소들 중 맨 앞의 1개만 출력
 */
strStream.map(String::length).sorted(Comparator.reverseOrder()).limit(1)
																											.forEach(System.out::println);

/* 가장 긴 문자열 출력
 * strStream.sorted(Comparator.comparingInt(String::length).reversed())
 *   => 각 요소들의 길이를 기준으로 내림차순 정렬
 * .limit(1).forEach(System.out::println) => 정렬된 요소들 중 맨 앞의 1개만 출력
 */
strStream.sorted(Comparator.comparingInt(String::length).reversed()).limit(1)
																											.forEach(System.out::println);
/* Comparator 기본 */
// comparing(Function<T, U> keyExtractor)
// comparing(Function<T, U> keyExtractor, Comparator<U> keyComparator)

/* 스트림과의 비교 대상이 기본형인 경우 */
// comparingInt(ToIntFunction<T> keyExtractor) => int 타입 비교
// comparingLong(ToLongFunction<T> keyExtractor) => Long 타입 비교
// comparingDouble(ToDoubleFunction<T> keyExtractor) => Double 타입 비교

/* 정렬 조건이 두 개 이상인 경우 => thenComparing()으로 체이닝  */
// sorted(Comparator.comparing().thenComparing().thenComparing()…thenComparing())

15-6. 임의의 로또 번호(1~45)를 정렬해서 출력

/* 실행결과
 * 1
 * 20
 * 25
 * 33
 * 35
 * 42
 */

/*
 * new Random().ints(1, 46).distinct() => 1~46사이(46은 포함 X) 랜덤한 정수 중 중복 제거
 * .limit(6) => 무한 스트림이기 때문에 6개로 제한
 * .sorted().forEach(System.out::println) => 정렬 후 출력
 */
IntStream intStream = new Random().ints(1, 46).distinct();
intStream.limit(6).sorted().forEach(System.out::println);

/* 무한 스트림 */
// IntStream ints(int begin, int end) // begin <= ints() < end
// LongStream longs(long begin, long end) // begin <= longs() < end
// DoubleStream doubles(double begin, double end) // begin <= doubles() < end

/* 유한 스트림 */
// IntStream ints(long streamSize, int begin, int end) // begin <= ints() < end
// LongStream longs(long streamSize, long begin, long end) // begin <= longs() < end
// DoubleStream doubles(long streamSize, double begin, double end) // begin <= doubles() < end

15-7. 두 개의 주사위를 굴려 나온 눈의 합이 6인 경우 모두 출력

/* 실행결과
 * [1,5]
 * [2,4]
 * [3,3]
 * [4,2]
 * [5,1]
 */

/* 정답 */
IntStream.rangeClosed(1, 6) // 1~6의 범위를 가진 IntStream 생성
         .boxed() // IntStream => Stream<Integer>로 오토박싱
					// 아래의 flatMap은 dice1과 dice2의 조합으로 나올 수 있는 모든 조합을 하나의 스트림으로 평탄화한 것
					// IntStream이 아닌 Stream<Integer>를 사용한 이유는 int[]에 있는 각 int 타입 값을 활용하기 위함
         .flatMap(dice1 -> IntStream.rangeClosed(1, 6) // 1~6의 값을 가지는 IntStream
									.mapToObj(dice2 -> new int[]{dice1, dice2})) // 1~6의 값을 가지는 IntStream을 모든 주사위의 경우의 수가 들어간 Stream<Integer[]>로 오토박싱
         .filter(dice -> dice[0] + dice[1] == 6) // 두 주사위의 합이 6인 것만 필터링
         .forEach(dice -> System.out.println("[" + dice[0] + ", " + dice[1] + "]"));

15-8. 불합격(150점 미만) 학생 수를 남자와 여자를 구별하여 출력

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

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

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

	// groupingBy()에서 사용 성적을 상,중,하 세 단계로 분류
	enum Level {
		HIGH, MID, LOW
	}
}

class Exercise {
	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)
					}; 

			/* 답변 작성 START */
			Map<Boolean, Map<Boolean, Long>> failedStuBySex 
				= Stream.of(stuArr).collect(partitioningBy(Student::isMale, // (2) 남학생 여학생 2분할
																			partitioningBy(s -> s.getScore() < 150, // (1) 150점 미만(불합격) 2분할
																				counting()))); 
			/* 답변 작성 END */

			// partitioningBy => true or false 2분할
			// groupingBy => 조건에 따른 N분할

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

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

/* 실행결과
 * 불합격[남자]:2명
 * 불합격[여자]:4명
 */

15-9. 각 반별 총점을 학년 별로 나누어 출력

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

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

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

	enum Level {
		HIGH, MID, LOW
	}
}

class Exercise {
	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)
											}; 

		/* 답변 작성 START */
		Map<Integer, Map<Integer, Long>> totalScoreByHakAndBan 
			= Stream.of(stuArr).collect(groupingBy(Student::getHak, // (2) 각 학년별 그룹핑
																		groupingBy(Student::getBan, // (1) 각 반별 그룹핑
																			summingLong(Student::getScore))));
		/* 답변 작성 END */

		for(Object e : totalScoreByHakAndBan.entrySet()) {  
			System.out.println(e); 
		} 
	} 
}
profile
공부겅부

0개의 댓글