Java/Java의 정석

Chapter 14 람다와 스트림

계란💕 2022. 3. 4. 12:00

ㅇ1. 람다식(Lambda expression)

  1.1 람다식이란?

  Def) 람다식: 함수(메서드)를 간단한 식(expression)으로 표현하는 방법

  - 람다식은 사실 익명 클래스의 객체와 동등하다. 

    -> 람다식을 다루기 위한  참조 변수가 필요하다.

    

  - 함수와 메서드의 차이

    -> 근본적으로 동일, 함수는 일반적 용어, 메서드는 객체지향개념 용어

    -> 함수는 클래스에 독립적, 메서드는 클래스에 종속적이다.

 

  1.2 람다식 작성하기 

    1) 메서드의 이름과 반환 타입을 제거하고 '->'을 블록 앞에 추가한다.

    2) 반환값이 있는 경우, 식이나 만 적고 return문을 생략가능하다. (세미콜론 안 붙인다.)

    3) 매개변수 타입이 추론 가능하면 생략 가능하다. (대부분 생략 가능)

 

  - 작성 시 주의사항

    1) 매개 변수가 하나인 경우, 괄호 생략 가능 (타입이 없을 때만)

    2) 블록 안의 문장이 하나뿐일 때, 괄호 생략 가능 (세미콜론 생략)

      -> 단, 하나 뿐인 문장이 return 문이면 괄호 생략할 수 없다.

 

  1.3 함수형 인터페이스 (Functional Interface)

  Def) "단 하나의 추상 메서드"만 선언된 인터페이스

  • 그래야 람다식인터페이스의 메서드가 1:1로 연결될 수 있기 때문이다.
  • 익명 클래스의 객체와 동등하다. 
  • 함수형 인터페이스 타입의 참조 변수로 람다식을 참조할 수 있음
  • 람다식을 다루기 위해 사용한다.

 

  • 아래 코드를 보면 람다식을 적용한 두 번째 코드가 훨씬 간단하다. 
<hide/>
Collections.sort(list, new Comparator<String>() {
    @Override
    public int compare(String o1, String o2) {
        return o1.compareTo(o2);
    }
});

Collections.sort(list, (a, b) -> a.compareTo(b));

 

 

 

  Ex)

<hide/>
@FunctionalInterface
interface MyFunction{
	void run();	// public abstract void run(); - 추상메서드
}
public class LambdaEx1 {
	static void execute(MyFunction f) {	// 매개변수 타입이 MyFunction인 메서드
		f.run();
	}
	static MyFunction getMyFunction() {	// 반환타입이 MyFunction인 메서드
		MyFunction f = () -> System.out.println("f3.run()");
		return f;
	}
	public static void main(String[] args) {
		//람다식으로 MyFunction의 run을 구현
		MyFunction f1 = () -> System.out.println("f1.run()");
		MyFunction f2 = new MyFunction() {	// 익명클래스로 run()을 구현
			public void run() {	//public을 반드시 붙여야한다.
				System.out.println("f2.run()");				
			}			
		};
	MyFunction f3 = getMyFunction();
	f1.run();
	f2.run();
	f3.run();
	execute(f1);
	execute( () -> System.out.println("run()") );
	}
}

  Note) 실행 결과

  - 람다식을 참조 변수로 다룰 수 있다. -> 메서드를 통해 람다식을 주고 받을 수 있다.

    -> 사실, 메서드가 아니라 객체를 주고 받는 것이다. 

 

  - 람다식의 타입과 형변환

    -> 람다식은 오직 함수형 인터페이스로만 형변환이 가능하다.

    Ex)

