강한 결합

코드를 직접적으로 사용하고 있다.

public class Coupling {
	public static void main(String[] args) {
		Person person = new Person(new IPhone());
		person.call();
		person.camera();
	}
	static class Person{
		IPhone iPhone;
		
		public Person(IPhone iPhone) {
			this.iPhone = iPhone;
		}
		public void call() {
			iPhone.call();
		}
		public void camera() {
			iPhone.camera();
		}
	}
	
	static class IPhone{
		public void call() {
			System.out.println("전화걸기");
		}
		public void camera() {
			System.out.println("사진찍기");
		}
	}
}

변경

사용자가 아이폰 대신 갤럭시를 쓰려고 한다.

단순히 갤럭시 클래스를 새로 만들고 사용하려고 했지만, 기존 아이폰과 강하게 결합되어 변경을 할 수가 없다.

public class Coupling2 {
	public static void main(String[] args) {
		Person person = new Person(new IPhone());
		//강한 결합으로 갤럭시를 쓰고 싶다면
		//기존 Person 코드의 수정이 필요하다.
//		Person person2 = new Person(new Galaxy());
		person.call();
		person.camera();
	}
	
	static class Person{
		IPhone iPhone;
		
		public Person(IPhone iPhone) {
			this.iPhone = iPhone;
		}
		public void call() {
			iPhone.call();
		}
		public void camera() {
			iPhone.camera();
		}
	}
	static class Galaxy{
		public void call() {
			System.out.println("전화걸기");
		}
		public void camera() {
			System.out.println("사진찍기");
		}
	}
	
	static class IPhone{
		public void call() {
			System.out.println("전화걸기");
		}
		public void camera() {
			System.out.println("사진찍기");
		}
	}
}

인터페이스를 이용한 느슨한 결합

중간에 추상화된 것에 의존하게 됐다. 따라서 앞으로 새로운 스마트폰이 생겨도 Person 클래스는 변경이 일어나지 않는다.

public class Coupling3 {
	public static void main(String[] args) {
		Person person = new Person(new IPhone());
		//강한 결합으로 갤럭시를 쓰고 싶다면
		//기존 Person 코드의 수정이 필요하다.
		Person person2 = new Person(new Galaxy());
		person.call();
		person.camera();
		person2.call();
		person2.camera();
	}
	
	static class Person{
		SmartPhone phone;
		
		public Person(SmartPhone phone) {
			this.phone = phone;
		}
		void call() {
			phone.call();
		}
		void camera() {
			phone.camera();
		}
	}
	interface SmartPhone {
		void call();
		void camera();
	}
	
	static class Galaxy implements SmartPhone{
		public void call() {
			System.out.println("전화걸기");
		}
		public void camera() {
			System.out.println("사진찍기");
		}
	}
	
	static class IPhone implements SmartPhone{
		public void call() {
			System.out.println("전화걸기");
		}
		public void camera() {
			System.out.println("사진찍기");
		}
	}
}

 

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

Stream 클래스 정의

자바로 curry, curryr, pipe, go 등을 구현해보려 했는데, 불가능 한 것 같다.

현재 내가 생각하는 이유는 다음과 같다.

자바스크립트는 완전한 함수 타입을 지원한다. 당연히 일급시민으로 다룬다.

추가로 자바스크립트는 동적타입 언어다.

 

반면에 자바는 함수란 존재할 수 없다. 함수형 인터페이스만 가능하다.

더불어 정적타입언어, 강타입 언어다. 

 

//자바스크립트
function func1(){/*구현부*/}
function func2(){/*구현부*/}

function highOrderFunc(fn){fn()}

highOrderFunc(func1);
highOrderFunc(func2);//다양한 함수로 호출가능

위와 같이 자바스크립트는 온전한 함수를 인자로 받아 함수가 함수를 호출할 수 있다. 이때 타입체크를 강하게 하지 않는다.

	@FunctionalInterface
	static interface Func1{
		void func(String strongType);
	}
	static interface Func2{
		void func(String strongType);
	}
	
	static class FuncCall{
		void highOrderFunc(Func1 fn1) {
			fn1.func("다양한 함수형 인터페이스 타입을 처리하려면");
		}
		void highOrderFunc(Func2 fn2) {
			fn2.func("모조리 오버로딩해야한다.");
		}
	}

자바의 경우 동적 타입으로 호출할 수 없어 애초에 타입 불일치로 오버로딩을 해야한다.

 

