https://www.udemy.com/course/best-javascript-data-structures/

 

테스트 값

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public class SortEx01 {
	public static void main(String[] args) {
		ThreadLocalRandom current = ThreadLocalRandom.current();
		Integer[] array = current.ints(0,300).distinct().limit(10).boxed().toArray(Integer[]::new);
		System.out.println(array.getClass().getSimpleName()+"   "+ Arrays.toString(array));
		
		//정렬들 저장용
		List<SortStategy<Integer>> list = new ArrayList<>();
		
		for(SortStategy<Integer> sort : list) 
			System.out.printf("%-20s  %s%n",sort.getClass().getSimpleName(),
					Arrays.toString(sort.sort(array)));
	}
	
	static interface SortStategy<T>{
		T[] sort(T[] arr);
		//자리 바꿈 메서드
		default void swap(Integer[] clone, int i, int j) {
			Integer tmp = clone[i];
			clone[i] = clone[j];
			clone[j] = tmp;
		}
	}
}

 

버블 정렬

큰 값을 뒤로 보내며, 최대반복 수를 점점 줄여간다.

static class BubbleSort implements SortStategy<Integer>{
    static int cnt = 0;

    public Integer[] sort(Integer[] arr) {
        Integer[] clone = arr.clone();

        for(int i=0;i<clone.length-1;i++) {// n -1 번 수행
            for(int j=1;j<clone.length-i;j++) {
                cnt++;
                if(clone[j]<clone[j-1]) {
                    swap(clone, j-1, j);
                }
            }
            System.out.println(Arrays.toString(clone));
        }
        System.out.print("버블소트 cnt("+cnt+")");
        return clone;
    }
}
Integer[]   [113, 247, 171, 173, 243, 35, 66, 223, 85, 83]
[113, 171, 173, 243, 35, 66, 223, 85, 83, 247]
[113, 171, 173, 35, 66, 223, 85, 83, 243, 247]
[113, 171, 35, 66, 173, 85, 83, 223, 243, 247]
[113, 35, 66, 171, 85, 83, 173, 223, 243, 247]
[35, 66, 113, 85, 83, 171, 173, 223, 243, 247]
[35, 66, 85, 83, 113, 171, 173, 223, 243, 247]
[35, 66, 83, 85, 113, 171, 173, 223, 243, 247]
[35, 66, 83, 85, 113, 171, 173, 223, 243, 247]
[35, 66, 83, 85, 113, 171, 173, 223, 243, 247]
버블소트 cnt(45)BubbleSort            [35, 66, 83, 85, 113, 171, 173, 223, 243, 247]

최대 값이 제일 뒤로 가니, 그에 따라 최대 반복 수를 줄여 정렬한다.

 

버블 정렬 최적화

static class BubbleSortOp implements SortStategy<Integer>{
    Boolean noSwap = false;
    static int cnt = 0;

    public Integer[] sort(Integer[] arr) {
        Integer[] clone = arr.clone();

        for(int i=0;i<clone.length-1;i++) {// n -1 번 수행용 반복문
            noSwap = true;
            for(int j=1;j<clone.length-i;j++) {//자리바꿈 루프
                cnt++;
                if(clone[j]<clone[j-1]) {
                    noSwap = false; // 자리바꿈이 한 번이라도 수행됐다.
                    swap(clone, j-1, j);
                }
            }
            if(noSwap) break; //자리 바꿈이 수행된적이 없으면 수행용 반복은 의미없음
        }
        System.out.print("버블소트 최적화 cnt("+cnt+")");
        return clone;
    }
}

더 이상 교환된 값이 없으면, 더 이상 정렬한 값이 없는 것으로 반복문을 빠져나온다.

Integer[]   [286, 172, 22, 197, 169, 62, 236, 99, 141, 156]
버블소트 cnt(45)BubbleSort            [22, 62, 99, 141, 156, 169, 172, 197, 236, 286]
버블소트 최적화 cnt(39)BubbleSortOp          [22, 62, 99, 141, 156, 169, 172, 197, 236, 286]

선택 정렬

지정된 위치에 최적에 값을 넣는다. 

가장 안 좋은 정렬이다. 어떠한 경우에도 n^2 시간 복잡도를 보인다.

//버블 정렬보다 나은 점은 스왑수 최소화, i루프 마다 단 한번의 스왑만 발생
static class SelectionSort implements SortStategy<Integer>{
    public Integer[] sort(Integer[] arr) {
        Integer[] clone = arr.clone();
        for(int i=0;i<clone.length-1;i++) {
            for(int j=i+1;j<clone.length;j++) {
                if(Integer.compare(clone[i], clone[j])>0) {
                    swap(clone, i, j);
                }
            }
        }
        return clone;
    }
}

삽입 정렬

논리적으로 배열안에 서브 배열을 만들어 점차 정렬해가는 정렬

배열의 요소가 1개면, 자연스럽게 정렬된 상태다. 즉, 배열의 요소가 2개부터 정렬이 가능한 상태로 시작 인덱스는 1로 준다.

배열의 요소가 거의 정렬된 상태일 때 매우 빠른 속도를 보인다.

static class InsertSort implements SortStategy<Integer>{
    //논리적으로 순회 마다 작은 서브배열을 만든다고 가정한다.
    //1부터 시작
    public Integer[] sort(Integer[] arr) {
        Integer[] clone = arr.clone();
        //배열이 단 한개는 이미 정렬된 것이다. (논리적 배열)
        for(int i = 1;i<clone.length;i++) {
            //배열 shift 하는 동안 값 손실 때문이 임시 저장
            Integer newMember = clone[i];
            Integer j = i-1;
            //새로운 멤버 clone[i]
            for(;j>=0 && newMember < clone[j];j--) {
                clone[j+1] = clone[j];
            }
            //들어갈 자리 j-- 보정 값 +1
            clone[j+1] = newMember;
        }
        return clone;
    }
}

전체 코드

package udemyjavascript;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public class SortEx01 {
	public static void main(String[] args) {
		ThreadLocalRandom current = ThreadLocalRandom.current();
		Integer[] array = current.ints(0,300).distinct().limit(10).boxed().toArray(Integer[]::new);
		System.out.println(array.getClass().getSimpleName()+"   "+ Arrays.toString(array));
		
		//정렬들 저장용
		List<SortStategy<Integer>> list = new ArrayList<>();
		list.add(new BubbleSort());
		list.add(new BubbleSortOp());
		list.add(new SelectionSort());
//		list.add(new InsertSort());
		
		for(SortStategy<Integer> sort : list) 
			System.out.printf("%-20s  %s%n",sort.getClass().getSimpleName(),
					Arrays.toString(sort.sort(array)));
	}
	
	static interface SortStategy<T>{
		T[] sort(T[] arr);
		//자리 바꿈 메서드
		default void swap(Integer[] clone, int i, int j) {
			Integer tmp = clone[i];
			clone[i] = clone[j];
			clone[j] = tmp;
		}
	}
	
	static class BubbleSort implements SortStategy<Integer>{
		static int cnt = 0;
		
		public Integer[] sort(Integer[] arr) {
			Integer[] clone = arr.clone();
			
			for(int i=0;i<clone.length-1;i++) {// n -1 번 수행
				for(int j=1;j<clone.length-i;j++) {
					cnt++;
					if(clone[j]<clone[j-1]) {
						swap(clone, j-1, j);
					}
				}
			}
			System.out.print("버블소트 cnt("+cnt+")");
			return clone;
		}
	}
	static class BubbleSortOp implements SortStategy<Integer>{
		Boolean noSwap = false;
		static int cnt = 0;
		
		public Integer[] sort(Integer[] arr) {
			Integer[] clone = arr.clone();
			
			for(int i=0;i<clone.length-1;i++) {// n -1 번 수행용 반복문
				noSwap = true;
				for(int j=1;j<clone.length-i;j++) {//자리바꿈 루프
					cnt++;
					if(clone[j]<clone[j-1]) {
						noSwap = false; // 자리바꿈이 한 번이라도 수행됐다.
						swap(clone, j-1, j);
					}
				}
				if(noSwap) break; //자리 바꿈이 수행된적이 없으면 수행용 반복은 의미없음
			}
			System.out.print("버블소트 최적화 cnt("+cnt+")");
			return clone;
		}
	}
	
	//버블 정렬보다 나은 점은 스왑수 최소화, i루프 마다 단 한번의 스왑만 발생
	static class SelectionSort implements SortStategy<Integer>{
		public Integer[] sort(Integer[] arr) {
			Integer[] clone = arr.clone();
			for(int i=0;i<clone.length-1;i++) {
				for(int j=i+1;j<clone.length;j++) {
					if(Integer.compare(clone[i], clone[j])>0) {
						swap(clone, i, j);
					}
				}
			}
			return clone;
		}
	}
	
	static class InsertSort implements SortStategy<Integer>{
		//논리적으로 순회 마다 작은 서브배열을 만든다고 가정한다.
		//1부터 시작
		public Integer[] sort(Integer[] arr) {
			Integer[] clone = arr.clone();
			//배열이 단 한개는 이미 정렬된 것이다. (논리적 배열)
			for(int i = 1;i<clone.length;i++) {
				//배열 shift 하는 동안 값 손실 때문이 임시 저장
				Integer newMember = clone[i];
				Integer j = i-1;
				//새로운 멤버 clone[i]
				for(;j>=0 && newMember < clone[j];j--) {
					clone[j+1] = clone[j];
				}
				//들어갈 자리 j-- 보정 값 +1
				clone[j+1] = newMember;
			}
			return clone;
		}
	}
}
Integer[]   [297, 76, 286, 161, 206, 20, 61, 160, 3, 9]
버블소트 cnt(45)BubbleSort            [3, 9, 20, 61, 76, 160, 161, 206, 286, 297]
버블소트 최적화 cnt(45)BubbleSortOp          [3, 9, 20, 61, 76, 160, 161, 206, 286, 297]
SelectionSort         [3, 9, 20, 61, 76, 160, 161, 206, 286, 297]

 

정렬 속도 비교

https://www.toptal.com/developers/sorting-algorithms

 

Sorting Algorithms Animations

Animation, code, analysis, and discussion of 8 sorting algorithms on 4 initial conditions.

www.toptal.com

 

'자료구조&알고리즘' 카테고리의 다른 글

달리기 경주  (0) 2023.07.14
 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

 

이 책은 개발자를 대상으로 쓰여진 책이기 때문에 흥미를 위해 첫 장부터 리팩터링을 수행하는 방식으로 진행한다.

첫 장부터 리팩터링이 나오니 그 과정을 전부 파악할 순 없다. 그냥 진행되는 과정에 감을 잡는 정도면 충분하다.

 

예제

https://github.com/rkwhr0010/refactoring/tree/main/refactoring/chap01

 

GitHub - rkwhr0010/refactoring: 리팩터링책 내용

리팩터링책 내용. Contribute to rkwhr0010/refactoring development by creating an account on GitHub.

github.com

리팩터링 과정마다 기존 코드를 커밋&푸시해 갱신하는 방식이 아닌 Ex01 ==> Ex02 이렇게 갱신하는 방식으로 따라했다.

 

폴더 구조는 해당 목차에 리팩터링이 없다면, 건너뛴다.

1.1 자, 시작해보자!

	
	plays = {
	    "hamlet":{"name" : "Hamlet", "type" : "tragedy"},
	    "as-like" : {"name":"As You Like It", "type" : "comedy"},
	    "othello":{"name":"Othello", "type":"tragedy"}
	}
	
	invoices = 
	    {
	        "customer" : "BigCo" ,
	        "performances" : [
	            {
	                "playID":"hamlet",
	                "audience" : 55
	            },
	            {
	                "playID" :"as-like",
	                "audience": 35
	            },
	            {
	                "playID":"othello",
	                "audience" :40
	                
	            }
	        ]
	    }
	
	
	<!DOCTYPE html>
	<html lang="en">
	<head>
	    <meta charset="UTF-8">
	    <meta http-equiv="X-UA-Compatible" content="IE=edge">
	    <meta name="viewport" content="width=device-width, initial-scale=1.0">
	    <title>Document</title>
	</head>
	<body>
	    <script src="plays.json" type="text/javascript"></script>
	    <script src="invoices.json" type="text/javascript"></script>
	<script>
	    //기본 함수
	    function statement(invoice, plays) {
	        let totalAmount = 0;
	        let volumeCredits = 0;
	        let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
	        const format = new Intl.NumberFormat("en-US", {
	            style: "currency",
	            currency: "USD",
	            minimumFractionDigits: 2
	        }).format;
	        for(let perf of invoice.performances){
	            const play = plays[perf.playID];
	            let thisAmount = 0;
	            switch(play.type){
	                case "tragedy" : //비극
	                thisAmount = 40000;
	                if(perf.audience > 30){
	                    thisAmount += 1000 * (perf.audience - 30);
	                }
	                break;
	                case "comedy" : //희극
	                thisAmount = 30000;
	                if(perf.audience > 30){
	                    thisAmount += 10000 + 500 * (perf.audience - 20);
	                }
	                thisAmount += 300 * perf.audience;
	                break;
	                default: throw new Error(`알수 없는 장르: ${play.type}`);
	            }
	            //포인트를 적립한다.
	            volumeCredits += Math.max(perf.audience -30,0);
	            //희극 관객 5명 마다 추가 포인트를 제공한다.
	            if("comedy" === play.type) volumeCredits += Math.floor(perf.audience/5);
	            //청구 내역을 출력한다.
	            result += `${play.name} : ${format(thisAmount/100)} (${perf.audience}석)\n`;
	            totalAmount += thisAmount;
	        }
	        result += `총액: ${format(totalAmount/100)}\n`;
	        result += `적립 포인트: ${volumeCredits}점\n`;
	        return result;
	    }
	    console.log(statement(invoices,plays))
	</script>
	</body>
</html>

청구 내역 (고객명 : BigCo)
Hamlet : $650.00 (55석)
As You Like It : $580.00 (35석)
Othello : $500.00 (40석)
총액: $1,730.00
적립 포인트: 47점

공연료 관련 프로그램, 예제 코드 출력 시 위와 같은 결과 출력

 

1.2 예시 프로그램을 본소감

프로그램이 작동하는 상황에서 그저 코드가 "지저분하다" 이유로 불평하는 것은 프로그램 구조를 너무 미적인 기준으로만 판단하는 걸까?

컴파일러는 코드가 이쁘건 말건 상관없다. 하지만 그 코드를 수정하는 사람은 미적인 상태에 민감하다

설계가 나쁘면 수정이 어렵다. 수정할 부분을 특정하기도 힘들고 그과정에서 실수를 하기 쉽상이다.

 

프로그램이 새로운 기능을 추가하기에 편한 구조가 아니라면, 먼저 기능을 추가하기 쉬운 형태로 리팩터링하고 나서 원하는 기능을 추가한다

HTML 출력하는 기능을 추가해야한다고 가정하자

HTML 태그를 전부 넣어야되니 함수 복잡도가 증가하기에 기존 함수를 복사해서  HTML버전을 만드는 것이 일반적일 것이다. 이러면 중복코드도 발생할 것이고, statement()함수 수정이 생기면 HTML버전도 함께 수정해야 한다.

 

리팩터링이 필요한 이유는 이러한 변경 때문이다.

리팩터링은 지금 코드가 절대 변경될 없고, 동작한다면 굳이 필요 없다.

 

 

1.3 리팩터링의 첫 단계

리팩터링 단계는 항상 같다. 리팩터링할 코드 영역을 꼼꼼하게 검사해줄 테스트 코드부터 마련한다.

이유는 리팩터링 기법들의 버그 발생 여지를 최소화하기 위함이다.

리팩터링 또한 코드를 변경하는 행위로 버그가 충분히 스며들 가능성이 있다,

변경 때마다 테스트 코드를 돌린다

테스트 코드는 작성하는데 시간이 걸리지만, 나중에 디버깅 시간이 줄어 결과적으로 전체 작업 시간이 감소된다.

 

 

1.4 statement()함수 쪼개기

함수를 리팩터링할 때는 먼저 전체 동작을 각각의 부분으로 나눌 있는 지점을 찾는다.

 

switch(play.type){
    case "tragedy" : //비극
    thisAmount = 40000;
    if(perf.audience > 30){
        thisAmount += 1000 * (perf.audience - 30);
    }
    break;
    case "comedy" : //희극
    thisAmount = 30000;
    if(perf.audience > 30){
        thisAmount += 10000 + 500 * (perf.audience - 20);
    }
    thisAmount += 300 * perf.audience;
    break;
    default: throw new Error(`알수 없는 장르: ${play.type}`);
}

switch문을 보면, 공연에 대한 요금을 계산하고 있다.

이러한 사실은 코드를 분석해서 얻은 정보다. 이런 식으로 파악한 정보는 휘발성이 강해 다음에 코드를 다시 본다면 까먹는 경우가 많다.

 

코드를 별도 함수로 추출해 명확한 이름을 붙이자. amountFor()

추가로 함수 내부에서 쓰이는 변수, 파라미터 이름도 명확하게 변경한다.

컴퓨터가 이해하는 코드는 바보도 작성할 수 있다. 사람이 이해하도록 작성하는 프로그래머가 진정한 실력자다.

좋은 코드라면 의도가 명확히 표현돼야 한다. 요즘 IDE 이름변경이 매우 쉬우므로 좋은 이름으로 바꾸길 주저하지 말자

//기본 함수
function statement(invoice, plays) {
    let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
    const format = new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
        minimumFractionDigits: 2
    }).format;

    for(let perf of invoice.performances){
        const play = plays[perf.playID];
        
        let thisAmount = amountFor(perf, play);//함수로 추출했다.

        //포인트를 적립한다.
        volumeCredits += Math.max(perf.audience -30,0);
        //희극 관객 5명 마다 추가 포인트를 제공한다.
        if("comedy" === play.type) volumeCredits += Math.floor(perf.audience/5);

        //청구 내역을 출력한다.
        result += `${play.name} : ${format(thisAmount/100)} (${perf.audience}석)\n`;
        totalAmount += thisAmount;
    }
    result += `총액: ${format(totalAmount/100)}\n`;
    result += `적립 포인트: ${volumeCredits}점\n`;

    return result;

    function amountFor(aPerformance, play) {//명확한 이름으로 변경
        let result = 0;

        switch(play.type){
            case "tragedy" : //비극
            result = 40000;
            if(aPerformance.audience > 30){
                result += 1000 * (aPerformance.audience - 30);
            }
            break;
            case "comedy" : //희극
            result = 30000;
            if(aPerformance.audience > 30){
                result += 10000 + 500 * (aPerformance.audience - 20);
            }
            result += 300 * aPerformance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${play.type}`);
        }
        return result; 
    }
}

 

play 변수 제거하기

aPerformance 루프 변수에서 오기에 순회마다 값이 변경된다.

반면에 play 개별공연(aPerformance)에서 얻기 때문에 매개변수로 전달할 필요가 없다.

amountFor() 안에서 다시 계산하면 된다.

이런 식으로 함수를 쪼갤 때마다 불필요한 매개변수를 최대한 제거한다.

이러한 임시 변수들 때문에 로컬 범위에 존재하는 이름이 많아져 추출 작업이 복잡해진다.

 for(let perf of invoice.performances){
        const play = playFor(perf); // 우변을 함수로 추출
... 

function playFor(aPerformance){
    return plays[aPerformance.playID];
}

정상동작 테스트함 다음 과정 변수 인라인하기

 

for(let perf of invoice.performances){
    // const play = playFor(perf); //변수 인라인하기
    let thisAmount = amountFor(perf, playFor(perf));
    //포인트를 적립한다.
    volumeCredits += Math.max(perf.audience -30,0);
    //희극 관객 5명 마다 추가 포인트를 제공한다.
    if("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience/5);
    //청구 내역을 출력한다.
    result += `${playFor(perf).name} : ${format(thisAmount/100)} (${perf.audience}석)\n`;
    totalAmount += thisAmount;
}

amountFor()함수에서 playFor()함수로 인해 매개변수를 하나 줄일 있게 됐다.

    function amountFor(aPerformance/*, play*/) {//필요없어진 매개변수제거
        let result = 0;
    
        switch(playFor(aPerformance).type){
            case "tragedy" : //비극
            result = 40000;
            if(aPerformance.audience > 30){
                result += 1000 * (aPerformance.audience - 30);
            }
            break;
            case "comedy" : //희극
            result = 30000;
            if(aPerformance.audience > 30){
                result += 10000 + 500 * (aPerformance.audience - 20);
            }
            result += 300 * aPerformance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${playFor(aPerformance).type}`);
        }
        return result; 
    }