<hide/>
@FunctionalInterface
interface MyFunction {
	void myMethod();
}
public class LambdaEx2 {
	public static void main(String[] args) {
		MyFunction f = ()->{};	// MyFunction f = (MyFunction)(()->{});
		Object obj = (MyFunction)(() -> {});	//Object타입으로 형변환이 생략됨
		String str = ((Object)(MyFunction)(() -> {})).toString();
		
		System.out.println(f);
		System.out.println(obj);
		System.out.println(str);
		
//		System.out.println(()->{});	// 에러 람다식은 Object 타입으로 형변환이 되지 않는다.
		System.out.println((MyFunction)() -> {});
//		System.out.println((MyFunction)(()->{}).toString());
		System.out.println(((Object)(MyFunction)(()->{})).toString());
	}
}

  Note) 실행 결과

  - 결과를 보면 컴파일러가 람다식의 타입을 어떤 형식으로 만들어내는지 알 수 있다.

  - 일반적인 익명 객체라면 "외부클래스이름$번호" 로 타입이 결정된다.

    -> 람다식의 타입은  '외부클래스이름$$Lambda$번호' 

 

  1.4 java.util.function 패키지

  • 자주 사용되는 다양한 함수형 인터페이스를 제공한다.
  • 함수형 인터페이스

    -> Supplier<T>: 매개변수 없고 반환값 있음

    -> Consumer<T>: 매개변수 있고 반환값 없음

    -> Functiona<T, R>: 일반적인 함수, 하나의 매개변수를 받아서 결과를 반환한다.

    -> Predicate<T>: 조건식을 표현하는데 사용된다. 매개변수 하나, 반환타입은 boolean

 

  - 매개 변수가 두 개인 함수형 인터페이스

    -> BiConsumer<T, U>: 두 개의 매개변수만 있고 반환값이 없다.

    -> BiPredicate<T, U>: 조건식을 표현하는데 사용ㅎ된다.

    -> BiFuntion<T, U, R>: 두 개의 매개변수T, U를 받아서 하나의 결과 R을 반환한다.

 

  Note) 3개의 매개변수가 필요하다면?

    -> 직접 함수형 인터페이스를 선언한다.

 

    - 매개변수의 타입과 반환타입이 일치하는 함수형 인터페이스

    -> UnaryOperator<T>: Funtion의 자손, Function과 달리 매개변수와 결과의 타입이 같다,

    -> BinaryOperator<T>: BiFuntion의 자손, BiFuntion과 달리 매개변수와 결과의 타입이 같다.

 

  - 컬렉션 프레임웍과 함수형 인터페이스

    -> removeIf(): 조건에 맞는 요소를 삭제

    -> replaceAll(): 모든 요소를 변환하여 대체하다.

    -> forEach(): 모든 요소에 작업 action을 수행

    -> compute(): 지정된 키의 값에 작업 f 를 수행

    ->compiteIfAbsent(): 키가 없으면 작업 f 수행 후 추가

    ->compiteIfPresent(): 지정된 키가 있을 때, 작업 f수행

    -> merge(): 모든 요소에 병합 작업 action을 수행

    -> replaceAll(); 모든 요서에 치환 작업 f를 수행

    -> 즉, 'compute' 는 맵의 value를 변환, 'merge'는 Map을 병합

 

  Ex)

<hide/>
import java.util.*;
public class LambdaEx4 {
	public static void main(String[] args) {
		ArrayList<Integer> list = new ArrayList<>();
		for(int i = 0; i < 10; ++i)
			list.add(i);
		// list의 모든 요소 출력
		list.forEach(i->System.out.print(i +","));
		System.out.println();
		
		// list에서 2 또는 3의 배수를 제거한다.
		list.removeIf(x -> x % 2 == 0 || x % 3 == 0);
		System.out.println(list);
		
		list.replaceAll(i-> i * 10);
		System.out.println(list);
		
		Map<String, String> map = new HashMap<>();
		map.put("1", "1");
		map.put("2", "2");
		map.put("3", "3");
		map.put("4", "4");
		
		// map의 모든 요소를 {k, v}의 형식으로 출력한다.
		map.forEach( (k, v) -> System.out.print( "{" + k + ", "+ v + "}," ));
		System.out.println();
	}
}

  Note) 실행결과

  - 메서드의 사용법 알 수 있다. 

 

 - 기본형을 사용하는 함수형 인터페이스

    -> DoubleToIntFunction: AToBFunction은 입력이 A타입 , 출력이 B타입 

    -> ToIntFunction<T>: ToBFunction은 출력이 B타입디아. 입력은 지네릭 타입

    -> IntFunction<R>: AFunction은 입력이 A타입이고 출력은 지네릭 타입

    -> ObjIntConsumer<T>: ObjAFunction은 입력이 T, A타입이고 출력은 없다.

 

  1.5 Function의 합성과 Predicate의 결합

  - 등가비교를 위한 Predicate의 작성에는 isEqual()를 사용한다. (static메서드 )

  - Function의 합성

  - Predicate의 결합

    -> and(), or(), negate()로 두 Prediacte를 하나로 결합 (default 메서드 / static 메서드 / 추상 메서드 가능)

  Ex)