위와 같은 이유로 별로 클래스로 정의했다. 

임시 방편으로 메서드 체이닝 방식으로 구현했다.

	/*
	 * pipe, go 구현을 시도하려 했지만, 근본적으로 자바는 함수가 개념이 없어
	 * 호출부를 단일로 추상화할 수 없다. apply, test ... 
	 * 따라서 하나의 클래스로 묶었다.
	 */
	static class Stream<T> {
		List<T> list;
		
		static <T> Stream<T> stream(List<T> list){
			Stream<T> stream = new Stream<T>();
			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);
		}
		
		List<T> toList(){
			return this.list;
		}
		
	}
		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)
				);
default = 131
stream = 131

전체 코드

package javabasic.stream;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
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((a,b)->a+b,null)
				);
		
	}
	/*
	 * 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));
	}
	
	
	/*
	 * pipe, go 구현을 시도하려 했지만, 근본적으로 자바는 함수가 개념이 없어
	 * 호출부를 단일로 추상화할 수 없다. apply, test ... 
	 * 따라서 하나의 클래스로 묶었다.
	 */
	static class Stream<T> {
		List<T> list;
		
		static <T> Stream<T> stream(List<T> list){
			Stream<T> stream = new Stream<T>();
			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);
		}
		
		List<T> toList(){
			return this.list;
		}
		
	}
	
	
}

reduce

	@SuppressWarnings("unchecked")
	/*
	 * 자바는 스트림 안에서 외부 변수를 참조하면 그 변수는 final 속성이 된다. 
	 * 따라서 갱신하면서 누산을 하지 못해, 간접적으로 값을 수정해야 한다.
	 */
	static <T> T reduce(List<T> list,BiFunction<T, T, T> reducer ,T memo) {
		//방법1
		T[] result = (T[])Array.newInstance(memo.getClass(), 1);
		result[0] = memo;
		//방법2
		HashMap<Class<T>, T> map = new HashMap<>();
		Class<T> typeToken = (Class<T>) memo.getClass();
		map.put(typeToken, memo);
		
		each(list, data->{
			result[0] = reducer.apply(result[0], data);
			map.compute(typeToken, (k,v)->reducer.apply(v, data));
		});
//		return result[0];
		return map.get(typeToken);
	}

자바에선 람다에서 참조하는 변수는 사실상 final이 된다. 그래서 일반적인 변수 하나에 값을 누산하면서 갱신하는 것이 불가능하다. 

따라서 일종의 편법으로 길이 1 인 배열을 생성해 간접적으로 값을 갱신하는 방법을 사용한다.

다른 방법으로 Map을 이용해 간접적으로 값을 갱신하는 방법도 있다.

 

reduce 리팩터링 1

	/*
	 * 자바는 스트림 안에서 외부 변수를 참조하면 그 변수는 final 속성이 된다. 
	 * 따라서 갱신하면서 누산을 하지 못해, 간접적으로 값을 수정해야 한다.
	 */
	static <T> T reduce(List<T> list,BiFunction<T, T, T> reducer ,T memo) {
		//방법1 Type safety: Unchecked cast from Object 
//		T[] result = (T[])Array.newInstance(memo.getClass(), 1);
		//방법2
		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());
	}

방법 1의 경우 지네릭 메서드로 타입이 런타임 호출 시에 결정된다. 따라서 명시적으로 고정된 배열을 생성하지 못한다. 리플렉션 API를 사용해서 배열을 생성한다. 이때 반환 타입은 Objecr로 지네릭 배열로 형변환이 필요하다. 따라서 방법 1을 배제했다.

 

reduce 오버로딩

	/*
	 * 자바는 스트림 안에서 외부 변수를 참조하면 그 변수는 final 속성이 된다. 
	 * 따라서 갱신하면서 누산을 하지 못해, 간접적으로 값을 수정해야 한다.
	 */
	static <T> T reduce(List<T> list, BinaryOperator<T> reducer ,T memo) {
		//간소화된 유효성 검사, 본질을 흐리지 않는 선에서 간략화
		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));
	}

memo가 없는 경우를 상정해 reduce메서드를 오버로딩 했다.

 

		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));
reduce = 15
reduce = 15
reduce = 1
reduce = 1

 

전체 코드

package javabasic.stream;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
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));
		
		
	}
	/*
	 * 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(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));
	}
	
}

+ Recent posts