지역 변수를 제거해서 함수 추출하기가 훨씬 쉬워졌다.

이유는 유효범위를 신경써야할 대상이 줄었기 때문이다.

추출 작업 전에 거의 항상 지역 변수부터 제거하도록 하자

 

amountFor()함수 파라미터는 정리했으니 statement() 보자

임시 변수인 thisAmount 선언되고 값이 변하지 않는다.

따라서 "변수 인라인하기" 적용한다.

 

	statement()속…
	
	    for(let perf of invoice.performances){
        	//let thisAmount = amountFor(perf); //thisAmount변수 인라인
	        //포인트를 적립한다.
	        volumeCredits += Math.max(perf.audience -30,0);
	        //희극 관객 5명 마다 추가 포인트를 제공한다.
	        if("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience/5);
	        //청구 내역을 출력한다.
	        result += `${playFor(perf).name} : ${format(amountFor(perf)/100)} (${perf.audience}석)\n`;
	        totalAmount += amountFor(perf);//thisAmount변수 인라인
	    }

 

적립 포인트 계산 코드 추출하기

play 변수를 제거한 결과 로컬 유효범위의 변수가 하나 줄어서 적립 포인트 계산 부분을 추출하기가 쉬워졌다.

 

	function statement(invoice, plays) {
	    let totalAmount = 0;
	    let volumeCredits = 0;
	    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
	    const format = new Intl.NumberFormat("en-US", {
	        style: "currency",
	        currency: "USD",
	        minimumFractionDigits: 2
	    }).format;
	    for(let perf of invoice.performances){
	        //포인트를 적립한다.
	        volumeCredits += Math.max(perf.audience -30,0);
	        //희극 관객 5명 마다 추가 포인트를 제공한다.
	        if("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience/5);
	        //청구 내역을 출력한다.
	        result += `${playFor(perf).name} : ${format(amountFor(perf)/100)} (${perf.audience}석)\n`;
	        totalAmount += amountFor(perf);//thisAmount변수 인라인
	    }
	    result += `총액: ${format(totalAmount/100)}\n`;
	    result += `적립 포인트: ${volumeCredits}점\n`;
	    
	    return result;
}

처리해야 변수 perf, volumeCredits

volumeCredits처리
//함수 추출
function volumeCreditsFor(perf){
    let volumeCredits = 0;
    volumeCredits += Math.max(perf.audience -30,0);
    if("comedy" === playFor(perf).type) 
        volumeCredits += Math.floor(perf.audience/5);
    return volumeCredits;
}

 ...............

    for(let perf of invoice.performances){
        volumeCredits += volumeCreditsFor(perf);//추출한 함수를 이용해 값을 누적
        result += `${playFor(perf).name} : ${format(amountFor(perf)/100)} (${perf.audience}석)\n`;
        totalAmount += amountFor(perf);
    }

테스트 동작 확인 이제 적절한 이름으로
...............
function volumeCreditsFor(aPerformance){
    let volumeCredits = 0;
    volumeCredits += Math.max(aPerformance.audience -30,0);
    if("comedy" === playFor(aPerformance).type) 
        volumeCredits += Math.floor(aPerformance.audience/5);
    return volumeCredits;
}

 

format 변수 제거하기

임시 변수는 나중에 문제를 일으킬 있다.

임시 변수는 자신이 속한 루틴에서만 의미가 있어서 루틴이 길고 복잡해지기 쉽다.

 

임시 변수를 제거하는 것이 다음 리팩터링

 

리팩토링 대상코드

const format = new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: "USD",
    minimumFractionDigits: 2
}).format;

format 임시 변수에 함수를 대입한 형태다.(함수 포인터처럼)

직접 함수를 선언해 사용하도록 바꾼다.

function statement(invoice, plays) {
    let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
    for(let perf of invoice.performances){
        volumeCredits += volumeCreditsFor(perf);
        result += `${playFor(perf).name} : ${format(amountFor(perf)/100)} (${perf.audience}석)\n`;
        totalAmount += amountFor(perf);
    }
    result += `총액: ${format(totalAmount/100)}\n`;//임시 변수였던 format을 함수 호출로 대체
    result += `적립 포인트: ${volumeCredits}점\n`;

    return result;
}
function format(aNumber){
    return new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
        minimumFractionDigits: 2
    }).format(aNumber);
}

format만으로 함수가 하는 일을 충분히 설명하지 못한다. 함수에 핵심 기능에 걸맞는 이름을 짓는다.(화폐 단위 맞추기)

    for(let perf of invoice.performances){
        volumeCredits += volumeCreditsFor(perf);
        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
        totalAmount += amountFor(perf);
    }
    result += `총액: ${usd(totalAmount)}\n`;
    result += `적립 포인트: ${volumeCredits}점\n`;

    return result;
}
function usd(aNumber){
    return new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
        minimumFractionDigits: 2
    }).format(aNumber/100); // 단위 변환 로직도 이 함수로 안으로 이동
}

함수를 작게 쪼개는 리팩터링은 이름을 잘지어야만 효과가 있다.

이름이 좋으면 함수 본문을 읽지 않고도 무슨 일을 하는지 있다.

 

이름 바꾸기는 쉬우므로 이름 짓기를 주저하지 말고 짓자. 좋은 이름이 떠올르면 다시 변경하면 그만이다.

 

 

volumeCredits 변수 제거하기

변수는 반복문을 바퀴 때마다 값을 누적하기 때문에 리팩터링하기가 까다롭다.

function statement(invoice, plays) {
    let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
    for(let perf of invoice.performances){
        volumeCredits += volumeCreditsFor(perf);
        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
        totalAmount += amountFor(perf);
    }
    result += `총액: ${usd(totalAmount)}\n`;
    result += `적립 포인트: ${volumeCredits}점\n`;

    return result;
}

먼저 반복문 쪼개기로 volumeCredits 값이 누적되는 부분을 따로 빼낸다.

위를 아래처럼

function statement(invoice, plays) {
    let totalAmount = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;

    for(let perf of invoice.performances){
        //청구 내역을 출력한다.
        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
        totalAmount += amountFor(perf);
    }
    let volumeCredits = 0;// 변수 선언을 반복문 앞으로 옮김
    for(let perf of invoice.performances){ // 값 누적 로직을 별도  for문으로 분리
        volumeCredits += volumeCreditsFor(perf);
    }
    result += `총액: ${usd(totalAmount)}\n`;
    result += `적립 포인트: ${volumeCredits}점\n`;

    return result;
}

이렇게 분리하면 임시 변수를 질의 함수로 바꾸기가 수월해진다.

함수로 추출한 것으로 변수 인라인을 한다.

    for(let perf of invoice.performances){
        //청구 내역을 출력한다.
        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
        totalAmount += amountFor(perf);
    }
    result += `총액: ${usd(totalAmount)}\n`;
    result += `적립 포인트: ${totalVolumeCredits()}점\n`;
return result;

반복문이 중복되는 것을 꺼리는 이들이 많다.

이정도 중복은 성능에 미치는 영향이 미미할 때가 많다.

 

실제로 리펙터링한 결과를 비교해도 실행 시간 차이가 거의 없다.

 

경험 많은 프로그래머조차 코드의 실제 성능을 정확히 예측하지 못한다.

이유는 똑똑한 컴파일러들은 최신 캐싱 기법 등으로 무장하고 있어서 우리의 직관을 초월하는 결과를 내어주기 때문이다.

또한 소프트웨어 성능은 대체로 코드의 몇몇 작은 부분에 의해 결정되므로 외의 부분은 수정한다고 해도 성능 차이를 체감할 없다.

 

때로는 리팩터링이 성능에 상당한 영향을 주기도 한다. 그런 경우라도 개의치 않고 리팩터링한다.

다음어진 코드라야 성능 개선 작업도 훨씬 수월하기 때문이다.

추가로 시간내어 작업을 한다면 결과적으로 깔끔하고, 빠른 코드를 얻게 된다.

 

따라서 리팩터링으로 인한 성능 문제에 대한 저자의 조언은

"특별한 경우가 아니라면 일단 무시하라"

리팩터링 성능이 떨어진다면, 그때 성능 개선하자

 

리팩터링 중간에 테스트가 실패하고 원인을 바로 찾기 못하면 가장 최근 커밋으로 돌아가서 리팩터링의 단계를 작게 나눠서 다시 시도할 있다.

 

코드가 복잡할 수록 단계를 작게 나누면 작업 속도가 빨라지기 때문이다.

 

totalAmount 앞에 과정과 동일하게 제거한다.

반복문 쪼개고, 변수 초기화 문장을 옮기고, 함수를 추출,

	    for(let perf of invoice.performances){
	        //청구 내역을 출력한다.
	        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
	    }
	    let totalAmount = tmpFunction();
	    function tmpFunction(){ //함수 추출/ 이름은 임시로
	        let totalAmount = 0;
	        for(let perf of invoice.performances){
	            totalAmount += amountFor(perf);
	        }
	        return totalAmount;
	    }
	
	…..
	
	
	    function totalAount(){ 
	        let result = 0; // 함수 안 변수이름도 자기 스타일에 맞게 변경
	        for(let perf of invoice.performances){
	            result += amountFor(perf);
	        }
	        return result;
	    }
	    result += `총액: ${usd(totalAount())}\n`; //함수 인라인 후 의미있는 이름으로 변경하기
	    result += `적립 포인트: ${totalVolumeCredits()}점\n`;

 

1.5 중간 점검: 난무하는 중첩 함수

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
<script src="plays.json" type="text/javascript"></script>
<script src="invoices.json" type="text/javascript"></script>
<script>