<hide/>
import java.util.function.*;
public class LambdaEx7 {
	public static void main(String[] args) {
		Function<String, Integer>  f = (s) -> Integer.parseInt(s, 16);
		Function<Integer, String>  g = (i) -> Integer.toBinaryString(i);
		
		Function<String, String> h = f.andThen(g);
		Function<Integer, Integer> h2 = f.compose(g);
		
		System.out.println(h.apply("FF"));	// "FF"-> 255 -> "11111111"
		System.out.println(h2.apply(2));	// 2 -> "10" -> 16
		
		Function<String, String> f2 = x -> x;	//항등함수
		System.out.println(f2.apply("AAA"));	//AAA가 그대로 출력된다.
		
		Predicate<Integer> p = i -> i < 100;
		Predicate<Integer> q = i -> i < 200;
		Predicate<Integer> r = i -> i % 2 == 0;
		Predicate<Integer> notP = p.negate(); // i >= 100
		
		Predicate<Integer> all = notP.and(r.or(r));
		System.out.println(all.test(150));
		String str1 = "abc";
		String str2 = "abc";
		
		Predicate<String> p2 = Predicate.isEqual(str1);
		boolean result = p2.test(str2);
		System.out.println(result);
	}
}

  Note) 실행 결과

  - h = g(f) : (16) -> (10) -> (2)

  - h2 = f(g) : (2) -> (10) -> (16)

  - f2는 항등함수

 

  1.6 메서드 참조(method reference)

  - 하나의 메서드만 호출하는 람다식은 '메서드 참조'로 간단히 할 수 있다.

    -> (참조변수) :: (메서드이름) 또는 (클래스이름) :: (메서드이름) 으로 바꿀 수 있다.  

  - 람다식을 더 간단히 했다.

  - 형식: (클래스이름) :: (메서드이름) 

    -> static 메서드 참조

    -> 인스턴스 메서드 참조

    -> 특정 객체 인스턴스메서드 참조 

  - 생성자의 메서드 참조 

    -> 람다식도 메서드 참조로 변환할 수 있다. 

    -> 메서드 참조는 람다식을 마치 static변수처럼 다룰 수 있게 한다. 

 

 

2. 스트림(Stream)

  2.1 스트림이란?