function statement(invoice, plays) {
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
    for(let perf of invoice.performances){
        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
    }
    result += `총액: ${usd(totalAount())}\n`; 
    result += `적립 포인트: ${totalVolumeCredits()}점\n`;
    return result;

    //중첩 함수 시작
    function totalAount(){ 
        let result = 0;
        for(let perf of invoice.performances){
            result += amountFor(perf);
        }
        return result;
    }

    function totalVolumeCredits(){
        let volumeCredits = 0;
        for(let perf of invoice.performances){ 
            volumeCredits += volumeCreditsFor(perf);
        }
        return volumeCredits;
    }
    function usd(aNumber){
        return new Intl.NumberFormat("en-US", {
            style: "currency",
            currency: "USD",
            minimumFractionDigits: 2
        }).format(aNumber/100); 
    }
    function volumeCreditsFor(aPerformance){
        let volumeCredits = 0;
        volumeCredits += Math.max(aPerformance.audience -30,0);
        if("comedy" === playFor(aPerformance).type) 
            volumeCredits += Math.floor(aPerformance.audience/5);
        return volumeCredits;
    }
    function playFor(aPerformance){
        return plays[aPerformance.playID];
    }
    function amountFor(aPerformance) {
        let result = 0;
        switch(playFor(aPerformance).type){
            case "tragedy" : 
            result = 40000;
            if(aPerformance.audience > 30){
                result += 1000 * (aPerformance.audience - 30);
            }
            break;
            case "comedy" : 
            result = 30000;
            if(aPerformance.audience > 30){
                result += 10000 + 500 * (aPerformance.audience - 20);
            }
            result += 300 * aPerformance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${playFor(aPerformance).type}`);
        }
        return result; 
    }
    
}

console.log(statement(invoices,plays));
</script>

</body>

</html>

최상위 statement()함수는 이제 일곱줄 뿐이며, 출력할 문장 생성 역할만 한다.

 

계산 로직은 여러 보조 함수로 빼냈다.

전체적인 흐름을 이해하기가 훨씬 쉬워졌다.

 

 

 

 

 

 

 
 

<!DOCTYPE html>
<html lang="ko">

<헤드>
<메타 문자셋="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>문서</title>
</헤드>

<몸>
<script src="plays.json" type="text/javascript"></script>
<script src="invoices.json" type="text/javascript"></script>
<스크립트>

기능문(인보이스, 재생) {
let result = ` 청구 내역 (고객명 : ${invoice.customer})\n`;
for(invoice.performances의 let perf){
result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
}
result += `전체: ${usd(totalAount())}\n`;
result += `적립 포인트: ${totalVolumeCredits()}점\n`;
반환 결과;

//중첩 맛있게 시작
함수 totalAount(){
let 결과 = 0;
for(invoice.performances의 let perf){
결과 += amountFor(perf);
}
반환 결과;
}

함수 totalVolumeCredits(){
let volumeCredits = 0;
for(invoice.performances의 let perf){
volumeCredits += volumeCreditsFor(perf);
}
반환 volumeCredits;
}
함수 usd(숫자){
새로운 Intl.NumberFormat("en-US", {를 반환합니다.
스타일: "통화",
통화: "USD",
minimumFractionDigits: 2
}).format(숫자/100);
}
기능 volumeCreditsFor(aPerformance){
let volumeCredits = 0;
volumeCredits += Math.max(aPerformance.audience -30,0);
if("코미디" === playFor(aPerformance).type)
volumeCredits += Math.floor(aPerformance.audience/5);
반환 volumeCredits;
}
함수 playFor(aPerformance){
반환 재생[aPerformance.playID];
}
함수 amountFor(aPerformance) {
let 결과 = 0;
switch(playFor(aPerformance).type){
사례 "비극":
결과 = 40000;
if(aPerformance.audience > 30){
결과 += 1000 * (aPerformance.audience - 30);
}
부서지다;
케이스 "코미디":
결과 = 30000;
if(aPerformance.audience > 30){
결과 += 10000 + 500 * (aPerformance.audience - 20);
}
결과 += 300 * aPerformance.audience;
부서지다;
default: throw new Error(`알수없는 장르: ${playFor(aPerformance).type}`);
}
반환 결과;
}

}

console.log(statement(invoices,plays));
</스크립트>

</body>

</html>

 

'IT책, 강의 > 리팩터링' 카테고리의 다른 글

03 - 코드에서 나는 악취  (0) 2023.07.10
02 - 리팩터링 원칙 - 02  (0) 2023.07.08
02 - 리팩터링 원칙 - 01  (0) 2023.07.06
01 - 리팩터링: 첫 번째 예시 - 02  (0) 2023.07.04
00 - 들어가며  (0) 2023.07.01
 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

 

리팩터링이란?

겉으로 드러나는 코드의 기능은 바꾸지 않으면서 내부 구조를 개선하는 방식으로 소프트웨어 시스템을 수정하는 과정

 

기존 방식은 설계부터 하고 코드를 작성하는 방식

이 방식은 시간이 지남에 따라 요구사항에 따른 변경이 생기면서 구조가 깨진다.

 

리팩터링은 위 방식의 반대로 처음부터 완벽한 설계를 갖추기보다 개발을 진행하면서 지속적으로 설계한다.

 

대상

프로그래머를 대상으로 리팩터링 지침서, 따라서 프로덕션 코드를 접할 기회가 없으면, 예제만으로 자신을 것으로 만들기 힘들

 

다루는 내용

버그가 생기지 않으면서 효율적인 구조로 리팩터링하는 방식을 학습

 

사용 언어

ES6 기준, 자바스크립트를 사용한다.(큰 의미는 없음 접근성 때문)

자바스크립트에 깊은 문법은 사용하지 않음

클래스를 활용하는 모든 객체지향 언어에서 응용해서 사용할 수 있다.

 

책을 효율적으로 읽기

리팩터링이 뭔지 모를 , 1 읽기

리팩터링해야 이유를 알고 싶다,  1~2 읽기

리팩터링 찾고 싶다, 3 읽기

리팩터링을 실습하고 싶다, 1~4장까지 꼼꼼히 읽고, 나머지를 빠르게 훑어보기

 

 

 

 

 

 

Spring에서 SuperTypeToken을 사용하고 싶다면, 이미 존재하는 org.springframework.core.ParameterizedTypeReference 사용하면 된다.

 

테스트

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
public class SpringbootAcApplication {
	
	public static void main(String[] args) {
		SpringApplication.run(SpringbootAcApplication.class, args);
	}
	@RestController
	public static class MyController{
		@RequestMapping("/")
		public List<User> users(){
			return DB.users();
		}
	}

	static class User{
		long id;
		String name;
		int age;
		String gender;
		int money;
		String email; 
		
		public User() {	}
		
		public long getId() {
			return id;
		}
		public String getName() {
			return name;
		}
		public int getAge() {
			return age;
		}
		public String getGender() {
			return gender;
		}
		public int getMoney() {
			return money;
		}
		public String getEmail() {
			return email;
		}
		
		public void setId(long id) {
			this.id = id;
		}
		public void setName(String name) {
			this.name = name;
		}
		public void setAge(int age) {
			this.age = age;
		}
		public void setGender(String gender) {
			this.gender = gender;
		}
		public void setMoney(int money) {
			this.money = money;
		}
		public void setEmail(String email) {
			this.email = email;
		}
		public User(long id,String name, int age, String gender, int money, String email) {
			this.id = id;
			this.name = name;
			this.age = age;
			this.gender = gender;
			this.money = money;
			this.email = email;
		}
		public String toString() {
			return "[id=" + id + ", name=" + name + ", age=" + age + ", gender=" + gender + ", money=" + money
					+ ", email=" + email + "]\n";
		}
	}
	
	static class DB{
		private static long num = 0L;
		static String[] nameDB = {
				"남재인", "한재인", "신이경", "오유리", "장경완", "봉숙자", "황여진", "심재인", "복미래", "신여진", 
				"배미란", "배영신", "이애정", "송여진", "남영애", "안미란", "문재규", "홍숙자", "장경님", "양경님", 
				"양장미", "추자경", "백장미", "권민서", "전재규", "윤재규", "전지해", "설재신", "배경완", "황보혜린",
				 "정무열", "조경구", "남성한", "조경모", "남치원", "유병헌", "최성한", "윤남규", "문성한", "강승헌", 
				 "백성한", "표경구", "조치원", "오재섭", "하경구", "정요한", "송광조", "백재범", "안남규", "배덕수", 
				 "노광조", "복일성", "안재범", "임경구", "유무영", "남궁덕수", "하치원", "하동건", "유무영", "유무영",
				 "성란", "임재", "심진", "설린", "강현", "전진", "이성", "사공리", "탁재", "복상", "하진", "이상", 
				 "심리", "송설", "조진", "문성", "문지", "임린", "예현", "손진", "유리", "전현", "유재", "배은", 
				 "예상", "황성", "임진", "심재", "백현", "한리", "남훈", "심광", "예철", "정건", "하웅", "권훈", 
				 "성훈", "한훈", "하훈", "심광", "성호", "봉건", "류혁", "노훈", "서훈", "권훈", "윤호", "송건", 
				 "윤광", "하웅", "노호", "이철", "문혁", "추철", "정광", "한호", "예철", "오훈", "사공웅", "고훈",
				 "제갈다솜", "하햇살", "양보다", "정자람", "전우리", "노보람", "최한울", "봉빛나", "장비", "전누리", 
				 "전두리", "전마음", "예한별", "김은별", "김민들레", "홍자람", "안꽃", "전나빛", "안아름", "고보름", 
				 "백나라", "전아람", "설빛나", "강나비", "문샛별", "유새벽", "성여름", "남다솜", "양하다", "권하루",
				 "손우람", "허버들", "봉미르", "남궁나라우람", "노힘찬", "황보달", "류빛가람", "윤나라우람", "유미르", 
				 "황보힘찬", "이믿음", "남궁샘", "남한길", "황보나길", "한한길", "전나라우람", "최한길", "권한길", "임믿음", 
				 "고한결", "설믿음", "황샘", "표나길", "안달", "양샘", "임달", "황빛가람", "홍한길", "제갈한결", "배버들",
				 "강광일", "송세은", "문준웅", "백은선", "설서우", "강경희", "권윤미", "봉재경", "표수민", "조동근", 
				 "추진석", "황민숙", "남원석", "심시준", "이선우", "조정우", "유태일", "추경윤", "권규환", "임은주", 
				 "표연웅", "류창기", "풍병수", "서인숙", "남궁명욱", "박시현", "전창현", "남궁주원", "이우태", "사공혜윤"
		};
		static String[] mailDB = {
				"@naver.com", "@nate.com" , "@daum.net" , "@kakao.com" , "@gmail.com" , "@outlook.com",
				"@hotmail.com" , "@yahoo.com"
		};
		static String[] gender = {
			"남자", "여자"	
		};
		
		static ThreadLocalRandom ran = ThreadLocalRandom.current();
		static List<User> users() {
			return IntStream.range(0, 20)
					.mapToObj(n-> getUser())
					.collect(Collectors.toList());
		}
		static User getUser() {
			return new User(num++,getUsername(), getAge(), getGender(), getMoney(), getEmail());
		}
		
		static String getUsername() {
			return nameDB[ran.nextInt(nameDB.length)];
		}
		
		static String getEmail() {
			StringBuilder sb = new StringBuilder();
			for(int i=0,len=ran.nextInt(5, 15);i<len;i++) 
				sb.append(getWord());
			sb.append(mailDB[ran.nextInt(mailDB.length)]);
			return sb.toString();
		}
		
		static char getWord() {
			return (char)ran.nextInt(97, 122+1);
		}
		static String getGender() {
			return gender[ran.nextInt(gender.length)];
		}
		static int getAge() {
			return ran.nextInt(10, 80);
		}
		static int getMoney() {
			return ran.nextInt(10_000, 1_000_000);
		}
	}
}

실행

import java.util.List;

import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import com.example.springbootac.SpringbootAcApplication.User;

public class Test {
	public static void main(String[] args) {
		RestTemplate rest = new RestTemplate();
		
		List<?> forObject = rest.getForObject("http://localhost:8080", List.class);
		
		System.out.println("타입 토큰 방식");
		System.out.println(forObject.get(0).getClass());
//		forObject.forEach(System.out::println);
		ResponseEntity<List<User>> exchange = rest.exchange(
				"http://localhost:8080", 
				HttpMethod.GET, 
				null, 
				new ParameterizedTypeReference<List<User>>() {});
		System.out.println("슈퍼 타입 토큰 방식");
		System.out.println(exchange.getBody().get(0).getClass());
//		System.out.println(exchange.getBody());
	}
}
20:16:45.500 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET http://localhost:8080
20:16:45.518 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[application/json, application/*+json]
20:16:45.545 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
20:16:45.551 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.util.List<?>]
타입 토큰 방식
class java.util.LinkedHashMap
20:16:45.588 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET http://localhost:8080
20:16:45.589 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[application/json, application/*+json]
20:16:45.592 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
20:16:45.593 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.util.List<com.example.springbootac.SpringbootAcApplication$User>]
슈퍼 타입 토큰 방식
class com.example.springbootac.SpringbootAcApplication$User

타입 토큰 방식은 기본적으로 지네릭 정보를 알 수 없기 때문에 JSON에 매칭되는 Map을 만들어 값을 반환한다.

슈퍼 타입 토큰은 슈퍼 타입의 지네릭 정보를 읽어 알맞게 DTO 객체로 바인딩한다.

 

DTO 클래스로 JSON이나, xml 같은 데이터를 받게 되면 validation을 쉽게 수행할 수 있고, IDE 코드 추천을 받을 수 있다는 것이 큰 장점이다.

'개발 > 자바(JAVA)' 카테고리의 다른 글

SuperTypeToken  (0) 2023.06.29
컴포지트 패턴 연습 - 2  (0) 2023.04.16
컴포지트 패턴 연습 - 1  (0) 2023.04.13
기본형 배열 List로 변환하기  (0) 2023.03.10
객체 지향 - 6  (0) 2023.02.03

https://www.youtube.com/watch?v=01sdXvZSjcI&list=PLv-xDnFD-nnmof-yoZQN8Fs2kVljIuFyC&index=16 

예시

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

public class SuperTypeToken02 {
	
	static class Sup<T>{
		T value;
	}
	//super 타입의 지네릭 정보는 런타임 시 지워지지 않고, 바이트 코드에 남아있다.
	static class Sub extends Sup<String>{	}
	
	public static void main(String[] args) throws Exception {
		Sup<String> sup = new Sup<>();
		sup.value = "asd";
		
		//java.lang.Object
		System.out.println(sup.getClass().getDeclaredField("value").getType());
		
		//지네릭 정보를 런타임에 읽을 수 있는 상황예시
		Sub sub = new Sub();
		Type type = sub.getClass().getGenericSuperclass();
		ParameterizedType pType = (ParameterizedType) type;
		//타입이 런타임에 지워지지 않는다.
		//java.lang.String
		System.out.println(pType.getActualTypeArguments()[0]);
		
		Sup<String> sup2 = new Sup<String>() {};
		Type type2 = sup2.getClass().getGenericSuperclass();
		ParameterizedType pType2 = (ParameterizedType) type2;
		//java.lang.String
		//바이트 코드에 정보가 남아 있기 때문에 아무리 중첩되도 정보를 다 가져올 수 있다.
		System.out.println(pType2.getActualTypeArguments()[0]);
		
		Sup sup3 = new Sup<List<Map<Class<String>,Number>>>() {};
		Type type3 = sup3.getClass().getGenericSuperclass();
		ParameterizedType pType3 = (ParameterizedType) type3;
		//java.lang.String
		System.out.println(pType3.getActualTypeArguments()[0]);
		
	}
}
class java.lang.Object
class java.lang.String
class java.lang.String
java.util.List<java.util.Map<java.lang.Class<java.lang.String>, java.lang.Number>>

지네릭 정보는 기본적으로 하위 호환성을 위해 컴파일 시 type eraser에 의해 지네릭 타입은 전부 Object로 바뀌고 그 자리에 형변환이 추가된다.

디컴파일러에 따라 저 형변환이 안보이는 경우가 있어 class 파일로 직접 확인

위 처럼 지네릭 타입 정보는 런타임 시 제거되고 그자리에 적절한 형변환 코드가 남는다.

 

모든 지네릭 정보가 다 지워지는 것은 아니다. 상위 타입 지네릭 정보는 지워지지 않는다.

그래서 예제 코드에서 상위 타입인 지네릭 정보를 읽어올 수 있던 것이다.

익명 클래스로 객체 선언과 동시에 인스턴스화 하면, 대상 클래스가 서브 타입이 된다. 즉, 하위 타입이 된다. 따라서 대상 타입의 지네릭 정보가 안지워진 것이다.

bin 폴더에 보면 위와 같이 익명클래스 클래스 코드가 존재하고, 타입 정보가 남아있다.

SuperTypeToken

익명 클래스 인스턴스를 인자로 넘겨 지네릭 정보를 얻는 방식을 슈퍼타입토큰이라 한다.

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public class SuperTypeToken03 {
	static class TypesafeMap{
		Map<TypeReference<?>, Object> map = new HashMap<>();
		
		<T> void put(TypeReference<T> tr, T v) {
			map.put(tr, v);
		}
		<T> T get(TypeReference<T> tr) {
			//단순 타입이면 문제없지만, 컨테이너 타입이면 캐스팅을 못한다. 
			//<String> ok ,  <List<String>> not ok
			
			if(tr.type instanceof Class<?>)
				return ((Class<T>)tr.type).cast(map.get(tr));
			else 
				return ((Class<T>)((ParameterizedType)tr.type).getRawType()).cast(map.get(tr));
				
		}
	}
	
	static class TypeReference<T>{
		//지네릭 정보까지 포함한 정보를 저장한 필드
		Type type;
		public TypeReference() {
			Type stype = this.getClass().getGenericSuperclass();
			//ParameterizedType인 경우는 <T> 에 정보가 들어와있는 것
			if(stype instanceof ParameterizedType) {
				this.type = ((ParameterizedType) stype).getActualTypeArguments()[0];
			} 
			else throw new RuntimeException();
		}
		
		//IDE에서 제공한 자동 생성 코드
		@Override
		public int hashCode() {
			return Objects.hash(type);
		}
		@Override
		public boolean equals(Object obj) {
			if (this == obj) return true;
			//getClass() != obj.getClass() 여기에 걸려 false를 리턴해 현재 동작하지 않는다.
			//우리는 현재 클래스가 아닌 클래스속 Type 이 목적이다.
			if (obj == null || getClass().getSuperclass() != obj.getClass().getSuperclass())
				return false;
			//이 클래스의 익명 클래스를 사용하기 때문에 슈퍼클래스가 익명클래스가 된다.
			TypeReference<?> other = (TypeReference<?>) obj;
			return Objects.equals(type, other.type);
		}
	}
	
	public static void main(String[] args) {
		TypesafeMap typesafeMap = new TypesafeMap();
		//되는 경우
		TypeReference<String> tr = new TypeReference<String>() {};
		typesafeMap.put(tr, "하나둘");
		System.out.println(typesafeMap.get(tr));
		//안되는 경우, equals()메서드를 오버라이딩 하지 않을 경우, 물리적 주소 값을 비교하게 되어 불가능
		typesafeMap.put(new TypeReference<Integer>() {}, 123);
		System.out.println(typesafeMap.get(new TypeReference<Integer>() {}));
		//컬렉션에서 요소가 같은 값인지 판단하는 기준은 Object.equals()메소드 결과다.
		//따라서 아래와 같은 경우는 다른 key로 인식하여 값을 가져오지 못한다.
		
		typesafeMap.put(new TypeReference<List<String>>() {}, Arrays.asList("하나","둘","셋"));
		System.out.println(typesafeMap.get(new TypeReference<List<String>>() {}));
		typesafeMap.put(new TypeReference<List<List<String>>>() {}, Arrays.asList(Arrays.asList("하나"),Arrays.asList("둘"),Arrays.asList("셋")));
		System.out.println(typesafeMap.get(new TypeReference<List<List<String>>>() {}));
        
        //get() 메서드 추가 설명 
		TypeReference<String> typeReference1 = new TypeReference<String>() {};
		TypeReference<List<String>> typeReference2 = new TypeReference<List<String>>() {};
		System.out.println(typeReference1.type);
		System.out.println(typeReference1.type instanceof ParameterizedType);
		System.out.println(typeReference2.type);
		System.out.println(typeReference2.type instanceof ParameterizedType);
		System.out.println(((ParameterizedType)typeReference2.type).getRawType());
	}
}

 

하나둘
123
[하나, 둘, 셋]
[[하나], [둘], [셋]]
class java.lang.String
false
java.util.List<java.lang.String>
true
interface java.util.List

TypesafeMap 개선

package javabasic.toby;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public class SuperTypeToken03 {
	static class TypesafeMap{
		Map<Type, Object> map = new HashMap<>();
		
		<T> void put(TypeReference<T> tr, T v) {
			map.put(tr.type, v);
		}
		<T> T get(TypeReference<T> tr) {
			//단순 타입이면 문제없지만, 컨테이너 타입이면 캐스팅을 못한다. 
			//<String> ok ,  <List<String>> not ok
			
			if(tr.type instanceof Class<?>)
				return ((Class<T>)tr.type).cast(map.get(tr.type));
			else 
				return ((Class<T>)((ParameterizedType)tr.type).getRawType()).cast(map.get(tr.type));
				
		}
	}
	
	static class TypeReference<T>{
		//지네릭 정보까지 포함한 정보를 저장한 필드
		Type type;
		public TypeReference() {
			Type stype = this.getClass().getGenericSuperclass();
			//ParameterizedType인 경우는 <T> 에 정보가 들어와있는 것
			if(stype instanceof ParameterizedType) {
				this.type = ((ParameterizedType) stype).getActualTypeArguments()[0];
			} 
			else throw new RuntimeException();
		}
	}
	
	public static void main(String[] args) {
		TypesafeMap typesafeMap = new TypesafeMap();
		
		typesafeMap.put(new TypeReference<String>() {}, "하나둘");
		System.out.println(typesafeMap.get(new TypeReference<String>() {}));
		typesafeMap.put(new TypeReference<Integer>() {}, 123);
		System.out.println(typesafeMap.get(new TypeReference<Integer>() {}));
		typesafeMap.put(new TypeReference<List<String>>() {}, Arrays.asList("하나","둘","셋"));
		System.out.println(typesafeMap.get(new TypeReference<List<String>>() {}));
		typesafeMap.put(new TypeReference<List<List<String>>>() {}, Arrays.asList(Arrays.asList("하나"),Arrays.asList("둘"),Arrays.asList("셋")));
		System.out.println(typesafeMap.get(new TypeReference<List<List<String>>>() {}));
		
	}
}

key로 Type을 사용하면, 따로 논리적 동치성을 보장하기 위한 코드가 필요없다. 

'개발 > 자바(JAVA)' 카테고리의 다른 글

ParameterizedTypeReference(SuperTypeToken)  (0) 2023.06.30
컴포지트 패턴 연습 - 2  (0) 2023.04.16
컴포지트 패턴 연습 - 1  (0) 2023.04.13
기본형 배열 List로 변환하기  (0) 2023.03.10
객체 지향 - 6  (0) 2023.02.03

테스트 데이터로 실습

package javabasic.stream;

import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.summarizingLong;
import static java.util.stream.Collectors.toList;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.LongSummaryStatistics;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;


public class FuncEx01 {
	private static final int USER_SIZE = 100;
	
	static <T> void println(T data) {
		System.out.println(data);
	}
	
	
	public static void main(String[] args) {
		List<User> users = DB.users();
		
		
		/*
		 * 실습, 만든 Stream과 실제 java Stream 비교
		 * 단, 자바 Stream.toList()의 경우 JDK 16부터 추가된 것이라 collect()메서드를 사용했다.
		 */
		//나이가 30대, 남자, 돈이 250_000원 이상 있는 사람
		List<User> list = Stream.stream(users)
			.filter(u->u.age-u.age%10 == 30)
			.filter(u->"남자".equals(u.gender))
			.filter(u->u.money>=250_000)
			.toList();
		List<User> list2 = users.stream()
			.filter(u->u.age-u.age%10 == 30)
			.filter(u->"남자".equals(u.gender))
			.filter(u->u.money>=250_000)
			.collect(toList());  
		println("비교 결과 = " + list.containsAll(list2)+"\n"+list);
		println("\n");
		
		
		//나이가 30대, 남자, 돈이 250_000원 이상 있는 사람의 돈의 합
		Optional<Integer> reduce = Stream.stream(users)
			.filter(u->u.age-u.age%10 == 30)
			.filter(u->"남자".equals(u.gender))
			.map(u->u.money)
			.filter(m->m>=250_000)
			.reduce(Integer::sum);
		Optional<Integer> reduce2 = users.stream()
			.filter(u->u.age-u.age%10 == 30)
			.filter(u->"남자".equals(u.gender))
			.map(u->u.money)
			.filter(m->m>=250_000)
			.reduce(Integer::sum);
		println(reduce+"  "+reduce2);
		println("비교 결과 = " + reduce.equals(reduce2));
		println("\n");
		
		
		//20대 여자 중에 kakao 이메일을 쓰는 사람
		int[] cnt1 = new int[] {0};
		
		List<User> list3 = Stream.stream(users)
			.filter(u-> {cnt1[0]++; return u.email.contains("@kakao");} )//분포도가 좋다.
			.filter(u-> {cnt1[0]++; return u.age/10 == 2;})
			.filter(u-> {cnt1[0]++; return Objects.equals("여자", u.gender);})
			.toList();
		//스트림 순서도 중요하다. 분포도를 따졌을 때 첫번째 필터 조건에서 최대한 걸러내야 
		//다음 로직에 넘겨줄 요소가 적기 때문이다.
		
		int[] cnt2 = new int[] {0};
		List<User> collect = users.stream()
			.filter(u-> {cnt2[0]++; return Objects.equals("여자", u.gender);})//분포도가 안좋다.
			.filter(u-> {cnt2[0]++; return u.email.contains("@kakao");} )
			.filter(u-> {cnt2[0]++; return u.age/10 == 2;})
			.collect(toList());
		println("비교 결과 = " + list3.containsAll(collect)+"\n"+list3);
		println("cnt 비교 결과 = cnt1("+cnt1[0]+") cnt2("+cnt2[0]+")");
		println("\n");
		//저 요인 만이 전부는 아니다. 자바 Stream은 지연로딩 기법으로 최종연산 전까진 로직을 수행하지 않는다.
		//최종 연산이 호출된 순간 리스트 속 요소 하나하나가 스트림 중간연산을 통과한다.
		//모든 요소가 중간연산을 통과하고 그 결과를 다음 중간연산으로 보내는 방식이 아니다.
		cnt1[0] = 0;
		cnt2[0] = 0;
		
		long count1 = Stream.stream(users)
				.map(u->{cnt1[0]++; return u.age;})
				.filter(age->{cnt1[0]++; return age>40;})
				.count();
		long count2 = list.stream()
			.map(u->{cnt2[0]++; return u.age;})
			.filter(age->{cnt2[0]++; return age>40;})
			.count();
		println("비교 결과 = " + (count1==count2));
		println("내가 만든 stream은 최적화가 전혀 없기 때문에 users 크기 * 2 (map,filter) 만큼 반복될 것이다.");
		println(cnt1[0]+"   "+cnt2[0]);
		println("\n");
		
		
		//남자, 여자 수
		Map<String, Long> countBy = Stream.stream(users)
			.countBy(u->u.gender);
		
		Map<String, Long> collect2 = users.stream()
			.collect(groupingBy(u->u.gender, counting()));
		println("비교 결과 = " + countBy.entrySet().equals(collect2.entrySet()) +"\n"+collect2);
		println("\n");
		
		
		//40대 남자, 30대 여자 유저
		Map<String, List<User>> groupBy = Stream.stream(users)
			.filter(u-> (u.age/10==3 && Objects.equals("여자", u.gender))
					|| u.age/10==4 && Objects.equals("남자", u.gender))
			.groupBy(u-> Objects.equals("남자", u.gender) 
					? "40대 남자" :"30대 여자");
		Map<String, List<User>> collect3 = users.stream()
			.filter(u-> (u.age/10==3 && Objects.equals("여자", u.gender))
					|| u.age/10==4 && Objects.equals("남자", u.gender))
			.collect(groupingBy(u-> Objects.equals("남자", u.gender) 
					? "40대 남자" :"30대 여자"));
		println("비교 결과 = " + groupBy.entrySet().equals(collect3.entrySet()) +"\n"+collect3);
		println("\n");
		
		
		//새대별 남,여 자산 정보
		//이 정도로 그룹화하는 정보는 현재 구현한 스트림으로 불가
		Map<String, Map<Integer, LongSummaryStatistics>> collect4 = users.stream()
			.collect((groupingBy(u->u.gender
					,groupingBy(u->u.age/10*10, TreeMap::new ,summarizingLong(u->u.money)))));
		collect4.forEach((gender,data)->{
			println(gender);
			data.forEach((age, data2)-> println(age+" "+data2));
		});
		
		
		//메일별, 세대 , 남녀 사용자
		TreeMap<String, TreeMap<Integer, Map<String, List<User>>>> collect5 = users.stream()
			.collect(groupingBy(u->u.email.substring(u.email.indexOf("@")+1, u.email.lastIndexOf(".")), TreeMap::new ,
						groupingBy(u->u.age/10*10,TreeMap::new,
							groupingBy(u->u.gender))));
		collect5.forEach( (email, data1)->{
			println(email);
			data1.forEach((age,data2)->{
				println("  "+age);
				data2.forEach((gender,user)->{
					println("    "+gender);
					user.forEach(u->println("      "+u));
				});
			});
		});
	}
	
	static class StreamUtils{
		private StreamUtils() {}
		/*
		 * filter는 처리 결과가 입력 결과와 타입은 같다.
		 * 길이는 같거나, 작을 수 밖에 없다.
		 */
		static <T> List<T> filter(List<T> list, Predicate<T> predi) {
			//함수형 프로그래밍은 원본 데이터를 수정하지 않는다. 새로운 데이터를 리턴하여
			//부수효과를 극단적으로 배제한다.
			ArrayList<T> newList = new ArrayList<>();
			each(list,data->{
				if(predi.test(data))
					newList.add(data);
			});
			return newList;
		}
		/*
		 * filter 정반대, 주어진 조건에 해당하지 않은 값만 걸러낸다.
		 */
		static <T> List<T> reject(List<T> list, Predicate<T> predi){
			return filter(list, predi.negate());
		}
		/*
		 * map은 처리 결과가 입력 결과와 타입은 같거나 다르다.
		 * 길이는 항상 같다.
		 */
		static <T,R> List<R> map(List<T> list, Function<T, R> mapper){
			ArrayList<R> newList = new ArrayList<>();
			each(list, data->newList.add(mapper.apply(data)));
			return newList;
		}
		/*
		 * 반복문을 중복을 대체할 함수
		 */
		static <T> List<T> each(List<T> list, Consumer<T> iter){
			for(T data : list) 
				iter.accept(data);
			return list;
		}
		
		/*
		 * 자바는 스트림 안에서 외부 변수를 참조하면 그 변수는 final 속성이 된다. 
		 * 따라서 갱신하면서 누산을 하지 못해, 간접적으로 값을 수정해야 한다.
		 */
		static <T> T reduce(List<T> list, BinaryOperator<T> reducer ,T memo) {
			//간소화된 유효성 검사, 본질을 흐리지 않는 선에서 간략화
			if(memo == null ) return reduce(list.subList(1, list.size()), reducer, list.get(0));
			if(list.size() < 2) return list.get(0);
			HashMap<Class<?>, T> map = new HashMap<>();
			map.put(memo.getClass(), memo);
			
			each(list, data->map.compute(memo.getClass(), (k,v)->reducer.apply(v, data)));
			return map.get(memo.getClass());
		}
		/*
		 * 자바는 람다에서 사용하는 변수는 final 속성을 띄기 때문에 memo 값 처리가 애매해진다.
		 * 삼항연산자 등 별도 처리하기보다 오버로딩으로 처리했다.
		 */
		static <T> T reduce(List<T> list, BinaryOperator<T> reducer) {
			if(list.size() < 2) return list.get(0);
			return reduce(list.subList(1, list.size()), reducer, list.get(0));
		}
		/*
		 * reduce 특화 함수
		 */
		static <T> T min(List<T> list, Comparator<T> comparator) {
			return reduce(list, BinaryOperator.minBy(comparator));
		}
		static <T> T max(List<T> list, Comparator<T> comparator) {
			return reduce(list, BinaryOperator.maxBy(comparator));
		}
		//주어진 조건으로 검사한 결과로 최대 최소 값을 판단한다.
		static <T,R> T minBy(List<T> list,Comparator<R> comparator ,Function<T, R> mapper) {
			return reduce(list, (a,b)-> comparator.compare(mapper.apply(a), mapper.apply(b)) > 0 ? b:a);
		}
		static <T,R> T maxBy(List<T> list,Comparator<R> comparator ,Function<T, R> mapper) {
			return minBy(list,comparator.reversed(),mapper);
		}
		
		/*
		 * 그룹 방법 2, 리스트의 요소를 하나로 축약한 다는 점에서 groupBy는 reduce의 특화 메서드여야 한다.
		 * 작성은 까다롭고 가시성이 안좋지만, 어차피 사용자입장에선 방법1과 똑같은 방식으로 호출한다.
		 */
		static <T,R> Map<R,List<T>> groupBy(List<T> list, Function<T,R> mapper){
			BiFunction<Map<R,List<T>> , T, Map<R,List<T>> > bi = (group, val) -> {
				group.compute(mapper.apply(val), (k,v)->{
					if(v == null) v = new ArrayList<>();
					v.add(val);
					return v;
				});
				return group;
			};
			return reduce(list, bi, new HashMap<R, List<T>>());
		}
		//groupBy,countBy 용 reduce 오버로딩
		static <T,R> R reduce(List<T> list, BiFunction<R ,T, R> reducer, R memo) {
			each(list, val-> reducer.apply(memo, val));
			return memo;
		}
		/*
		 * 기본 groupBy로직을 재사용했다. 
		 * 만약 groupBy로직이 변경되면 똑같이 적용받는다.
		 */
		static <T,R> Map<R,Long> countBy(List<T> list, Function<T,R> mapper){
			Map<R, Long> countBy = new HashMap<>();
			for(Entry<R, List<T>> entry:groupBy(list,mapper).entrySet()) 
				countBy.put(entry.getKey(), Long.valueOf(entry.getValue().size()));
			return countBy;
		}
		
		static <T> Optional<T> find(List<T> list, Predicate<T> predi) {
			for(int i=0;i<list.size();i++) {
				T value = list.get(i);
				if(predi.test(value)) return Optional.of(value);
			}
			return Optional.empty();//편의상 null리턴
		}
		static <T> Integer findIndex(List<T> list, Predicate<T> predi) {
			for(int i=0;i<list.size();i++) {
				if(predi.test(list.get(i))) return i;
			}
			return -1;
		}
		
		static <T> Boolean some(List<T> list, Predicate<T> predi) {
			return findIndex(list, predi) != -1;
		}
		static <T> Boolean every(List<T> list, Predicate<T> predi) {
			return findIndex(list, predi.negate()) == -1;
		}
		static <T> Boolean contains(List<T> list, T data){
			return findIndex(list, val-> Objects.equals(val, data) ) != -1 ;
		}
		static <T,R> List<R> pluck(List<T> list, String key,Class<R> typeToken){
			List<R> result = map(list, val-> pluckHelper(val, key, typeToken));
			return some(result, Objects::isNull).booleanValue() ? Collections.emptyList() :result;
		}
		//런타임 시 타입이 확정되는 경우 어쩔 수 없이 Object를 리턴해야 한다.
		static <T,R> R pluckHelper(T val,String key,Class<R> typeToken) {
			try {
				Field field = val.getClass().getDeclaredField(key);
				return typeToken.cast(field.get(val));
			} catch (Exception e) {
				return null;
			}
		}
		
	}
	
	
	
	/*
	 * pipe, go 구현을 시도하려 했지만, 근본적으로 자바는 함수가 개념이 없어
	 * 호출부를 단일로 추상화할 수 없다. apply, test ... 
	 * 따라서 하나의 클래스로 묶었다.
	 */
	static class Stream<T> {
		final List<T> list;
		
		private Stream(List<T> list) {
			this.list = list;
		}
		
		static <T> Stream<T> stream(List<T> list){
			return new Stream<>(list);
		}
		Stream<T> filter(Predicate<T> predi) {
			//함수형 프로그래밍은 원본 데이터를 수정하지 않는다. 새로운 데이터를 리턴하여
			//부수효과를 극단적으로 배제한다.
			ArrayList<T> newList = new ArrayList<>();
			forEach(data->{
				if(predi.test(data))
					newList.add(data);
			});
			return stream(newList);
		}
		Stream<T> reject(Predicate<T> predi){
			return filter(predi.negate());
		}
		<R> Stream<R> map(Function<T, R> mapper){
			ArrayList<R> newList = new ArrayList<>();
			forEach(data->newList.add(mapper.apply(data)));
			return stream(newList);
		}
		
		void forEach(Consumer<T> iter){
			for(T data : list) iter.accept(data);
		}
		
		Optional<T> reduce(BinaryOperator<T> reducer ,T memo) {
			if(memo == null) return reduce(reducer);
			if(this.list.size() < 2) return Optional.of(this.list.get(0));
			//람다에서 참조하는 변수는 final 특성을 부여받는다. 따라서 간접적으로 값을 갱신하기 위해 Map을 활용
			HashMap<String, T> reduceStore = new HashMap<>();
			String accumulation = "누산용임시키";
			reduceStore.put(accumulation, memo);
			forEach(data->reduceStore.compute(accumulation, (k,v)-> reducer.apply(v, data)));
			return Optional.of(reduceStore.get(accumulation));
		}
		Optional<T> reduce(BinaryOperator<T> reducer) {
			T tmpValue = this.list.get(0);
			if(this.list.size() < 2) return Optional.of(tmpValue);
			List<T> subList = this.list.subList(1, list.size());
			return Stream.stream(subList).reduce(reducer, tmpValue);
		}
		//groupBy,countBy 용 reduce 오버로딩
		<R> R reduce(BiFunction<R ,T, R> reducer, R memo) {
			forEach(val-> reducer.apply(memo, val));
			return memo;
		}
		/*
		 * reduce 특화 함수
		 */
		Optional<T> min(Comparator<? super T> comparator) {
			return reduce(BinaryOperator.<T>minBy(comparator));
		}
		Optional<T> max(Comparator<? super T> comparator) {
			return reduce(BinaryOperator.<T>maxBy(comparator));
		}
		//주어진 조건으로 검사한 결과로 최대 최소 값을 판단한다.
		<R> Optional<T> minBy(Comparator<? super R> comparator ,Function<T, R> mapper) {
			return reduce( (a,b)-> comparator.compare(mapper.apply(a), mapper.apply(b)) > 0 ? b:a);
		}
		<R> Optional<T> maxBy(Comparator<? super R> comparator ,Function<T, R> mapper) {
			return minBy(comparator.reversed(),mapper);
		}
		
		/*
		 * 주어진 조건에 따라 그룹화
		 */
		<R> Map<R,List<T>> groupBy(Function<T,R> mapper){
			BiFunction<Map<R,List<T>> , T, Map<R,List<T>> > bi = (group, val) -> {
				group.compute(mapper.apply(val), (k,v)->{
					if(v == null) v = new ArrayList<>();
					v.add(val);
					return v;
				});
				return group;
			};
			return reduce(bi, new HashMap<R, List<T>>());
		}

		/*
		 * 기본 groupBy로직을 재사용했다. 
		 * 만약 groupBy로직이 변경되면 똑같이 적용받는다.
		 */
		<R> Map<R,Long> countBy(Function<T,R> mapper){
			Map<R, Long> countBy = new HashMap<>();
			for(Entry<R, List<T>> entry:groupBy(mapper).entrySet()) 
				countBy.put(entry.getKey(), Long.valueOf(entry.getValue().size()));
			return countBy;
		}
		Optional<T> find(Predicate<T> predi) {
			for(int i=0;i<this.list.size();i++) {
				T value = this.list.get(i);
				if(predi.test(value)) return Optional.of(value);
			}
			return Optional.empty();//편의상 null리턴
		}
		Integer findIndex(Predicate<T> predi) {
			for(int i=0;i<this.list.size();i++) 
				if(predi.test(this.list.get(i))) return i;
			return -1;
		}
		Boolean some(Predicate<T> predi) {
			return findIndex(predi) != -1;
		}
		Boolean every(Predicate<T> predi) {
			return findIndex(predi.negate()) == -1;
		}
		<R> List<R> pluck(String key,Class<R> typeToken){
			Stream<R> result = map(val-> pluckHelper(val, key, typeToken));
			return result.some(Objects::isNull).booleanValue() ? 
					Collections.emptyList() :result.toList();
		}
		//런타임 시 타입이 확정되는 경우 어쩔 수 없이 Object를 리턴해야 한다.
		<R> R pluckHelper(T val,String key,Class<R> typeToken) {
			try {
				Field field = val.getClass().getDeclaredField(key);
				return typeToken.cast(field.get(val));
			} catch (Exception e) {
				return null;
			}
		}
		List<T> toList(){
			//방어적 복사
			return new ArrayList<>(this.list);
		}
		long count() {
			return this.list.size();
		}
		
	}
	
	static class User{
		private static long serial;
		final long id;
		final String name;
		final int age;
		final String gender;
		final int money;
		final String email; 
		public User(String name, int age, String gender, int money, String email) {
			id = ++serial;
			this.name = name;
			this.age = age;
			this.gender = gender;
			this.money = money;
			this.email = email;
		}
		public String toString() {
			return "[id=" + id + ", name=" + name + ", age=" + age + ", gender=" + gender + ", money=" + money
					+ ", email=" + email + "]\n";
		}
	}
	
	static class DB{
		static String[] nameDB = {
				"남재인", "한재인", "신이경", "오유리", "장경완", "봉숙자", "황여진", "심재인", "복미래", "신여진", 
				"배미란", "배영신", "이애정", "송여진", "남영애", "안미란", "문재규", "홍숙자", "장경님", "양경님", 
				"양장미", "추자경", "백장미", "권민서", "전재규", "윤재규", "전지해", "설재신", "배경완", "황보혜린",
				 "정무열", "조경구", "남성한", "조경모", "남치원", "유병헌", "최성한", "윤남규", "문성한", "강승헌", 
				 "백성한", "표경구", "조치원", "오재섭", "하경구", "정요한", "송광조", "백재범", "안남규", "배덕수", 
				 "노광조", "복일성", "안재범", "임경구", "유무영", "남궁덕수", "하치원", "하동건", "유무영", "유무영",
				 "성란", "임재", "심진", "설린", "강현", "전진", "이성", "사공리", "탁재", "복상", "하진", "이상", 
				 "심리", "송설", "조진", "문성", "문지", "임린", "예현", "손진", "유리", "전현", "유재", "배은", 
				 "예상", "황성", "임진", "심재", "백현", "한리", "남훈", "심광", "예철", "정건", "하웅", "권훈", 
				 "성훈", "한훈", "하훈", "심광", "성호", "봉건", "류혁", "노훈", "서훈", "권훈", "윤호", "송건", 
				 "윤광", "하웅", "노호", "이철", "문혁", "추철", "정광", "한호", "예철", "오훈", "사공웅", "고훈",
				 "제갈다솜", "하햇살", "양보다", "정자람", "전우리", "노보람", "최한울", "봉빛나", "장비", "전누리", 
				 "전두리", "전마음", "예한별", "김은별", "김민들레", "홍자람", "안꽃", "전나빛", "안아름", "고보름", 
				 "백나라", "전아람", "설빛나", "강나비", "문샛별", "유새벽", "성여름", "남다솜", "양하다", "권하루",
				 "손우람", "허버들", "봉미르", "남궁나라우람", "노힘찬", "황보달", "류빛가람", "윤나라우람", "유미르", 
				 "황보힘찬", "이믿음", "남궁샘", "남한길", "황보나길", "한한길", "전나라우람", "최한길", "권한길", "임믿음", 
				 "고한결", "설믿음", "황샘", "표나길", "안달", "양샘", "임달", "황빛가람", "홍한길", "제갈한결", "배버들",
				 "강광일", "송세은", "문준웅", "백은선", "설서우", "강경희", "권윤미", "봉재경", "표수민", "조동근", 
				 "추진석", "황민숙", "남원석", "심시준", "이선우", "조정우", "유태일", "추경윤", "권규환", "임은주", 
				 "표연웅", "류창기", "풍병수", "서인숙", "남궁명욱", "박시현", "전창현", "남궁주원", "이우태", "사공혜윤"
		};
		static String[] mailDB = {
				"@naver.com", "@nate.com" , "@daum.net" , "@kakao.com" , "@gmail.com" , "@outlook.com",
				"@hotmail.com" , "@yahoo.com"
		};
		static String[] gender = {
			"남자", "여자"	
		};
		
		static ThreadLocalRandom ran = ThreadLocalRandom.current();
		static List<User> users() {
			return IntStream.range(0, USER_SIZE)
					.mapToObj(n-> getUser())
					.collect(Collectors.toList());
		}
		static User getUser() {
			return new User(getUsername(), getAge(), getGender(), getMoney(), getEmail());
		}
		
		static String getUsername() {
			return nameDB[ran.nextInt(nameDB.length)];
		}
		
		static String getEmail() {
			StringBuilder sb = new StringBuilder();
			for(int i=0,len=ran.nextInt(5, 15);i<len;i++) 
				sb.append(getWord());
			sb.append(mailDB[ran.nextInt(mailDB.length)]);
			return sb.toString();
		}
		
		static char getWord() {
			return (char)ran.nextInt(97, 122+1);
		}
		static String getGender() {
			return gender[ran.nextInt(gender.length)];
		}
		static int getAge() {
			return ran.nextInt(10, 80);
		}
		static int getMoney() {
			return ran.nextInt(10_000, 1_000_000);
		}
	}
}
비교 결과 = true
[[id=15, name=표경구, age=33, gender=남자, money=314489, email=gtmfnbdzobddnp@gmail.com]
, [id=41, name=송건, age=35, gender=남자, money=290228, email=udrsmmbey@outlook.com]
, [id=45, name=임진, age=39, gender=남자, money=444444, email=sdxmigxegegcf@daum.net]
]


Optional[1049161]  Optional[1049161]
비교 결과 = true


비교 결과 = true
[[id=96, name=예현, age=26, gender=여자, money=21656, email=tjczaz@kakao.com]
]
cnt 비교 결과 = cnt1(109) cnt2(158)


비교 결과 = false
내가 만든 stream은 최적화가 전혀 없기 때문에 users 크기 * 2 (map,filter) 만큼 반복될 것이다.
200   6


비교 결과 = true
{여자=53, 남자=47}


비교 결과 = true
{40대 남자=[[id=23, name=이선우, age=44, gender=남자, money=468012, email=fggpaencwz@outlook.com]
], 30대 여자=[[id=3, name=남치원, age=33, gender=여자, money=333145, email=vndezofeom@daum.net]
, [id=29, name=손우람, age=32, gender=여자, money=728586, email=oyidwlrfsg@daum.net]
, [id=33, name=남궁나라우람, age=36, gender=여자, money=650486, email=mnpsm@kakao.com]
, [id=75, name=강현, age=38, gender=여자, money=403083, email=kawgqtx@daum.net]
]}


여자
10 LongSummaryStatistics{count=12, sum=5059276, min=60107, average=421606.333333, max=988529}
20 LongSummaryStatistics{count=6, sum=2765477, min=21656, average=460912.833333, max=934750}
30 LongSummaryStatistics{count=4, sum=2115300, min=333145, average=528825.000000, max=728586}
40 LongSummaryStatistics{count=8, sum=3903000, min=152607, average=487875.000000, max=865687}
50 LongSummaryStatistics{count=3, sum=2001864, min=133476, average=667288.000000, max=962910}
60 LongSummaryStatistics{count=12, sum=5947893, min=33604, average=495657.750000, max=994724}
70 LongSummaryStatistics{count=8, sum=3752254, min=104377, average=469031.750000, max=835425}
남자
10 LongSummaryStatistics{count=9, sum=4812207, min=77748, average=534689.666667, max=949794}
20 LongSummaryStatistics{count=9, sum=4359975, min=10394, average=484441.666667, max=877966}
30 LongSummaryStatistics{count=5, sum=1368168, min=154407, average=273633.600000, max=444444}
40 LongSummaryStatistics{count=1, sum=468012, min=468012, average=468012.000000, max=468012}
50 LongSummaryStatistics{count=8, sum=2717464, min=22002, average=339683.000000, max=827747}
60 LongSummaryStatistics{count=7, sum=3496732, min=91445, average=499533.142857, max=999230}
70 LongSummaryStatistics{count=8, sum=3086908, min=220017, average=385863.500000, max=638264}
daum
  10
    여자
      [id=26, name=전지해, age=14, gender=여자, money=903863, email=cafnlba@daum.net]

      [id=42, name=설빛나, age=16, gender=여자, money=120450, email=xpeuzh@daum.net]

      [id=51, name=안재범, age=19, gender=여자, money=60107, email=bpmpwyjbylht@daum.net]

    남자
      [id=1, name=임린, age=11, gender=남자, money=704492, email=cwhbycqugqgczp@daum.net]

      [id=10, name=풍병수, age=17, gender=남자, money=310580, email=wjlkup@daum.net]

      [id=85, name=남한길, age=12, gender=남자, money=77748, email=rghcntv@daum.net]

  20
    여자
      [id=20, name=전진, age=28, gender=여자, money=351280, email=guslzndju@daum.net]

  30
    여자
      [id=3, name=남치원, age=33, gender=여자, money=333145, email=vndezofeom@daum.net]

      [id=29, name=손우람, age=32, gender=여자, money=728586, email=oyidwlrfsg@daum.net]

      [id=75, name=강현, age=38, gender=여자, money=403083, email=kawgqtx@daum.net]

    남자
      [id=45, name=임진, age=39, gender=남자, money=444444, email=sdxmigxegegcf@daum.net]

  50
    남자
      [id=9, name=임은주, age=52, gender=남자, money=562328, email=gmnpagm@daum.net]

  60
    여자
      [id=86, name=문성, age=62, gender=여자, money=434243, email=nbnhnr@daum.net]

    남자
      [id=35, name=예상, age=60, gender=남자, money=478077, email=xlgttj@daum.net]

  70
    여자
      [id=80, name=권민서, age=73, gender=여자, money=104377, email=bxdkw@daum.net]

    남자
      [id=11, name=문샛별, age=79, gender=남자, money=462553, email=lstuztooumdut@daum.net]

gmail
  10
    여자
      [id=12, name=장경님, age=12, gender=여자, money=988529, email=tyzizhpapm@gmail.com]

      [id=28, name=추자경, age=18, gender=여자, money=549524, email=lmjwbxek@gmail.com]

      [id=62, name=권민서, age=17, gender=여자, money=141048, email=zrmkngqsg@gmail.com]

      [id=73, name=문준웅, age=18, gender=여자, money=241932, email=ctnqmeyhnirka@gmail.com]

    남자
      [id=61, name=한호, age=11, gender=남자, money=939524, email=iuhaczelpkrr@gmail.com]

      [id=83, name=예한별, age=14, gender=남자, money=949794, email=upidffv@gmail.com]

  20
    여자
      [id=99, name=안재범, age=25, gender=여자, money=94538, email=qssefp@gmail.com]

    남자
      [id=17, name=전지해, age=20, gender=남자, money=598154, email=uoxuvjvffc@gmail.com]

  30
    남자
      [id=15, name=표경구, age=33, gender=남자, money=314489, email=gtmfnbdzobddnp@gmail.com]

  50
    여자
      [id=54, name=전누리, age=57, gender=여자, money=133476, email=ztaybdb@gmail.com]

    남자
      [id=8, name=조진, age=53, gender=남자, money=37517, email=dfjxs@gmail.com]

hotmail
  10
    여자
      [id=46, name=오유리, age=13, gender=여자, money=241904, email=xxpbplynagby@hotmail.com]

    남자
      [id=100, name=심시준, age=13, gender=남자, money=573493, email=xbmkixzungwtk@hotmail.com]

  20
    남자
      [id=37, name=이애정, age=29, gender=남자, money=712619, email=fwmqlppeaqonlx@hotmail.com]

      [id=76, name=추경윤, age=24, gender=남자, money=10394, email=kwlczlnfmacbpx@hotmail.com]

  30
    남자
      [id=88, name=정무열, age=37, gender=남자, money=164600, email=ozkspnsnjqbone@hotmail.com]

  40
    여자
      [id=7, name=노광조, age=42, gender=여자, money=633359, email=bsflfhq@hotmail.com]

      [id=14, name=양샘, age=47, gender=여자, money=637883, email=rqwrb@hotmail.com]

      [id=48, name=하웅, age=48, gender=여자, money=181082, email=wafjklvk@hotmail.com]

  50
    여자
      [id=39, name=양하다, age=51, gender=여자, money=962910, email=liwtgqiyxriq@hotmail.com]

    남자
      [id=89, name=윤나라우람, age=54, gender=남자, money=22002, email=xwmnt@hotmail.com]

  60
    남자
      [id=19, name=고보름, age=63, gender=남자, money=669109, email=vpoqukkrnkk@hotmail.com]

      [id=44, name=봉재경, age=64, gender=남자, money=999230, email=gjpxo@hotmail.com]

      [id=57, name=오재섭, age=66, gender=남자, money=599934, email=vgyvflqlxlt@hotmail.com]

  70
    여자
      [id=93, name=남성한, age=72, gender=여자, money=533141, email=czgpofbrg@hotmail.com]

    남자
      [id=58, name=권하루, age=73, gender=남자, money=324715, email=vcwuumugk@hotmail.com]

kakao
  10
    여자
      [id=71, name=서인숙, age=14, gender=여자, money=272687, email=xomdc@kakao.com]

      [id=98, name=김민들레, age=18, gender=여자, money=471026, email=nupqlxvcah@kakao.com]

    남자
      [id=24, name=사공리, age=16, gender=남자, money=570165, email=ztsep@kakao.com]

      [id=91, name=유무영, age=16, gender=남자, money=471883, email=eaumdfxbca@kakao.com]

  20
    여자
      [id=96, name=예현, age=26, gender=여자, money=21656, email=tjczaz@kakao.com]

  30
    여자
      [id=33, name=남궁나라우람, age=36, gender=여자, money=650486, email=mnpsm@kakao.com]

  50
    여자
      [id=55, name=전아람, age=52, gender=여자, money=905478, email=gsreid@kakao.com]

    남자
      [id=60, name=오재섭, age=57, gender=남자, money=827747, email=mefrk@kakao.com]

nate
  20
    여자
      [id=34, name=노광조, age=21, gender=여자, money=788194, email=vmqdlkrtd@nate.com]

  30
    남자
      [id=72, name=강광일, age=36, gender=남자, money=154407, email=pkmufvpouvnyre@nate.com]

  40
    여자
      [id=25, name=예철, age=49, gender=여자, money=405311, email=gbmzvm@nate.com]

  50
    남자
      [id=21, name=류창기, age=58, gender=남자, money=33594, email=eeuct@nate.com]

  60
    여자
      [id=22, name=배덕수, age=67, gender=여자, money=65668, email=latpigtfm@nate.com]

      [id=64, name=남치원, age=64, gender=여자, money=153221, email=fmrcmnyoarw@nate.com]

      [id=84, name=송광조, age=66, gender=여자, money=838967, email=rqfrl@nate.com]

  70
    남자
      [id=43, name=유무영, age=78, gender=남자, money=220017, email=wctljzwkh@nate.com]

naver
  10
    남자
      [id=97, name=문재규, age=19, gender=남자, money=214528, email=vvyjgpk@naver.com]

  20
    여자
      [id=67, name=임재, age=21, gender=여자, money=934750, email=ynnmctxyyhcye@naver.com]

    남자
      [id=32, name=남영애, age=28, gender=남자, money=43114, email=zpqfmykq@naver.com]

      [id=56, name=안미란, age=25, gender=남자, money=514538, email=nqtlblqmhiic@naver.com]

  40
    여자
      [id=2, name=문준웅, age=43, gender=여자, money=865687, email=ztdcawg@naver.com]

      [id=81, name=백장미, age=42, gender=여자, money=152607, email=effcvgwmbm@naver.com]

  50
    남자
      [id=31, name=남성한, age=53, gender=남자, money=591379, email=zzzuxykkl@naver.com]

  60
    여자
      [id=6, name=조경구, age=68, gender=여자, money=871043, email=xlcrpcyvmimdm@naver.com]

      [id=38, name=홍숙자, age=67, gender=여자, money=499318, email=ztvnsysg@naver.com]

      [id=40, name=전우리, age=66, gender=여자, money=33604, email=agntgnhynsto@naver.com]

      [id=66, name=백장미, age=65, gender=여자, money=803355, email=hmiorgusuvr@naver.com]

      [id=77, name=이상, age=64, gender=여자, money=579251, email=iujjz@naver.com]

    남자
      [id=68, name=이믿음, age=64, gender=남자, money=216739, email=lwkvuffwd@naver.com]

      [id=95, name=권한길, age=63, gender=남자, money=91445, email=cmwjs@naver.com]

  70
    여자
      [id=4, name=표수민, age=77, gender=여자, money=649938, email=zvcpwjw@naver.com]

    남자
      [id=30, name=유태일, age=70, gender=남자, money=518323, email=ctwythbsezz@naver.com]

      [id=47, name=강현, age=78, gender=남자, money=347609, email=qkjjteznfp@naver.com]

outlook
  20
    남자
      [id=13, name=남치원, age=21, gender=남자, money=877966, email=vtfwmg@outlook.com]

      [id=70, name=백장미, age=25, gender=남자, money=320175, email=ifziofwkklcm@outlook.com]

      [id=92, name=문성한, age=20, gender=남자, money=837607, email=egtuakkqsopl@outlook.com]

  30
    남자
      [id=41, name=송건, age=35, gender=남자, money=290228, email=udrsmmbey@outlook.com]

  40
    여자
      [id=50, name=양보다, age=46, gender=여자, money=780840, email=lmicguahzefqq@outlook.com]

    남자
      [id=23, name=이선우, age=44, gender=남자, money=468012, email=fggpaencwz@outlook.com]

  60
    남자
      [id=16, name=복미래, age=65, gender=남자, money=442198, email=fdbgaxjheqzxed@outlook.com]

  70
    여자
      [id=82, name=강승헌, age=75, gender=여자, money=141987, email=fnyblldow@outlook.com]

    남자
      [id=65, name=안남규, age=73, gender=남자, money=339445, email=psnsnvg@outlook.com]

yahoo
  10
    여자
      [id=74, name=강현, age=15, gender=여자, money=926397, email=eqtpd@yahoo.com]

      [id=78, name=남다솜, age=15, gender=여자, money=141809, email=olqanipipcy@yahoo.com]

  20
    여자
      [id=53, name=심진, age=25, gender=여자, money=575059, email=lencs@yahoo.com]

    남자
      [id=79, name=설믿음, age=21, gender=남자, money=445408, email=fnbdifxyqw@yahoo.com]

  40
    여자
      [id=27, name=조정우, age=44, gender=여자, money=246231, email=puadexzcwxx@yahoo.com]

  50
    남자
      [id=69, name=홍자람, age=57, gender=남자, money=475667, email=ekfqygfgoqabk@yahoo.com]

      [id=90, name=배미란, age=57, gender=남자, money=167230, email=ulxbuuowodj@yahoo.com]

  60
    여자
      [id=18, name=설재신, age=62, gender=여자, money=102531, email=xvjamjdl@yahoo.com]

      [id=36, name=황성, age=61, gender=여자, money=994724, email=fgojm@yahoo.com]

      [id=87, name=이우태, age=64, gender=여자, money=571968, email=eqxhgmszafh@yahoo.com]

  70
    여자
      [id=5, name=백은선, age=76, gender=여자, money=835425, email=zefprlxtvt@yahoo.com]

      [id=49, name=권한길, age=77, gender=여자, money=607624, email=eyjzuv@yahoo.com]

      [id=59, name=남궁주원, age=70, gender=여자, money=717484, email=jlktqhy@yahoo.com]

      [id=94, name=배영신, age=70, gender=여자, money=162278, email=shlkvyegelwfv@yahoo.com]

    남자
      [id=52, name=유미르, age=74, gender=남자, money=235982, email=fhfbeozpkozxy@yahoo.com]

      [id=63, name=남영애, age=74, gender=남자, money=638264, email=rkntpvsavuzl@yahoo.com]

groupBy 위주로 실습

매 실행마다 테스트 데이터가 달라지므로 위 결과와 다를 것

groupBy

static <T,R> Map<R,List<T>> groupBy(List<T> list, Function<T,R> mapper){
    //그룹 방법 1, 직접 구현
    Map<R, List<T>> groupMap = new HashMap<>();
    each(list, val -> 
        groupMap.compute(mapper.apply(val), 
            (k,v)->{
                if(v==null) v = new ArrayList<>();
                v.add(val);
                return v;
            })
    );
    return groupMap;
}

groupBy는 기본적으로 reduce 특화버전이 되야한다. 모든 요소를 축약해 하나의 요소로 반환하기 때문이다.

/*
 * 그룹 방법 2, 리스트의 요소를 하나로 축약한 다는 점에서 groupBy는 reduce의 특화 메서드여야 한다.
 * 작성은 까다롭고 가시성이 안좋지만, 어차피 사용자입장에선 방법1과 똑같은 방식으로 호출한다.
 */
static <T,R> Map<R,List<T>> groupBy(List<T> list, Function<T,R> mapper){
    BiFunction<Map<R,List<T>> , T, Map<R,List<T>> > bi = (group, val) -> {
        group.compute(mapper.apply(val), (k,v)->{
            if(v == null) v = new ArrayList<>();
            v.add(val);
            return v;
        });
        return group;
    };
    return reduce(list, bi, new HashMap<R, List<T>>());
}
//groupBy 용 reduce 오버로딩
static <T,R> R reduce(List<T> list, BiFunction<R ,T, R> reducer, R memo) {
    each(list, val-> reducer.apply(memo, val));
    return memo;
}

groupBy에서 사용할 reduce를 오버로딩한다.

 

countBy

/*
 * 기본 groupBy로직을 재사용했다. 
 */
static <T,R> Map<R,Long> countBy(List<T> list, Function<T,R> mapper){
    Map<R, Long> countBy = new HashMap<>();
    for(Entry<R, List<T>> entry:groupBy(list,mapper).entrySet()) 
        countBy.put(entry.getKey(), Long.valueOf(entry.getValue().size()));
    return countBy;
}

Stream클래스에 추가

/*
 * 그룹 방법 2, 리스트의 요소를 하나로 축약한 다는 점에서 groupBy는 reduce의 특화 메서드여야 한다.
 * 작성은 까다롭고 가시성이 안좋지만, 어차피 사용자입장에선 방법1과 똑같은 방식으로 호출한다.
 */
<R> Map<R,List<T>> groupBy(Function<T,R> mapper){
    BiFunction<Map<R,List<T>> , T, Map<R,List<T>> > bi = (group, val) -> {
        group.compute(mapper.apply(val), (k,v)->{
            if(v == null) v = new ArrayList<>();
            v.add(val);
            return v;
        });
        return group;
    };
    return reduce(bi, new HashMap<R, List<T>>());
}
//groupBy,countBy 용 reduce 오버로딩
<R> R reduce(BiFunction<R ,T, R> reducer, R memo) {
    each(list, val-> reducer.apply(memo, val));
    return memo;
}
/*
 * 기본 groupBy로직을 재사용했다. 
 * 만약 groupBy로직이 변경되면 똑같이 적용받는다.
 */
<R> Map<R,Long> countBy(Function<T,R> mapper){
    Map<R, Long> countBy = new HashMap<>();
    for(Entry<R, List<T>> entry:groupBy(mapper).entrySet()) 
        countBy.put(entry.getKey(), Long.valueOf(entry.getValue().size()));
    return countBy;
}

전체 코드

package javabasic.stream;

import java.lang.reflect.Field;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.*;


public class FuncEx01 {
	static class User{
		final Long id;
		final String name;
		final Integer age;
		
		public User(Long id, String name, Integer age) {
			this.id = id;
			this.name = name;
			this.age = age;
		}

		@Override
		public String toString() {
			return "[id=" + id + ", name=" + name + ", age=" + age + "]\n";
		}
		
	}
	
	static List<User> getUsers(){
		return Arrays.asList( 
				new User(10L, "ID", 36),
				new User(20L, "BJ", 32),
				new User(30L, "JM", 32),
				new User(40L, "PJ", 27),
				new User(50L, "HA", 25),
				new User(60L, "JE", 26),
				new User(70L, "JI", 31),
				new User(80L, "MP", 23)
				);
	}
	
	public static void main(String[] args) {
		List<User> users = getUsers();
		/*
		System.out.println("filter = "+
				filter(users,user->user.age>30)+"\n"
				+users);
		
		System.out.println("map = "+
				map(users, user->user.id));
		each(Arrays.asList(1,2,3,4,5),System.out::print);
		System.out.println();
		
		System.out.println("reduce = "+
				reduce(Arrays.asList(1,2,3,4,5), (a,b)->a+b, Integer.valueOf(0)));
		System.out.println("reduce = "+
				reduce(Arrays.asList(1,2,3,4,5), (a,b)->a+b));
		System.out.println("reduce = "+
				reduce(Arrays.asList(1), (a,b)->a+b, Integer.valueOf(0)));
		System.out.println("reduce = "+
				reduce(Arrays.asList(1), (a,b)->a+b));
		
		List<String> result = Stream.stream(users)
			.filter(user->user.age > 30)
			.map(user->user.name)
			.toList();
		System.out.println(result);
		//메서드 체이닝 방식과 차이 비교
		System.out.println("default = "+
					reduce(
							map(
								filter(users,user->user.age>30) 
								, user->user.age)
							, (a,b)->a+b));
		
		//기존은 안쪽에서부터 밖으로 나온다. 중첩구조가 심해질 수록 가독성이 안좋아진다.
		//스트림을 통한 메서드 체이닝 방식은 순차적으로 적용된다.
		
		System.out.println("stream = " +
				Stream.stream(users)
					.filter(user->user.age > 30)
					.map(user->user.age)
					.reduce(Integer::sum)
//					.reduce((a,b)->a+b)
				);
		*/
		/*
		//find, findIndex 실습
		System.out.println(find(users, user->user.age<32));
		System.out.println(find(users, user->user.age>55));
		System.out.println(findIndex(users, user->user.age<32));
		System.out.println(findIndex(users, user->user.age>55));
		System.out.println(Stream.stream(users).find(user->user.age<32));
		System.out.println(Stream.stream(users).find(user->user.age>55));
		System.out.println(Stream.stream(users).findIndex( user->user.age<32));
		System.out.println(Stream.stream(users).findIndex( user->user.age>55));
		
		System.out.println("every , find 테스트");
		System.out.println(some(users, u->u.age>30));
		System.out.println(some(users, u->u.age>50));
		System.out.println(every(users, u->u.age>30));
		System.out.println(every(users, u->u.age>10));
		System.out.println(Stream.stream(users).some(u->u.age>30));
		System.out.println(Stream.stream(users).some(u->u.age>50));
		System.out.println(Stream.stream(users).every(u->u.age>30));
		System.out.println(Stream.stream(users).every(u->u.age>10));
		
		System.out.println("pluck 테스트");
		System.out.println(pluck(users, "age",Integer.class));
		//타입불일치
		System.out.println(pluck(users, "age",String.class));
		System.out.println(Stream.stream(users).pluck("age",Integer.class));
		//타입불일치
		System.out.println(Stream.stream(users).pluck("age",String.class));
		
		System.out.println("reject 테스트");
		System.out.println(reject(users,u->u.age>30));
		System.out.println(Stream.stream(users).reject(u->u.age>30).toList());
		*/
		
		System.out.println("min, max, minBy, maxBy 테스트");
		System.out.println("나이 "+min(users, (u1,u2)->Integer.compare(u1.age,u2.age)));
		System.out.println("나이 "+max(users, (u1,u2)->Integer.compare(u1.age,u2.age)));
		System.out.println("이름 "+min(users, (u1,u2)-> u1.name.compareTo(u2.name) ));
		System.out.println("이름 "+max(users, (u1,u2)-> u1.name.compareTo(u2.name) ));
		System.out.println("나이 "+minBy(users, Integer::compare, u->u.age));
		System.out.println("나이 "+maxBy(users, Integer::compare, u->u.age));
		System.out.println(
				Stream.stream(users)
					.map(u->u.age)
					.max(Integer::compare));
		System.out.println(
				Stream.stream(users)
				.minBy(Long::compare,u->u.id));
		
		System.out.println(groupBy(users, u->u.age-u.age%10));
		System.out.println(countBy(users, u->u.age-u.age%10));
		System.out.println(Stream.stream(users).groupBy(u->u.age-u.age%10));
		System.out.println(Stream.stream(users).countBy(u->u.age-u.age%10));
		
		
		
	}
	
	
	
	/*
	 * filter는 처리 결과가 입력 결과와 타입은 같다.
	 * 길이는 같거나, 작을 수 밖에 없다.
	 */
	static <T> List<T> filter(List<T> list, Predicate<T> predi) {
		//함수형 프로그래밍은 원본 데이터를 수정하지 않는다. 새로운 데이터를 리턴하여
		//부수효과를 극단적으로 배제한다.
		ArrayList<T> newList = new ArrayList<>();
		each(list,data->{
			if(predi.test(data))
				newList.add(data);
		});
		
		return newList;
	}
	/*
	 * filter 정반대, 주어진 조건에 해당하지 않은 값만 걸러낸다.
	 */
	static <T> List<T> reject(List<T> list, Predicate<T> predi){
		return filter(list, predi.negate());
	}
	/*
	 * map은 처리 결과가 입력 결과와 타입은 같거나 다르다.
	 * 길이는 항상 같다.
	 */
	static <T,R> List<R> map(List<T> list, Function<T, R> mapper){
		ArrayList<R> newList = new ArrayList<>();
		each(list, data->newList.add(mapper.apply(data)));
		return newList;
	}
	/*
	 * 반복문을 중복을 대체할 함수
	 */
	static <T> List<T> each(List<T> list, Consumer<T> iter){
		for(T data : list) 
			iter.accept(data);
		return list;
	}
	
	/*
	 * 자바는 스트림 안에서 외부 변수를 참조하면 그 변수는 final 속성이 된다. 
	 * 따라서 갱신하면서 누산을 하지 못해, 간접적으로 값을 수정해야 한다.
	 */
	static <T> T reduce(List<T> list, BinaryOperator<T> reducer ,T memo) {
		//간소화된 유효성 검사, 본질을 흐리지 않는 선에서 간략화
		if(memo == null ) return reduce(list.subList(1, list.size()), reducer, list.get(0));
		if(list.size() < 2) return list.get(0);
		HashMap<Class<?>, T> map = new HashMap<>();
		map.put(memo.getClass(), memo);
		
		each(list, data->map.compute(memo.getClass(), (k,v)->reducer.apply(v, data)));
		return map.get(memo.getClass());
	}
	/*
	 * 자바는 람다에서 사용하는 변수는 final 속성을 띄기 때문에 memo 값 처리가 애매해진다.
	 * 삼항연산자 등 별도 처리하기보다 오버로딩으로 처리했다.
	 */
	static <T> T reduce(List<T> list, BinaryOperator<T> reducer) {
		if(list.size() < 2) return list.get(0);
		return reduce(list.subList(1, list.size()), reducer, list.get(0));
	}
	/*
	 * null 값을 간접적으로 다루기 위한 래퍼클래스 사용
	 */
	static <T> Optional<T> find(List<T> list, Predicate<T> predi) {
		for(int i=0;i<list.size();i++) {
			T value = list.get(i);
			if(predi.test(value)) return Optional.of(value);
		}
		return Optional.empty();//편의상 null리턴
	}
	static <T> Integer findIndex(List<T> list, Predicate<T> predi) {
		for(int i=0;i<list.size();i++) {
			if(predi.test(list.get(i))) return i;
		}
		return -1;
	}
	
	static <T> Boolean some(List<T> list, Predicate<T> predi) {
		return findIndex(list, predi) != -1;
	}
	static <T> Boolean every(List<T> list, Predicate<T> predi) {
		return findIndex(list, predi.negate()) == -1;
	}
	static <T> Boolean contains(List<T> list, T data){
		return findIndex(list, val-> Objects.equals(val, data) ) != -1 ;
	}
	static <T,R> List<R> pluck(List<T> list, String key,Class<R> typeToken){
		List<R> result = map(list, val-> pluckHelper(val, key, typeToken));
		return some(result, Objects::isNull).booleanValue() ? Collections.emptyList() :result;
	}
	//런타임 시 타입이 확정되는 경우 어쩔 수 없이 Object를 리턴해야 한다.
	static <T,R> R pluckHelper(T val,String key,Class<R> typeToken) {
		try {
			Field field = val.getClass().getDeclaredField(key);
			return typeToken.cast(field.get(val));
		} catch (Exception e) {
			return null;
		}
	}
	/*
	 * reduce 특화 함수
	 */
	static <T> T min(List<T> list, Comparator<T> comparator) {
		return reduce(list, BinaryOperator.minBy(comparator));
	}
	static <T> T max(List<T> list, Comparator<T> comparator) {
		return reduce(list, BinaryOperator.maxBy(comparator));
	}
	//주어진 조건으로 검사한 결과로 최대 최소 값을 판단한다.
	static <T,R> T minBy(List<T> list,Comparator<R> comparator ,Function<T, R> mapper) {
		return reduce(list, (a,b)-> comparator.compare(mapper.apply(a), mapper.apply(b)) > 0 ? b:a);
	}
	static <T,R> T maxBy(List<T> list,Comparator<R> comparator ,Function<T, R> mapper) {
		return minBy(list,comparator.reversed(),mapper);
	}
	

	/*
	 * 그룹 방법 2, 리스트의 요소를 하나로 축약한 다는 점에서 groupBy는 reduce의 특화 메서드여야 한다.
	 * 작성은 까다롭고 가시성이 안좋지만, 어차피 사용자입장에선 방법1과 똑같은 방식으로 호출한다.
	 */
	static <T,R> Map<R,List<T>> groupBy(List<T> list, Function<T,R> mapper){
		BiFunction<Map<R,List<T>> , T, Map<R,List<T>> > bi = (group, val) -> {
			group.compute(mapper.apply(val), (k,v)->{
				if(v == null) v = new ArrayList<>();
				v.add(val);
				return v;
			});
			return group;
		};
		return reduce(list, bi, new HashMap<R, List<T>>());
	}
	//groupBy,countBy 용 reduce 오버로딩
	static <T,R> R reduce(List<T> list, BiFunction<R ,T, R> reducer, R memo) {
		each(list, val-> reducer.apply(memo, val));
		return memo;
	}
	/*
	 * 기본 groupBy로직을 재사용했다. 
	 * 만약 groupBy로직이 변경되면 똑같이 적용받는다.
	 */
	static <T,R> Map<R,Long> countBy(List<T> list, Function<T,R> mapper){
		Map<R, Long> countBy = new HashMap<>();
		for(Entry<R, List<T>> entry:groupBy(list,mapper).entrySet()) 
			countBy.put(entry.getKey(), Long.valueOf(entry.getValue().size()));
		return countBy;
	}
	
	
	
	
	
	/*
	 * pipe, go 구현을 시도하려 했지만, 근본적으로 자바는 함수가 개념이 없어
	 * 호출부를 단일로 추상화할 수 없다. apply, test ... 
	 * 따라서 하나의 클래스로 묶었다.
	 */
	static class Stream<T> {
		List<T> list;
		
		static <T> Stream<T> stream(List<T> list){
			Stream<T> stream = new Stream<>();
			stream.list = list;
			return stream;
		}
		Stream<T> filter(Predicate<T> predi) {
			//함수형 프로그래밍은 원본 데이터를 수정하지 않는다. 새로운 데이터를 리턴하여
			//부수효과를 극단적으로 배제한다.
			ArrayList<T> newList = new ArrayList<>();
			forEach(data->{
				if(predi.test(data))
					newList.add(data);
			});
			return stream(newList);
		}
		Stream<T> reject(Predicate<T> predi){
			return filter(predi.negate());
		}
		<R> Stream<R> map(Function<T, R> mapper){
			ArrayList<R> newList = new ArrayList<>();
			forEach(data->newList.add(mapper.apply(data)));
			return stream(newList);
		}
		
		void forEach(Consumer<T> iter){
			for(T data : list) iter.accept(data);
		}
		
		Optional<T> reduce(BinaryOperator<T> reducer ,T memo) {
			//간소화된 유효성 검사, 본질을 흐리지 않는 선에서 간략화
			if(memo == null) return reduce(reducer);
			if(this.list.size() < 2) return Optional.of(this.list.get(0));
			HashMap<Class<?>, T> map = new HashMap<>();
			map.put(memo.getClass(), memo);
			each(this.list, data->map.compute(memo.getClass(), (k,v)-> reducer.apply(v, data)));
			return Optional.of(map.get(memo.getClass()));
		}
		Optional<T> reduce(BinaryOperator<T> reducer) {
			T tmpValue = this.list.get(0);
			if(this.list.size() < 2) return Optional.of(tmpValue);
			this.list = this.list.subList(1, list.size());
			return reduce(reducer, tmpValue);
		}
		
		Optional<T> find(Predicate<T> predi) {
			for(int i=0;i<this.list.size();i++) {
				T value = this.list.get(i);
				if(predi.test(value)) return Optional.of(value);
			}
			return Optional.empty();//편의상 null리턴
		}
		Integer findIndex(Predicate<T> predi) {
			for(int i=0;i<this.list.size();i++) {
				if(predi.test(this.list.get(i))) return i;
			}
			return -1;//편의상 null리턴
		}
		
		Boolean some(Predicate<T> predi) {
			return findIndex(predi) != -1;
		}
		Boolean every(Predicate<T> predi) {
			return findIndex(predi.negate()) == -1;
		}
		<R> List<R> pluck(String key,Class<R> typeToken){
			Stream<R> result = map(val-> pluckHelper(val, key, typeToken));
			return result.some(Objects::isNull).booleanValue() ? Collections.emptyList() :result.toList();
		}
		//런타임 시 타입이 확정되는 경우 어쩔 수 없이 Object를 리턴해야 한다.
		<R> R pluckHelper(T val,String key,Class<R> typeToken) {
			try {
				Field field = val.getClass().getDeclaredField(key);
				return typeToken.cast(field.get(val));
			} catch (Exception e) {
				return null;
			}
		}
		
		/*
		 * reduce 특화 함수
		 */
		Optional<T> min(Comparator<? super T> comparator) {
			return reduce(BinaryOperator.<T>minBy(comparator));
		}
		Optional<T> max(Comparator<? super T> comparator) {
			return reduce(BinaryOperator.<T>maxBy(comparator));
		}
		//주어진 조건으로 검사한 결과로 최대 최소 값을 판단한다.
		<R> Optional<T> minBy(Comparator<? super R> comparator ,Function<T, R> mapper) {
			return reduce( (a,b)-> comparator.compare(mapper.apply(a), mapper.apply(b)) > 0 ? b:a);
		}
		<R> Optional<T> maxBy(Comparator<? super R> comparator ,Function<T, R> mapper) {
			return minBy(comparator.reversed(),mapper);
		}
		
		/*
		 * 그룹 방법 2, 리스트의 요소를 하나로 축약한 다는 점에서 groupBy는 reduce의 특화 메서드여야 한다.
		 * 작성은 까다롭고 가시성이 안좋지만, 어차피 사용자입장에선 방법1과 똑같은 방식으로 호출한다.
		 */
		<R> Map<R,List<T>> groupBy(Function<T,R> mapper){
			BiFunction<Map<R,List<T>> , T, Map<R,List<T>> > bi = (group, val) -> {
				group.compute(mapper.apply(val), (k,v)->{
					if(v == null) v = new ArrayList<>();
					v.add(val);
					return v;
				});
				return group;
			};
			return reduce(bi, new HashMap<R, List<T>>());
		}
		//groupBy,countBy 용 reduce 오버로딩
		<R> R reduce(BiFunction<R ,T, R> reducer, R memo) {
			each(list, val-> reducer.apply(memo, val));
			return memo;
		}
		/*
		 * 기본 groupBy로직을 재사용했다. 
		 * 만약 groupBy로직이 변경되면 똑같이 적용받는다.
		 */
		<R> Map<R,Long> countBy(Function<T,R> mapper){
			Map<R, Long> countBy = new HashMap<>();
			for(Entry<R, List<T>> entry:groupBy(mapper).entrySet()) 
				countBy.put(entry.getKey(), Long.valueOf(entry.getValue().size()));
			return countBy;
		}
		
		List<T> toList(){
			return this.list;
		}
		
	}
	
	
}

min, max, minBy, maxBy

위 메서드들은 전부 reduce 특화 함수다.

reduce를 이용해 메서드를 정의한다.

minBy, maxBy 의 경우 배열에 요소에 추가로 적용할 메서드를 인자로 받아 원하는 값으로 비교할 수 있게 해 추상화 수준을 높힌 메서다.

 

/*
 * reduce 특화 함수
 */
static <T> T min(List<T> list, Comparator<T> comparator) {
    return reduce(list, BinaryOperator.minBy(comparator));
}
static <T> T max(List<T> list, Comparator<T> comparator) {
    return reduce(list, BinaryOperator.maxBy(comparator));
}
//주어진 조건으로 검사한 결과로 최대 최소 값을 판단한다.
static <T,R> T minBy(List<T> list,Comparator<R> comparator ,Function<T, R> mapper) {
    return reduce(list, (a,b)-> comparator.compare(mapper.apply(a), mapper.apply(b)) > 0 ? b:a);
}
static <T,R> T maxBy(List<T> list,Comparator<R> comparator ,Function<T, R> mapper) {
    return minBy(list,comparator.reversed(),mapper);
}




/*Stream 클래스 메서드*/
/*
 * reduce 특화 함수
 */
Optional<T> min(Comparator<? super T> comparator) {
    return reduce(BinaryOperator.<T>minBy(comparator));
}
Optional<T> max(Comparator<? super T> comparator) {
    return reduce(BinaryOperator.<T>maxBy(comparator));
}
//주어진 조건으로 검사한 결과로 최대 최소 값을 판단한다.
<R> Optional<T> minBy(Comparator<? super R> comparator ,Function<T, R> mapper) {
    return reduce( (a,b)-> comparator.compare(mapper.apply(a), mapper.apply(b)) > 0 ? b:a);
}
<R> Optional<T> maxBy(Comparator<? super R> comparator ,Function<T, R> mapper) {
    return minBy(comparator.reversed(),mapper);
}
		System.out.println("min, max, minBy, maxBy 테스트");
		System.out.println("나이 "+min(users, (u1,u2)->Integer.compare(u1.age,u2.age)));
		System.out.println("나이 "+max(users, (u1,u2)->Integer.compare(u1.age,u2.age)));
		System.out.println("이름 "+min(users, (u1,u2)-> u1.name.compareTo(u2.name) ));
		System.out.println("이름 "+max(users, (u1,u2)-> u1.name.compareTo(u2.name) ));
		System.out.println("나이 "+minBy(users, Integer::compare, u->u.age));
		System.out.println("나이 "+maxBy(users, Integer::compare, u->u.age));
		System.out.println(
				Stream.stream(users)
					.map(u->u.age)
					.max(Integer::compare));
		System.out.println(
				Stream.stream(users)
				.minBy(Long::compare,u->u.id));
min, max, minBy, maxBy 테스트
나이 [id=80, name=MP, age=23]

나이 [id=10, name=ID, age=36]

이름 [id=20, name=BJ, age=32]

이름 [id=40, name=PJ, age=27]

나이 [id=80, name=MP, age=23]

나이 [id=10, name=ID, age=36]

Optional[36]
Optional[[id=10, name=ID, age=36]
]

정적으로 타입을 체크하는 자바는 모든 경우의 수를 고려해서 내부적으로 max, min을 판단할 수가 없다.

따라서 클라이언트에게 비교기준과 판단을 위임하는 식으로 처리한다.

타입이 명확한 스트림은 실제로 판단기준을 위임하지 않는다.
참조형을 다루는 일반 스트림은 호출자에게 위임한다.

이러한 구조는 폭넓게 해석하면 템플릿 메서드 패턴을 적용했다고 볼 수 있다.

 

다른 방법

static class User implements Comparable<User>{
    final Long id;
    final String name;
    final Integer age;

    public User(Long id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "[id=" + id + ", name=" + name + ", age=" + age + "]\n";
    }

    @Override
    public int compareTo(User o) {
        return Long.compare(this.id, o.id);
    }

}

들어오는 모든 타입 모두 Comparable 인터페이스를 구현하도록 하면 된다.

static <T extends Comparable<T>> T min(List<T> list) {
    return reduce(list, (a,b)->a.compareTo(b)<=0 ? a : b );
}
static <T extends Comparable<T>> T max(List<T> list) {
    return reduce(list, (a,b)->a.compareTo(b)>0 ? a : b );
}

단점으로 정렬 조건을 바꿀 수가 없다. compareTo()를 수정하면 된다고 생각할지 모르지만, 그렇다면 다른 모든 User 클래스 사용하는 곳에 영향을 미친다.

추가로 Stream 클래스에 적용 시 Comparable 구현 여부를 알 수 없어 적용이 힘들다.

위와 같이 제한된 지네릭 클래스로 만들면, 스트림을 생성하려면 반드시 Comparator를 구현해야한다는 제약이 생긴다. Comparator가 필요가 없어도 강제하게 된다.

 

종합해보면 깔끔하게 알고리즘 일부를 외부로 위임하는 템플릿 메서드 패턴을 따르는게 좋다.

전체 코드

package javabasic.stream;

import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.*;
import java.util.stream.IntStream;


public class FuncEx01 {
	static class User implements Comparable<User>{
		final Long id;
		final String name;
		final Integer age;
		
		public User(Long id, String name, Integer age) {
			this.id = id;
			this.name = name;
			this.age = age;
		}

		@Override
		public String toString() {
			return "[id=" + id + ", name=" + name + ", age=" + age + "]\n";
		}

		@Override
		public int compareTo(User o) {
			return Integer.compare(this.age, o.age);
		}
	}
	
	static List<User> getUsers(){
		return Arrays.asList( 
				new User(10L, "ID", 36),
				new User(20L, "BJ", 32),
				new User(30L, "JM", 32),
				new User(40L, "PJ", 27),
				new User(50L, "HA", 25),
				new User(60L, "JE", 26),
				new User(70L, "JI", 31),
				new User(80L, "MP", 23)
				);
	}
	
	public static void main(String[] args) {
		List<User> users = getUsers();
		
		
		
		/*
		System.out.println("filter = "+
				filter(users,user->user.age>30)+"\n"
				+users);
		
		System.out.println("map = "+
				map(users, user->user.id));
		each(Arrays.asList(1,2,3,4,5),System.out::print);
		System.out.println();
		
		System.out.println("reduce = "+
				reduce(Arrays.asList(1,2,3,4,5), (a,b)->a+b, Integer.valueOf(0)));
		System.out.println("reduce = "+
				reduce(Arrays.asList(1,2,3,4,5), (a,b)->a+b));
		System.out.println("reduce = "+
				reduce(Arrays.asList(1), (a,b)->a+b, Integer.valueOf(0)));
		System.out.println("reduce = "+
				reduce(Arrays.asList(1), (a,b)->a+b));
		
		List<String> result = Stream.stream(users)
			.filter(user->user.age > 30)
			.map(user->user.name)
			.toList();
		System.out.println(result);
		//메서드 체이닝 방식과 차이 비교
		System.out.println("default = "+
					reduce(
							map(
								filter(users,user->user.age>30) 
								, user->user.age)
							, (a,b)->a+b));
		
		//기존은 안쪽에서부터 밖으로 나온다. 중첩구조가 심해질 수록 가독성이 안좋아진다.
		//스트림을 통한 메서드 체이닝 방식은 순차적으로 적용된다.
		
		System.out.println("stream = " +
				Stream.stream(users)
					.filter(user->user.age > 30)
					.map(user->user.age)
					.reduce(Integer::sum)
//					.reduce((a,b)->a+b)
				);
		*/
		/*
		//find, findIndex 실습
		System.out.println(find(users, user->user.age<32));
		System.out.println(find(users, user->user.age>55));
		System.out.println(findIndex(users, user->user.age<32));
		System.out.println(findIndex(users, user->user.age>55));
		System.out.println(Stream.stream(users).find(user->user.age<32));
		System.out.println(Stream.stream(users).find(user->user.age>55));
		System.out.println(Stream.stream(users).findIndex( user->user.age<32));
		System.out.println(Stream.stream(users).findIndex( user->user.age>55));
		
		System.out.println("every , find 테스트");
		System.out.println(some(users, u->u.age>30));
		System.out.println(some(users, u->u.age>50));
		System.out.println(every(users, u->u.age>30));
		System.out.println(every(users, u->u.age>10));
		System.out.println(Stream.stream(users).some(u->u.age>30));
		System.out.println(Stream.stream(users).some(u->u.age>50));
		System.out.println(Stream.stream(users).every(u->u.age>30));
		System.out.println(Stream.stream(users).every(u->u.age>10));
		System.out.println("pluck 테스트");
		System.out.println(pluck(users, "age",Integer.class));
		//타입불일치
		System.out.println(pluck(users, "age",String.class));
		System.out.println(Stream.stream(users).pluck("age",Integer.class));
		//타입불일치
		System.out.println(Stream.stream(users).pluck("age",String.class));
		
		System.out.println("reject 테스트");
		System.out.println(reject(users,u->u.age>30));
		System.out.println(Stream.stream(users).reject(u->u.age>30).toList());
		*/
		System.out.println("min, max, minBy, maxBy 테스트");
		System.out.println("나이 "+min(users, (u1,u2)->Integer.compare(u1.age,u2.age)));
		System.out.println("나이 "+max(users, (u1,u2)->Integer.compare(u1.age,u2.age)));
		System.out.println("이름 "+min(users, (u1,u2)-> u1.name.compareTo(u2.name) ));
		System.out.println("이름 "+max(users, (u1,u2)-> u1.name.compareTo(u2.name) ));
		System.out.println("나이 "+minBy(users, Integer::compare, u->u.age));
		System.out.println("나이 "+maxBy(users, Integer::compare, u->u.age));
		System.out.println(
				Stream.stream(users)
					.map(u->u.age)
					.max(Integer::compare));
		System.out.println(
				Stream.stream(users)
				.minBy(Long::compare,u->u.id));
	}
	
	/*
	 * filter는 처리 결과가 입력 결과와 타입은 같다.
	 * 길이는 같거나, 작을 수 밖에 없다.
	 */
	static <T> List<T> filter(List<T> list, Predicate<T> predi) {
		//함수형 프로그래밍은 원본 데이터를 수정하지 않는다. 새로운 데이터를 리턴하여
		//부수효과를 극단적으로 배제한다.
		ArrayList<T> newList = new ArrayList<>();
		each(list,data->{
			if(predi.test(data))
				newList.add(data);
		});
		
		return newList;
	}
	/*
	 * filter 정반대, 주어진 조건에 해당하지 않은 값만 걸러낸다.
	 */
	static <T> List<T> reject(List<T> list, Predicate<T> predi){
		return filter(list, predi.negate());
	}
	/*
	 * map은 처리 결과가 입력 결과와 타입은 같거나 다르다.
	 * 길이는 항상 같다.
	 */
	static <T,R> List<R> map(List<T> list, Function<T, R> mapper){
		ArrayList<R> newList = new ArrayList<>();
		each(list, data->newList.add(mapper.apply(data)));
		return newList;
	}
	/*
	 * 반복문을 중복을 대체할 함수
	 */
	static <T> List<T> each(List<T> list, Consumer<T> iter){
		for(T data : list) 
			iter.accept(data);
		return list;
	}
	
	/*
	 * 자바는 스트림 안에서 외부 변수를 참조하면 그 변수는 final 속성이 된다. 
	 * 따라서 갱신하면서 누산을 하지 못해, 간접적으로 값을 수정해야 한다.
	 */
	static <T> T reduce(List<T> list, BinaryOperator<T> reducer ,T memo) {
		//간소화된 유효성 검사, 본질을 흐리지 않는 선에서 간략화
		if(memo == null ) return reduce(list.subList(1, list.size()), reducer, list.get(0));
		if(list.size() < 2) return list.get(0);
		HashMap<Class<?>, T> map = new HashMap<>();
		map.put(memo.getClass(), memo);
		
		each(list, data->map.compute(memo.getClass(), (k,v)->reducer.apply(v, data)));
		return map.get(memo.getClass());
	}
	/*
	 * 자바는 람다에서 사용하는 변수는 final 속성을 띄기 때문에 memo 값 처리가 애매해진다.
	 * 삼항연산자 등 별도 처리하기보다 오버로딩으로 처리했다.
	 */
	static <T> T reduce(List<T> list, BinaryOperator<T> reducer) {
		if(list.size() < 2) return list.get(0);
		return reduce(list.subList(1, list.size()), reducer, list.get(0));
	}
	/*
	 * null 값을 간접적으로 다루기 위한 래퍼클래스 사용
	 */
	static <T> Optional<T> find(List<T> list, Predicate<T> predi) {
		for(int i=0;i<list.size();i++) {
			T value = list.get(i);
			if(predi.test(value)) return Optional.of(value);
		}
		return Optional.empty();//편의상 null리턴
	}
	static <T> Integer findIndex(List<T> list, Predicate<T> predi) {
		for(int i=0;i<list.size();i++) {
			if(predi.test(list.get(i))) return i;
		}
		return -1;
	}
	
	static <T> Boolean some(List<T> list, Predicate<T> predi) {
		return findIndex(list, predi) != -1;
	}
	static <T> Boolean every(List<T> list, Predicate<T> predi) {
		return findIndex(list, predi.negate()) == -1;
	}
	static <T> Boolean contains(List<T> list, T data){
		return findIndex(list, val-> Objects.equals(val, data) ) != -1 ;
	}
	static <T,R> List<R> pluck(List<T> list, String key,Class<R> typeToken){
		List<R> result = map(list, val-> pluckHelper(val, key, typeToken));
		return some(result, Objects::isNull).booleanValue() ? Collections.emptyList() :result;
	}
	//런타임 시 타입이 확정되는 경우 어쩔 수 없이 Object를 리턴해야 한다.
	static <T,R> R pluckHelper(T val,String key,Class<R> typeToken) {
		try {
			Field field = val.getClass().getDeclaredField(key);
			return typeToken.cast(field.get(val));
		} catch (Exception e) {
			return null;
		}
	}
	/*
	 * reduce 특화 함수
	 */
	static <T extends Comparable<T>> T min(List<T> list) {
		return reduce(list, (a,b)->a.compareTo(b)<=0 ? a : b );
	}
	static <T extends Comparable<T>> T max(List<T> list) {
		return reduce(list, (a,b)->a.compareTo(b)>0 ? a : b );
	}
	static <T> T min(List<T> list, Comparator<T> comparator) {
		return reduce(list, BinaryOperator.minBy(comparator));
	}
	
	static <T> T max(List<T> list, Comparator<T> comparator) {
		return reduce(list, BinaryOperator.maxBy(comparator));
	}
	//주어진 조건으로 검사한 결과로 최대 최소 값을 판단한다.
	static <T,R> T minBy(List<T> list,Comparator<R> comparator ,Function<T, R> mapper) {
		return reduce(list, (a,b)-> comparator.compare(mapper.apply(a), mapper.apply(b)) > 0 ? b:a);
	}
	static <T,R> T maxBy(List<T> list,Comparator<R> comparator ,Function<T, R> mapper) {
		return minBy(list,comparator.reversed(),mapper);
	}
	
	
	/*
	 * pipe, go 구현을 시도하려 했지만, 근본적으로 자바는 함수가 개념이 없어
	 * 호출부를 단일로 추상화할 수 없다. apply, test ... 
	 * 따라서 하나의 클래스로 묶었다.
	 */
	static class Stream<T> {
		List<T> list;
		
		static <T> Stream<T> stream(List<T> list){
			Stream<T> stream = new Stream<>();
			stream.list = list;
			return stream;
		}
		Stream<T> filter(Predicate<T> predi) {
			//함수형 프로그래밍은 원본 데이터를 수정하지 않는다. 새로운 데이터를 리턴하여
			//부수효과를 극단적으로 배제한다.
			ArrayList<T> newList = new ArrayList<>();
			forEach(data->{
				if(predi.test(data))
					newList.add(data);
			});
			return stream(newList);
		}
		Stream<T> reject(Predicate<T> predi){
			return filter(predi.negate());
		}
		<R> Stream<R> map(Function<T, R> mapper){
			ArrayList<R> newList = new ArrayList<>();
			forEach(data->newList.add(mapper.apply(data)));
			return stream(newList);
		}
		
		void forEach(Consumer<T> iter){
			for(T data : list) iter.accept(data);
		}
		
		Optional<T> reduce(BinaryOperator<T> reducer ,T memo) {
			//간소화된 유효성 검사, 본질을 흐리지 않는 선에서 간략화
			if(memo == null) return reduce(reducer);
			if(this.list.size() < 2) return Optional.of(this.list.get(0));
			HashMap<Class<?>, T> map = new HashMap<>();
			map.put(memo.getClass(), memo);
			each(this.list, data->map.compute(memo.getClass(), (k,v)-> reducer.apply(v, data)));
			return Optional.of(map.get(memo.getClass()));
		}
		Optional<T> reduce(BinaryOperator<T> reducer) {
			T tmpValue = this.list.get(0);
			if(this.list.size() < 2) return Optional.of(tmpValue);
			this.list = this.list.subList(1, list.size());
			return reduce(reducer, tmpValue);
		}
		
		Optional<T> find(Predicate<T> predi) {
			for(int i=0;i<this.list.size();i++) {
				T value = this.list.get(i);
				if(predi.test(value)) return Optional.of(value);
			}
			return Optional.empty();//편의상 null리턴
		}
		Integer findIndex(Predicate<T> predi) {
			for(int i=0;i<this.list.size();i++) {
				if(predi.test(this.list.get(i))) return i;
			}
			return -1;//편의상 null리턴
		}
		
		Boolean some(Predicate<T> predi) {
			return findIndex(predi) != -1;
		}
		Boolean every(Predicate<T> predi) {
			return findIndex(predi.negate()) == -1;
		}
		<R> List<R> pluck(String key,Class<R> typeToken){
			Stream<R> result = map(val-> pluckHelper(val, key, typeToken));
			return result.some(Objects::isNull).booleanValue() ? Collections.emptyList() :result.toList();
		}
		//런타임 시 타입이 확정되는 경우 어쩔 수 없이 Object를 리턴해야 한다.
		<R> R pluckHelper(T val,String key,Class<R> typeToken) {
			try {
				Field field = val.getClass().getDeclaredField(key);
				return typeToken.cast(field.get(val));
			} catch (Exception e) {
				return null;
			}
		}
		
		/*
		 * reduce 특화 함수
		 */
		Optional<T> min(Comparator<? super T> comparator) {
			return reduce(BinaryOperator.<T>minBy(comparator));
		}
		Optional<T> max(Comparator<? super T> comparator) {
			return reduce(BinaryOperator.<T>maxBy(comparator));
		}
		//주어진 조건으로 검사한 결과로 최대 최소 값을 판단한다.
		<R> Optional<T> minBy(Comparator<? super R> comparator ,Function<T, R> mapper) {
			return reduce( (a,b)-> comparator.compare(mapper.apply(a), mapper.apply(b)) > 0 ? b:a);
		}
		<R> Optional<T> maxBy(Comparator<? super R> comparator ,Function<T, R> mapper) {
			return minBy(comparator.reversed(),mapper);
		}
		
		List<T> toList(){
			return this.list;
		}
		
	}
	
	
}

pluck

주어진 key 값에 해당하는 값으로 이루어진 리스트를 리턴한다.

/*요소에서 특정 값만 추출하는 함수 */
function pluck(list, key){
    return map(list, obj => obj[key]);
}

위와 같이 동적 언어인 자바스크립트 처럼 유연하게 대응하기가 자바는 힘들다.

먼저 타입 T 는 호출할 때마다 달라지고, 호출자에 의해 타입이 결정된다. 그래서 런타임 시 클래스 정보를 얻어올 수 있는 리플랙션 API를 사용해 값을 얻어왔다. 이때 리턴 타입은 Object 다. 그래서 추가로 타입 토큰을 인자로 받아 형변환에 사용했다.

static <T,R> List<R> pluck(List<T> list, String key,Class<R> typeToken){
    List<R> result = map(list, val-> pluckHelper(val, key, typeToken));
    return some(result, Objects::isNull).booleanValue() ? Collections.emptyList() :result;
}
//자바스크립트 같이 동적 언어와 다르게 자바는 정적 언어로 아래와 같이 타입토큰으로 형변환을 했다.
static <T,R> R pluckHelper(T val,String key,Class<R> typeToken) {
    try {
        Field field = val.getClass().getDeclaredField(key);
        return typeToken.cast(field.get(val));
    } catch (Exception e) {
        return null;
    }
}

/*Stream 클래스 메서드*/
<R> List<R> pluck(String key,Class<R> typeToken){
    Stream<R> result = map(val-> pluckHelper(val, key, typeToken));
    return result.some(Objects::isNull).booleanValue() ? Collections.emptyList() :result.toList();
}
<R> R pluckHelper(T val,String key,Class<R> typeToken) {
    try {
        Field field = val.getClass().getDeclaredField(key);
        return typeToken.cast(field.get(val));
    } catch (Exception e) {
        return null;
    }
}
		System.out.println("pluck 테스트");
		System.out.println(pluck(users, "age",Integer.class));
		//타입불일치
		System.out.println(pluck(users, "age",String.class));
		System.out.println(Stream.stream(users).pluck("age",Integer.class));
		//타입불일치
		System.out.println(Stream.stream(users).pluck("age",String.class));

 

pluck 테스트
[36, 32, 32, 27, 25, 26, 31, 23]
[]
[36, 32, 32, 27, 25, 26, 31, 23]
[]

reject

/*
 * filter 정반대, 주어진 조건에 해당하지 않은 값만 걸러낸다.
 */
static <T> List<T> reject(List<T> list, Predicate<T> predi){
    return filter(list, predi.negate());
}


/*Stream 클래스 메서드*/
Stream<T> reject(Predicate<T> predi){
    return filter(predi.negate());
}

전체 코드

package javabasic.stream;

import java.lang.reflect.Field;
import java.util.*;
import java.util.function.*;

import javabasic.stream.FuncEx01.Stream;

public class FuncEx01 {
	static class User{
		final Long id;
		final String name;
		final Integer age;
		
		public User(Long id, String name, Integer age) {
			this.id = id;
			this.name = name;
			this.age = age;
		}

		@Override
		public String toString() {
			return "[id=" + id + ", name=" + name + ", age=" + age + "]\n";
		}
		
	}
	
	static List<User> getUsers(){
		return Arrays.asList( 
				new User(10L, "ID", 36),
				new User(20L, "BJ", 32),
				new User(30L, "JM", 32),
				new User(40L, "PJ", 27),
				new User(50L, "HA", 25),
				new User(60L, "JE", 26),
				new User(70L, "JI", 31),
				new User(80L, "MP", 23)
				);
	}
	
	
	public static void main(String[] args) {
		List<User> users = getUsers();
		/*
		System.out.println("filter = "+
				filter(users,user->user.age>30)+"\n"
				+users);
		
		System.out.println("map = "+
				map(users, user->user.id));
		each(Arrays.asList(1,2,3,4,5),System.out::print);
		System.out.println();
		
		System.out.println("reduce = "+
				reduce(Arrays.asList(1,2,3,4,5), (a,b)->a+b, Integer.valueOf(0)));
		System.out.println("reduce = "+
				reduce(Arrays.asList(1,2,3,4,5), (a,b)->a+b));
		System.out.println("reduce = "+
				reduce(Arrays.asList(1), (a,b)->a+b, Integer.valueOf(0)));
		System.out.println("reduce = "+
				reduce(Arrays.asList(1), (a,b)->a+b));
		
		List<String> result = Stream.stream(users)
			.filter(user->user.age > 30)
			.map(user->user.name)
			.toList();
		System.out.println(result);
		//메서드 체이닝 방식과 차이 비교
		System.out.println("default = "+
					reduce(
							map(
								filter(users,user->user.age>30) 
								, user->user.age)
							, (a,b)->a+b));
		
		//기존은 안쪽에서부터 밖으로 나온다. 중첩구조가 심해질 수록 가독성이 안좋아진다.
		//스트림을 통한 메서드 체이닝 방식은 순차적으로 적용된다.
		
		System.out.println("stream = " +
				Stream.stream(users)
					.filter(user->user.age > 30)
					.map(user->user.age)
					.reduce(Integer::sum)
//					.reduce((a,b)->a+b)
				);
		*/
		//find, findIndex 실습
		System.out.println(find(users, user->user.age<32));
		System.out.println(find(users, user->user.age>55));
		System.out.println(findIndex(users, user->user.age<32));
		System.out.println(findIndex(users, user->user.age>55));
		System.out.println(Stream.stream(users).find(user->user.age<32));
		System.out.println(Stream.stream(users).find(user->user.age>55));
		System.out.println(Stream.stream(users).findIndex( user->user.age<32));
		System.out.println(Stream.stream(users).findIndex( user->user.age>55));
		
		System.out.println("every , find 테스트");
		System.out.println(some(users, u->u.age>30));
		System.out.println(some(users, u->u.age>50));
		System.out.println(every(users, u->u.age>30));
		System.out.println(every(users, u->u.age>10));
		System.out.println(Stream.stream(users).some(u->u.age>30));
		System.out.println(Stream.stream(users).some(u->u.age>50));
		System.out.println(Stream.stream(users).every(u->u.age>30));
		System.out.println(Stream.stream(users).every(u->u.age>10));
		
		System.out.println("pluck 테스트");
		System.out.println(pluck(users, "age",Integer.class));
		//타입불일치
		System.out.println(pluck(users, "age",String.class));
		System.out.println(Stream.stream(users).pluck("age",Integer.class));
		//타입불일치
		System.out.println(Stream.stream(users).pluck("age",String.class));
		
		System.out.println("reject 테스트");
		System.out.println(reject(users,u->u.age>30));
		System.out.println(Stream.stream(users).reject(u->u.age>30).toList());
		
	}
	
	/*
	 * filter는 처리 결과가 입력 결과와 타입은 같다.
	 * 길이는 같거나, 작을 수 밖에 없다.
	 */
	static <T> List<T> filter(List<T> list, Predicate<T> predi) {
		//함수형 프로그래밍은 원본 데이터를 수정하지 않는다. 새로운 데이터를 리턴하여
		//부수효과를 극단적으로 배제한다.
		ArrayList<T> newList = new ArrayList<>();
		each(list,data->{
			if(predi.test(data))
				newList.add(data);
		});
		
		return newList;
	}
	/*
	 * filter 정반대, 주어진 조건에 해당하지 않은 값만 걸러낸다.
	 */
	static <T> List<T> reject(List<T> list, Predicate<T> predi){
		return filter(list, predi.negate());
	}
	/*
	 * map은 처리 결과가 입력 결과와 타입은 같거나 다르다.
	 * 길이는 항상 같다.
	 */
	static <T,R> List<R> map(List<T> list, Function<T, R> mapper){
		ArrayList<R> newList = new ArrayList<>();
		each(list, data->newList.add(mapper.apply(data)));
		return newList;
	}
	/*
	 * 반복문을 중복을 대체할 함수
	 */
	static <T> List<T> each(List<T> list, Consumer<T> iter){
		for(T data : list) 
			iter.accept(data);
		return list;
	}
	
	/*
	 * 자바는 스트림 안에서 외부 변수를 참조하면 그 변수는 final 속성이 된다. 
	 * 따라서 갱신하면서 누산을 하지 못해, 간접적으로 값을 수정해야 한다.
	 */
	static <T> T reduce(List<T> list, BinaryOperator<T> reducer ,T memo) {
		//간소화된 유효성 검사, 본질을 흐리지 않는 선에서 간략화
		if(memo == null ) return reduce(list.subList(1, list.size()), reducer, list.get(0));
		if(list.size() < 2) return list.get(0);
		HashMap<Class<?>, T> map = new HashMap<>();
		map.put(memo.getClass(), memo);
		
		each(list, data->map.compute(memo.getClass(), (k,v)->reducer.apply(v, data)));
		return map.get(memo.getClass());
	}
	/*
	 * 자바는 람다에서 사용하는 변수는 final 속성을 띄기 때문에 memo 값 처리가 애매해진다.
	 * 삼항연산자 등 별도 처리하기보다 오버로딩으로 처리했다.
	 */
	static <T> T reduce(List<T> list, BinaryOperator<T> reducer) {
		if(list.size() < 2) return list.get(0);
		return reduce(list.subList(1, list.size()), reducer, list.get(0));
	}
	/*
	 * null 값을 간접적으로 다루기 위한 래퍼클래스 사용
	 */
	static <T> Optional<T> find(List<T> list, Predicate<T> predi) {
		for(int i=0;i<list.size();i++) {
			T value = list.get(i);
			if(predi.test(value)) return Optional.of(value);
		}
		return Optional.empty();//편의상 null리턴
	}
	static <T> Integer findIndex(List<T> list, Predicate<T> predi) {
		for(int i=0;i<list.size();i++) {
			if(predi.test(list.get(i))) return i;
		}
		return -1;
	}
	
	static <T> Boolean some(List<T> list, Predicate<T> predi) {
		return findIndex(list, predi) != -1;
	}
	static <T> Boolean every(List<T> list, Predicate<T> predi) {
		return findIndex(list, predi.negate()) == -1;
//		return findIndex(list, val->!predi.test(val)) == -1;
	}
	static <T> Boolean contains(List<T> list, T data){
		return findIndex(list, val-> Objects.equals(val, data) ) != -1 ;
	}
	static <T,R> List<R> pluck(List<T> list, String key,Class<R> typeToken){
		List<R> result = map(list, val-> pluckHelper(val, key, typeToken));
		return some(result, Objects::isNull).booleanValue() ? Collections.emptyList() :result;
	}
	//런타임 시 타입이 확정되는 경우 어쩔 수 없이 Object를 리턴해야 한다.
	static <T,R> R pluckHelper(T val,String key,Class<R> typeToken) {
		try {
			Field field = val.getClass().getDeclaredField(key);
			return typeToken.cast(field.get(val));
		} catch (Exception e) {
			return null;
		}
	}
	/*
	 * pipe, go 구현을 시도하려 했지만, 근본적으로 자바는 함수가 개념이 없어
	 * 호출부를 단일로 추상화할 수 없다. apply, test ... 
	 * 따라서 하나의 클래스로 묶었다.
	 */
	static class Stream<T> {
		List<T> list;
		
		static <T> Stream<T> stream(List<T> list){
			Stream<T> stream = new Stream<>();
			stream.list = list;
			return stream;
		}
		Stream<T> filter(Predicate<T> predi) {
			//함수형 프로그래밍은 원본 데이터를 수정하지 않는다. 새로운 데이터를 리턴하여
			//부수효과를 극단적으로 배제한다.
			ArrayList<T> newList = new ArrayList<>();
			forEach(data->{
				if(predi.test(data))
					newList.add(data);
			});
			return stream(newList);
		}
		Stream<T> reject(Predicate<T> predi){
			return filter(predi.negate());
		}
		<R> Stream<R> map(Function<T, R> mapper){
			ArrayList<R> newList = new ArrayList<>();
			forEach(data->newList.add(mapper.apply(data)));
			return stream(newList);
		}
		
		void forEach(Consumer<T> iter){
			for(T data : list) iter.accept(data);
		}
		
		T reduce(BinaryOperator<T> reducer ,T memo) {
			//간소화된 유효성 검사, 본질을 흐리지 않는 선에서 간략화
			if(memo == null) return reduce(reducer);
			if(this.list.size() < 2) return this.list.get(0);
			HashMap<Class<?>, T> map = new HashMap<>();
			map.put(memo.getClass(), memo);
			each(this.list, data->map.compute(memo.getClass(), (k,v)->{
				T result = reducer.apply(v, data);
				return result;
			}));
			return map.get(memo.getClass());
		}
		T reduce(BinaryOperator<T> reducer) {
			T tmpValue = this.list.get(0);
			if(this.list.size() < 2) return tmpValue;
			this.list = this.list.subList(1, list.size());
			return reduce(reducer, tmpValue);
		}
		
		Optional<T> find(Predicate<T> predi) {
			for(int i=0;i<this.list.size();i++) {
				T value = this.list.get(i);
				if(predi.test(value)) return Optional.of(value);
			}
			return Optional.empty();//편의상 null리턴
		}
		Integer findIndex(Predicate<T> predi) {
			for(int i=0;i<this.list.size();i++) {
				if(predi.test(this.list.get(i))) return i;
			}
			return -1;//편의상 null리턴
		}
		
		Boolean some(Predicate<T> predi) {
			return findIndex(predi) != -1;
		}
		Boolean every(Predicate<T> predi) {
			return findIndex(predi.negate()) == -1;
		}
		<R> List<R> pluck(String key,Class<R> typeToken){
			Stream<R> result = map(val-> pluckHelper(val, key, typeToken));
			return result.some(Objects::isNull).booleanValue() ? Collections.emptyList() :result.toList();
		}
		//런타임 시 타입이 확정되는 경우 어쩔 수 없이 Object를 리턴해야 한다.
		<R> R pluckHelper(T val,String key,Class<R> typeToken) {
			try {
				Field field = val.getClass().getDeclaredField(key);
				return typeToken.cast(field.get(val));
			} catch (Exception e) {
				return null;
			}
		}
		List<T> toList(){
			return this.list;
		}
		
	}
	
	
}
 
 

 

 

오류: 네트워크 연결 상태를 확인하세요.

find, findIndex

find() 주어진 조건에 일치하는 요소를 찾으면 그 요소를 반환한다.

findIndex() 주어진 조건에 일치하는 요소 인덱스를 리턴한다.

 

반복문을 끝까지 순회하지 않고 순환을 제어해야 한다. 그래서 each를 사용 못한다.

findIndex가 추상화 수준이 더 높아 다른 곳에 활용도가 많다.

예를 들어, 지네릭 메서드, 지네릭 클래스로 코드를 정의하면, find는 타입변수에 따라 리턴타입이 가변적이라 동일한 수준으로 제어하기가 힘들다. 반면에 findIndex는 인덱스를 리턴하므로 모든 클래스를 같은 수준에서 다룰 수 있다. 

 

/*유틸클래스 스태틱 메서드*/
static <T> Optional<T> find(List<T> list, Predicate<T> predi) {
    for(int i=0;i<list.size();i++) {
        T value = list.get(i);
        if(predi.test(value)) return Optional.of(value);
    }
    return Optional.empty();//편의상 null리턴
}
static <T> Integer findIndex(List<T> list, Predicate<T> predi) {
    for(int i=0;i<list.size();i++) {
        if(predi.test(list.get(i))) return i;
    }
    return -1;
}


/* String 클래스 메서드*/
Optional<T> find(Predicate<T> predi) {
    for(int i=0;i<this.list.size();i++) {
        T value = this.list.get(i);
        if(predi.test(value)) return Optional.of(value);
    }
    return Optional.empty();//편의상 null리턴
}
Integer findIndex(Predicate<T> predi) {
    for(int i=0;i<this.list.size();i++) {
        if(predi.test(this.list.get(i))) return i;
    }
    return -1;
}
		//find, findIndex 실습
		System.out.println(find(users, user->user.age<32));
		System.out.println(find(users, user->user.age>55));
		System.out.println(findIndex(users, user->user.age<32));
		System.out.println(findIndex(users, user->user.age>55));
		System.out.println(Stream.stream(users).find(user->user.age<32));
		System.out.println(Stream.stream(users).find(user->user.age>55));
		System.out.println(Stream.stream(users).findIndex( user->user.age<32));
		System.out.println(Stream.stream(users).findIndex( user->user.age>55));
Optional[[id=40, name=PJ, age=27]
]
Optional.empty
3
-1
Optional[[id=40, name=PJ, age=27]
]
Optional.empty
3
-1

some, every

findIndex()메서드를 활용해서 만든다.

some()메서드는 주어진 조건에 하나라도 일치하는 값이 있으면 참을 반환한다.

every()메서드는 주어진 조건에 모두 일치하면 참을 반환한다.

 

some() 메서드의 경우, findIndex() 호출 시 못찾는 경우인 -1만 아니면 찾은 것

every() 메서드의 경우, 반드시 모든 리스트 요소를 순회해서 검색해야 한다. 

findIndex() 는 모든 요소를 탐색해 못찾으면 -1을 반환한다. 즉, 끝까지 탐색했음을 보장하는 값이 -1 반환이다.

이점을 이용해 주어진 조건에 반대 조건으로 검사해 -1을 반환한다면, 이는 주어진 조건에 모두 해당하는 것과 같다.

static <T> Boolean some(List<T> list, Predicate<T> predi) {
    return findIndex(list, predi) != -1;
}
static <T> Boolean every(List<T> list, Predicate<T> predi) {
    return findIndex(list, predi.negate()) == -1;
}
    
    
    
/*Stream 클래스 메서드*/
Boolean some(Predicate<T> predi) {
    return findIndex(predi) != -1;
}
Boolean every(Predicate<T> predi) {
    return findIndex(predi.negate()) == -1;
}
		System.out.println("every , find 테스트");
		System.out.println(some(users, u->u.age>30));
		System.out.println(some(users, u->u.age>50));
		System.out.println(every(users, u->u.age>30));
		System.out.println(every(users, u->u.age>10));
		System.out.println(Stream.stream(users).some(u->u.age>30));
		System.out.println(Stream.stream(users).some(u->u.age>50));
		System.out.println(Stream.stream(users).every(u->u.age>30));
		System.out.println(Stream.stream(users).every(u->u.age>10));
every , find 테스트
true
false
false
true
true
false
false
true

전체 코드

package javabasic.stream;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;

public class FuncEx01 {
	static class User{
		final Long id;
		final String name;
		final Integer age;
		
		public User(Long id, String name, Integer age) {
			this.id = id;
			this.name = name;
			this.age = age;
		}

		@Override
		public String toString() {
			return "[id=" + id + ", name=" + name + ", age=" + age + "]\n";
		}
		
	}
	
	static List<User> getUsers(){
		return Arrays.asList( 
				new User(10L, "ID", 36),
				new User(20L, "BJ", 32),
				new User(30L, "JM", 32),
				new User(40L, "PJ", 27),
				new User(50L, "HA", 25),
				new User(60L, "JE", 26),
				new User(70L, "JI", 31),
				new User(80L, "MP", 23)
				);
	}
	
	
	public static void main(String[] args) {
		List<User> users = getUsers();
		/*
		System.out.println("filter = "+
				filter(users,user->user.age>30)+"\n"
				+users);
		
		System.out.println("map = "+
				map(users, user->user.id));
		each(Arrays.asList(1,2,3,4,5),System.out::print);
		System.out.println();
		
		System.out.println("reduce = "+
				reduce(Arrays.asList(1,2,3,4,5), (a,b)->a+b, Integer.valueOf(0)));
		System.out.println("reduce = "+
				reduce(Arrays.asList(1,2,3,4,5), (a,b)->a+b));
		System.out.println("reduce = "+
				reduce(Arrays.asList(1), (a,b)->a+b, Integer.valueOf(0)));
		System.out.println("reduce = "+
				reduce(Arrays.asList(1), (a,b)->a+b));
		
		List<String> result = Stream.stream(users)
			.filter(user->user.age > 30)
			.map(user->user.name)
			.toList();
		System.out.println(result);
		//메서드 체이닝 방식과 차이 비교
		System.out.println("default = "+
					reduce(
							map(
								filter(users,user->user.age>30) 
								, user->user.age)
							, (a,b)->a+b));
		
		//기존은 안쪽에서부터 밖으로 나온다. 중첩구조가 심해질 수록 가독성이 안좋아진다.
		//스트림을 통한 메서드 체이닝 방식은 순차적으로 적용된다.
		
		System.out.println("stream = " +
				Stream.stream(users)
					.filter(user->user.age > 30)
					.map(user->user.age)
					.reduce(Integer::sum)
//					.reduce((a,b)->a+b)
				);
		*/
		//find, findIndex 실습
		System.out.println(find(users, user->user.age<32));
		System.out.println(find(users, user->user.age>55));
		System.out.println(findIndex(users, user->user.age<32));
		System.out.println(findIndex(users, user->user.age>55));
		System.out.println(Stream.stream(users).find(user->user.age<32));
		System.out.println(Stream.stream(users).find(user->user.age>55));
		System.out.println(Stream.stream(users).findIndex( user->user.age<32));
		System.out.println(Stream.stream(users).findIndex( user->user.age>55));
		
		System.out.println("every , find 테스트");
		System.out.println(some(users, u->u.age>30));
		System.out.println(some(users, u->u.age>50));
		System.out.println(every(users, u->u.age>30));
		System.out.println(every(users, u->u.age>10));
		System.out.println(Stream.stream(users).some(u->u.age>30));
		System.out.println(Stream.stream(users).some(u->u.age>50));
		System.out.println(Stream.stream(users).every(u->u.age>30));
		System.out.println(Stream.stream(users).every(u->u.age>10));
		
	}
	
	/*
	 * filter는 처리 결과가 입력 결과와 타입은 같다.
	 * 길이는 같거나, 작을 수 밖에 없다.
	 */
	static <T> List<T> filter(List<T> list, Predicate<T> predi) {
		//함수형 프로그래밍은 원본 데이터를 수정하지 않는다. 새로운 데이터를 리턴하여
		//부수효과를 극단적으로 배제한다.
		ArrayList<T> newList = new ArrayList<>();
		each(list,data->{
			if(predi.test(data))
				newList.add(data);
		});
		
		return newList;
	}
	/*
	 * map은 처리 결과가 입력 결과와 타입은 같거나 다르다.
	 * 길이는 항상 같다.
	 */
	static <T,R> List<R> map(List<T> list, Function<T, R> mapper){
		ArrayList<R> newList = new ArrayList<>();
		each(list, data->newList.add(mapper.apply(data)));
		return newList;
	}
	/*
	 * 반복문을 중복을 대체할 함수
	 */
	static <T> List<T> each(List<T> list, Consumer<T> iter){
		for(T data : list) 
			iter.accept(data);
		return list;
	}
	
	/*
	 * 자바는 스트림 안에서 외부 변수를 참조하면 그 변수는 final 속성이 된다. 
	 * 따라서 갱신하면서 누산을 하지 못해, 간접적으로 값을 수정해야 한다.
	 */
	static <T> T reduce(List<T> list, BinaryOperator<T> reducer ,T memo) {
		//간소화된 유효성 검사, 본질을 흐리지 않는 선에서 간략화
		if(memo == null ) return reduce(list.subList(1, list.size()), reducer, list.get(0));
		if(list.size() < 2) return list.get(0);
		HashMap<Class<?>, T> map = new HashMap<>();
		map.put(memo.getClass(), memo);
		
		each(list, data->map.compute(memo.getClass(), (k,v)->reducer.apply(v, data)));
		return map.get(memo.getClass());
	}
	/*
	 * 자바는 람다에서 사용하는 변수는 final 속성을 띄기 때문에 memo 값 처리가 애매해진다.
	 * 삼항연산자 등 별도 처리하기보다 오버로딩으로 처리했다.
	 */
	static <T> T reduce(List<T> list, BinaryOperator<T> reducer) {
		if(list.size() < 2) return list.get(0);
		return reduce(list.subList(1, list.size()), reducer, list.get(0));
	}
	/*
	 * null 값을 간접적으로 다루기 위한 래퍼클래스 사용
	 */
	static <T> Optional<T> find(List<T> list, Predicate<T> predi) {
		for(int i=0;i<list.size();i++) {
			T value = list.get(i);
			if(predi.test(value)) return Optional.of(value);
		}
		return Optional.empty();//편의상 null리턴
	}
	static <T> Integer findIndex(List<T> list, Predicate<T> predi) {
		for(int i=0;i<list.size();i++) {
			if(predi.test(list.get(i))) return i;
		}
		return -1;
	}
	
	static <T> Boolean some(List<T> list, Predicate<T> predi) {
		return findIndex(list, predi) != -1;
	}
	static <T> Boolean every(List<T> list, Predicate<T> predi) {
		return findIndex(list, predi.negate()) == -1;
	}
	
	
	/*
	 * pipe, go 구현을 시도하려 했지만, 근본적으로 자바는 함수가 개념이 없어
	 * 호출부를 단일로 추상화할 수 없다. apply, test ... 
	 * 따라서 하나의 클래스로 묶었다.
	 */
	static class Stream<T> {
		List<T> list;
		
		static <T> Stream<T> stream(List<T> list){
			Stream<T> stream = new Stream<>();
			stream.list = list;
			return stream;
		}
		Stream<T> filter(Predicate<T> predi) {
			//함수형 프로그래밍은 원본 데이터를 수정하지 않는다. 새로운 데이터를 리턴하여
			//부수효과를 극단적으로 배제한다.
			ArrayList<T> newList = new ArrayList<>();
			forEach(data->{
				if(predi.test(data))
					newList.add(data);
			});
			return stream(newList);
		}
		<R> Stream<R> map(Function<T, R> mapper){
			ArrayList<R> newList = new ArrayList<>();
			forEach(data->newList.add(mapper.apply(data)));
			return stream(newList);
		}
		
		void forEach(Consumer<T> iter){
			for(T data : list) iter.accept(data);
		}
		
		T reduce(BinaryOperator<T> reducer ,T memo) {
			//간소화된 유효성 검사, 본질을 흐리지 않는 선에서 간략화
			if(memo == null) return reduce(reducer);
			if(this.list.size() < 2) return this.list.get(0);
			HashMap<Class<?>, T> map = new HashMap<>();
			map.put(memo.getClass(), memo);
			each(this.list, data->map.compute(memo.getClass(), (k,v)->{
				T result = reducer.apply(v, data);
				return result;
			}));
			return map.get(memo.getClass());
		}
		T reduce(BinaryOperator<T> reducer) {
			T tmpValue = this.list.get(0);
			if(this.list.size() < 2) return tmpValue;
			this.list = this.list.subList(1, list.size());
			return reduce(reducer, tmpValue);
		}
		
		Optional<T> find(Predicate<T> predi) {
			for(int i=0;i<this.list.size();i++) {
				T value = this.list.get(i);
				if(predi.test(value)) return Optional.of(value);
			}
			return Optional.empty();//편의상 null리턴
		}
		Integer findIndex(Predicate<T> predi) {
			for(int i=0;i<this.list.size();i++) {
				if(predi.test(this.list.get(i))) return i;
			}
			return -1;//편의상 null리턴
		}
		
		Boolean some(Predicate<T> predi) {
			return findIndex(predi) != -1;
		}
		Boolean every(Predicate<T> predi) {
			return findIndex(predi.negate()) == -1;
		}
		
		List<T> toList(){
			return this.list;
		}
		
	}
	
	
}

+ Recent posts