<hide/>
데이터소스객체.Stream생성().중개연산().최종연산();

  Def) 다양한 데이터 소스를 표준화된 방법으로 다루기 위한 것

  - 배열, 컬렉션 등의 데이터를 하나씩 참조하여 처리 가능한 기능

  - for문, Iterator를 쓰지 않아도 되서 간결하다.

 

  1) 스트림 생성

  2) 중간 연산(= 중개 연산, 0~n번): 연산 결과가 스트림이 연산, 스트림에 연속해서 중간 연산할 수 있다.

  3) 최종 연산 (0~1번): 연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 단 한번만 가능하다.

 

  - 스트림의 특징

    -> 스트림은 데이터 소스로부터 데이터를 읽기만할 뿐 변경하지 않는다.

    -> 스트림은 Iterator처럼 일회용이다. (필요하면 다시 스트림을 생성해야한다.)

    -> 최종 연산 전까지 중간 연산이 수행되지 않는다. - 지연된 연산 때문이다.

    -> 스트림은 작업을 내부 반복으로 처리한다.

    -> 스트림 작업을 병렬로 처리한다. - 병렬 스트림

    -> 기본형 스트림 - IntStream, LongStream, DoubleStream : 오토 박싱 & 언박싱의 비효율 제거(IntStream사용)

      - IntStream: 숫자와 관련된  유용한 메서드를 Stream<T>보다 더 많이 제공한다.

      - Stream<T>:요소의 타입이 T인 스트림

 

  2.2 스트림 만들기

  - 컬렉션: Collection 인터페이스에 정의된 메서드 stream()으로 컬렉션스트림으로 변환

  - 배열: 객체 배열로부터 스트림 생성하기

    ->  배열을 소스로 하는 스트림을 생성하는 메서드는 Stream과 Arrays에 static메서드로 정의되어 있다.

  - 임의의 수(난수 스트림): 난수를 요소로 갖는 스트림 생성하기

    -> 지정된 범위의 난수를 요소로 갖는 스트림을 생성하는 메서드(Random클래스) 

    -> 특정 범위의 정수를 요소로 갖는 스트림 생성하기 (IntStream, LongStream)

      - 람다식을 소스로 하는 스트림 생성하기

      - iterate()는 이전 요소를 seed로 해서 다음 요소를 계산한다.

      - generate()는 seed를 사용하지 않는다.

      - limit은 무한 스트림을 유한 스트림으로 만들어준다.

  - 람다식 - iterate() , generate()

     -> 람다식을 매개 변수로 받아서 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성한다.

    -> iterate(): 씨앗값(seed)으로 지정된 값부터 시작해서 람다식 f에 의해 계산결과를 다시 seed로 해서 반복한다.

    -> generate(): iterate()처럼 무한스트림을 생성해서 반환하지만 이전 결과를 이용해서 다음 요소를 계산하지 않는다.

  - 빈 스트림: 파일을 소스로 하는 스트림 생성하기

    -> lines(): 파일 내요을 한 줄씩 읽어서 스트림 요소로 만든다.

    -> 요소가 하나도 없는 비어있는 스트림 생성하기

  - forEach()는 스트림의 요소를 소모하면서 작업을 수행하므로 같은 스트림에 forEach()를 두 번 이상 호출 부락능하다. 

 

  2.3 스트림의 중간 연산

  - 스트림 자르기 - skip(), limit()

  - 스트림의 요소 걸러내기 - filter(): 조건에 맞지 않는 요소 제거, distinct(): 중복 제거

  - 중간 연산: 연산 결과가 스트림인 연산, 반복적으로 적용가능

  - 스트림 정렬하기 - sorted() 

  - Comparator의 comparing()으로 정렬 기준을 제공

  - 정렬 기준이 여러 개일 때는 thenComparion() 을 사용한다.

  - 스트림의 요소 변환하기 - map()

   -> 요소에 저장된 값 중 원하는 값만 뽑아내거나 특정 형태로 변환해야 할 때,

  - 스트림의 요소를 소비하지 않고 엿보기 - peek()

    -> 연산과 연산 사이에 올바르게 처리되었는지 확인할 때 이용한다.

  - 스트림의 스트림스트림으로 변환 - flatMap()
  - range(1, 10): 1~9까지 데이터가 만들어진다.

 

  Ex)

<hide/>
import java.util.stream.*;
import java.util.*;
public class StreamEx1 {
	public static void main(String[] args) {
        Stream<Student> studentStream = Stream.of(
            new Student("이자바" , 3, 300),
            new Student("김자바" , 1, 200),
            new Student("안자바" , 2, 100),
            new Student("박자바" , 2, 150),
            new Student("소자바" , 1, 200),
            new Student("나자바" , 3, 290),
            new Student("김자바" , 3, 180));
            studentStream.sorted(Comparator.comparing(Student::getBan)	//반별 정렬
                .thenComparing(Comparator.naturalOrder()))	// 기본 정렬
                .forEach(System.out::println);
	}
}
class Student implements Comparable<Student>{
	String name;
	int ban;
	int totalScore;
	Student(String name, int ban, int totalScore ){
		this.name = name;
		this.ban = ban;
		this.totalScore = totalScore;
	}
	public String toString() {
		return String.format("[%s, %d, %d]", name, ban, totalScore);
	}
	String getName()	{return name;}
	int getBan() 	{return ban;}
	int getTotalScore()	{return totalScore;}
// 	총점 내림차순을 기본 정렬로 한다.
	public int compareTo(Student s) {
		return s.totalScore - this.totalScore;
	}
}

  Note) 실행 결과

  - 학생의 성적 정보를 요소로 하는 Stream<Student>을 반별로 정렬한 다음에 총점별 내림차순으로 정렬한다.

 

  2.4 Optional<T>와 Optionallnt

  - Optional: 'T 타입의 객체'를 감싸는 래퍼클래스

    ->T타입: 모든 타입의 객체 저장가능, null도 저장 가능

    - Optional<T>가 필요한 이유

    1) null을 직접 다루는 건 위험하다. (NullPointerException 발생 위험 때문에 )

      ex) String str = ""; (null을 쓰지 않고 빈 문자열로 넣는다. )

    2) null 체크하려면 if 문 필수

 

  - Optional<T>객체 생성하기 

    -> null대신 빈 Optional<T>객체를 사용하자

 

  - Optional<T>객체의 값 가져오기 

    -> get(): Optional객체에 저장된 값 가져온다. 

    -> orElse(): 예외가 발생했을 때 대체할 값을 지정 가능

    -> orElseGet(): null을 대체할 값을 반환하는 람다식을 지정할 수 있다.

    -> orElseThrow(): null일 때 발생시킬 예외의 종류를 지정 가능하다.

 

  - isPresent(): Optional객체의 값이 null이면 false, 아니면 true를 반환

  - Optionallant, OptionalLong, OptionalDouble - 기본 값을 감싸는 래퍼클래스

    -> Optionallnt의 값 가져오기 - int getAslant()

    -> 빈 Optional객체와의 비교

 

  2.5 최종 연산

  - 최종 연산: 연산 결과가 스트림이 아닌 연산, 단 한번만 적용가능(스트림의 요소를 소모)

    -> 스트림은 한 번 최종 연산하면 스트림 연산이 종료된다.

<hide/>
        System.out.println("== 컬렉션 스트림 ==");
        ArrayList list1 = new ArrayList(Arrays.asList(1, 2, 3));
        System.out.println("list1 = " + list1);

        System.out.println("== to Stream ==");
        Stream stream2 = list1.stream(); 
// 최종연산 stream2.forEach(System.out::println);
        stream2.forEach(num -> System.out.println("num = "+ num)); 	// 최종연산

  Note) 실행 결과

    -> reduce(stream의 요소를 하나씩 줄여가며 연산한다.)와 collect(reduce 이용한 그룹 작업)가 최종 연산의 핵심이다.

  - 스트림의 모든 요소에 지정된 작업을 수행 - forEach(),  forEachOredered()

    -> 둘이 같지만 병령인 경우에만 다르다. ( forEachOredered()는 순서를 보장한다. )

  - 조건 검사 - allMatch(): 모든 요소가 조건을 만족시키면 true,

                    anyMatch(): 한 요소라도 조건을 만족시키면 true,

                    noneMatch(): 모든 요소가 조건을 만족시키지 않으면 true

  - 조건에 일치하는 요소 찾기

    -> findFirst(): 첫 번째 요소를 반환 (순차 스트림에 사용),

    -> findAny(): 아무거나 하나를 반환 (병렬 스트림에 사용)

 

  - 스트림의 최종 연산 - reduce()

    - reduce(): 스트림의 요소를 하나씩 줄여가며 누적 연산 수행

    -> identity: 초기값

    -> accumulator: 이전 연산 결과의 스트림의 요소에 수행할 연산

    -> combiner: 병렬 처리된 결과를 합치는데 사용할 연산(병렬 연산)

 

 

  Ex)

<hide/>
import java.util.*;
import java.util.stream.*;
public class StreamEx5 {
	public static void main(String[] args) {
		String[] strArr = {
				"Inheritance", "Java", "Lambda", "Stream",
				"OptionalDouble", "IntStream", "count", "sum"
		};
		Stream.of(strArr).forEach(System.out::println);
		
		boolean noEmptyStr = Stream.of(strArr).noneMatch(s->s.length()==0);
		Optional<String> sWord = Stream.of(strArr).filter(s->s.charAt(0)=='s').findFirst();
		
		System.out.println("noEmptyStr = " + noEmptyStr );
		System.out.println("sWord = " + sWord.get());
        
		// Stream<String[]>을 IntStream으로 변환
		IntStream intStream1 = Stream.of(strArr).mapToInt(String::length);
		IntStream intStream2 = Stream.of(strArr).mapToInt(String::length);
		IntStream intStream3 = Stream.of(strArr).mapToInt(String::length);
		IntStream intStream4 = Stream.of(strArr).mapToInt(String::length);
		
		int count = intStream1.reduce(0, (a, b) ->a + 1);
		int sum = intStream2.reduce(0, (a, b) -> a + b);
		
		OptionalInt max = intStream3.reduce(Integer::max);
		OptionalInt min = intStream4.reduce(Integer::min);
		
		System.out.println("count = "+ count);
		System.out.println("sum = "+ sum);
		System.out.println("max = "+ max.getAsInt());
		System.out.println("min = " + min.getAsInt());
	}	
}

  Note) 실행 결과

 

 

  Ex) 스트림 연산

<hide/>
// Java 프로그래밍 - 스트림

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.OptionalInt;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class Main {

    public static void main(String[] args) {

//      1. 스트림 생성

//      1-1. 배열 스트림
        System.out.println("== 배열 스트림 == ");
        String[] arr = new String[]{"a", "b", "c"};

        System.out.println("== fori ==");
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }

        System.out.println("== forEach ==");
        for (String item: arr) {
            System.out.println(item);
        }

        System.out.println("== to Stream ==");
        Stream stream1 = Arrays.stream(arr);
        stream1.forEach(System.out::println);



//      1-2. 컬렉션 스트림
        System.out.println("== 컬렉션 스트림 ==");
        ArrayList list1 = new ArrayList(Arrays.asList(1, 2, 3));
        System.out.println("list1 = " + list1);

        System.out.println("== to Stream ==");
        Stream stream2 = list1.stream();
     // stream2.forEach(System.out::println);
        stream2.forEach(num -> System.out.println("num = "+ num));


//      1-3. 스트림 builder
        System.out.println("== 스트림 builder ==");
        Stream streamBuilder = Stream.builder().add(1).add(2).add(3).build();
        streamBuilder.forEach(System.out::println);



//      1-4. 스트림 generate
        System.out.println("== 스트림 generate ==");
        Stream streamGenerate = Stream.generate( () -> "abc").limit(3); // 세번 반복하여 출력한다.
        streamGenerate.forEach(System.out::println);



//      1-5. 스트림 iterate
        System.out.println("== 스트림 iterate ==");
        Stream streamIterator = Stream.iterate(10, n -> n * 2).limit(3);// 초기값 10,
        streamIterator.forEach(System.out::println);


//      1-6. 기본 타입 스트림
        System.out.println("== 기본타입 스트림 ==");
        IntStream intStream = IntStream.range(1, 5);
        intStream.forEach(System.out::println);


//      2. 스트림 중개 연산

//      2-1. Filtering
        System.out.println("== Filtering ==");
        IntStream intStream2 = IntStream.range(1, 10).filter(n -> n % 2 == 0);  // 짝수만 필터링
        intStream2.forEach(System.out::println);


//      2-2. Mapping    필터링과 다르게 각각의 원소들을 연산해서 다시 만들어준다.
        System.out.println("== Mapping ==");
        IntStream intStream3 = IntStream.range(1, 10).map(n -> n + 1);
        intStream3.forEach(n -> System.out.print(n + " "));
        System.out.println();


//      2-3. Sorting
        System.out.println("== Sorting ==");
        IntStream intStream4 = IntStream.builder().add(5).add(1).add(3).add(4).add(2).build();
        IntStream intStreamSort = intStream4.sorted();
        intStreamSort.forEach(System.out::println);



//      3. 최종 연산

//      3-1. Sum, Average
        System.out.println("== sum, average ==");
        int sum = IntStream.range(1, 5).sum();
        System.out.println("sum = "+ sum);
        double average = IntStream.range(1, 5).average().getAsDouble();
        System.out.println("average = " + average);


//      3-2. Min, Max
        System.out.println("== min, max ==");
        int min = IntStream.range(1, 5).min().getAsInt();
        System.out.println("min = "+ min);
        int max = IntStream.range(1, 5).max().getAsInt();
        System.out.println("max = "+ max);



//      3-3. reduce
        System.out.println("== reduce ==");
        Stream<Integer> stream3  = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)).stream();  // 스트림으로 바꿘준다.
        System.out.println(stream3.reduce((x, y) -> x + y).get());      // get()으로 데이터를 가져온다.
        // reduce는 연쇄적으로 더해서 데이터를 뽑아내는 기능이다.


//      3-4. forEach
        System.out.println("== forEach == ");
        IntStream.range(1, 10).filter(n -> n == 5).forEach(System.out::println);
        // 5를 가져와서 출력한다. 
    }
}

  Note) 실행 결과

 

 

  2.6 collect()

  - collect() : 스트림의 최종 연산, 매개변수로 컬렉터를 필요로 한다. 

    -> Collector를 매개변수로 하는 스트림의 최종연산

  -  컬렉터는 인터페이스,'Collector'를 구현해야한다.

    -> 수집에 필요한 메서드를 정의해 놓은 인터페이스

  - Collectors: 클래스, static 메서드로 미리 작성된 컬렉터를 제공한다. 

    -> 다양한 기능의 컬렉터(Collector를 구현한 클래스)를 제공한다. 

  - 스트림을 컬렉션 또는 배열로 변환 - toList(), toSet(), toMap(), toCollection(), toArray(), toArray() 스트림을 배열로 만든다.

  - 통계 - counting(), summingInt(), averageingInt(), maxBy(), minBy()

  - 리듀싱 - reducing()

    -> Collectors에 있다. 

  - 문자열 결합 - joining

  - 그룹화와 분할 - groupingBy(), partitioningBy()

  - partitioningBy(): 스트림을 2분할 한다.

  - groupingBy(): 스트림을 n분할 한다.

 

  Ex)

<hide/>
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	}		//성적을 상, 중, 하 세 단계로 분류
}
public class StreamEx7 {
	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)
			};

			System.out.printf("1. 단순분할 (성별로 분할) %n");
			Map<Boolean, List<Student>>	stuBySex = Stream.of(stuArr).collect(partitioningBy(Student::isMale));
			
			List<Student> maleStudent = stuBySex.get(true);
			List<Student> femaleStudent = stuBySex.get(false);
			
			for(Student s: maleStudent) 	System.out.println(s);
			for(Student s: femaleStudent) 	System.out.println(s);
			
			System.out.printf("%n2.단순분할 + 통계(성별 학생수)%n");
			Map<Boolean, Long> stuNumBySex = Stream.of(stuArr).collect(partitioningBy(Student::isMale, counting()) );
			
			System.out.println("남학생 수: " + stuNumBySex.get(true));
			System.out.println("여학생 수: " + stuNumBySex.get(false));
			
			System.out.printf("%n3.단순분할 + 통계(성별 1등 )%n");
			Map<Boolean, Optional<Student>> topScoreBySex = Stream.of(stuArr).collect(partitioningBy(Student::isMale, maxBy(comparingInt(Student::getScore))));
			
			System.out.println("남학생 1등: " + topScoreBySex.get(true));
			System.out.println("여학생 1등: " + topScoreBySex.get(false));
			
			Map<Boolean, Student> topScoreBySex2 = Stream.of(stuArr).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));
			System.out.printf("%n4. 다중분할(성별 불합격자, 100점 이하)%n");
			
			Map<Boolean,  Map<Boolean, List<Student>>> failedStuBySex = Stream.of(stuArr).collect(partitioningBy(Student::isMale, partitioningBy(s -> s.getScore() <= 100)));
			
			List<Student> failedMaleStu = failedStuBySex.get(true).get(true);
			List<Student> failedFemaleStu = failedStuBySex.get(false).get(true);
			
			for(Student s: failedMaleStu)	System.out.println(s);
			for(Student s: failedFemaleStu)	System.out.println(s);
	}
}

  Note) 실행 결과

  - 1. 기본분할: partitioningBy(Student::isMale) 

    -> 학생들을 성별로 분할

  - 2. 기본 분할 + 통계 정보

    -> partitioningBy(Student::isMale ,counting())

    ->counting()을 이용해서 남 여 학생 수를 구한다.

  - maxBy()는 반환타입이 Optional<Student>이다. 

    -> 반환 결과를 <Strudent>로 얻으려면 collectiongAndThen()과 Optional::get을 함께 이용한다. 

  - 성적 150점 이하인 학생들은 불합격자처리하고 성별로 분류하여 얻어내려면 ?

    -> partitioningBy()를 이용해서 이중분할하면 된다. 

  - groupingBy()에 의한 분류

Map<Integer, List<Student>> stuByBan = stuStream.collect(groupingBy(Student::getBan));

 

  2.7 Collector 구현하기 

  - 컬렉터 작성은 Collector인터페이스를 구현하는 것을 의미한다. 

    -> supplier() : 작업 결과를 저장할 공간 제공

    -> accumulator(): 스트림의 요소를 수집할 방법을 제공

    -> combiner(): 두 저장공간을 병합할 방법을 제공 (병렬 스트림)

    -> finisher(): 결과를 최종적으로 변환할 방법을 제공

 

    -> Characteristics.CONCURRENT: 병렬로 처리할 수 있는 작업

    -> Characteristics.UNORDERED: 스트림의 요소의 순서가 유지될 필요가 없는 작업

    -> Characteristics.IDENTITY_FINISH: finisher()가 항등 함수인 작업

 

  2.8 스트림의 변환