테스트 데이터로 실습

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

사용할 데이터

package javabasic.stream;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
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;
		}
	}
	
	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) {
	}
}
	public static void main(String[] args) {
		List<User> users = getUsers();
		
		System.out.println(
				filter(users,user->user.age>30)+"\n"
				+users
				);
		
		
	}
    static <T> List<T> filter(List<T> list, Predicate<T> predi) {
		//함수형 프로그래밍은 원본 데이터를 수정하지 않는다. 새로운 데이터를 리턴하여
		//부수효과를 극단적으로 배제한다.
		ArrayList<T> newList = new ArrayList<>();
		for(T data : list) {
			//자바는 근본적으로 함수단독으로 존재할 수 없다.
			//따라서 추상 메서드가 단 하나인 인터페이스를 사용해 메서드 형식으로 호출할 수 밖에 없다.
			if(predi.test(data))
				newList.add(data);
		}
		return newList;
	}
filter = [[id=10, name=ID, age=36]
, [id=20, name=BJ, age=32]
, [id=30, name=JM, age=32]
, [id=70, name=JI, age=31]
]
[[id=10, name=ID, age=36]
, [id=20, name=BJ, age=32]
, [id=30, name=JM, age=32]
, [id=40, name=PJ, age=27]
, [id=50, name=HA, age=25]
, [id=60, name=JE, age=26]
, [id=70, name=JI, age=31]
, [id=80, name=MP, age=23]
]

원본에 영향을 가지 않도록 처리했다.

Map

	/*
	 * map은 처리 결과가 입력 결과와 타입은 같거나 다르다.
	 * 길이는 항상 같다.
	 */
	static <T,R> List<R> map(List<T> list, Function<T, R> mapper){
		ArrayList<R> newList = new ArrayList<>();
		for(T data : list) 
			newList.add(mapper.apply(data));
		return newList;
	}
		System.out.println("map = "+
				map(users, user->user.id)
				);
map = [10, 20, 30, 40, 50, 60, 70, 80]

each 만든 후 filter, map에 적용

전체 코드

package javabasic.stream;

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

 

 

[무료] 자바스크립트로 알아보는 함수형 프로그래밍 (ES5) - 인프런 | 강의

마플(http://www.marpple.com)의 CTO 유인동님이 알려주는 함수형 프로그래밍에 대한 강좌 입니다. 함수형 프로그래밍으로 라이브러리를 직접 만들어가며 함수형 프로그래밍의 패러다임과 코딩의 즐거

www.inflearn.com

/* 요소 리스트 값을 하나씩 꺼내어 준다.*/
function each(list, iter){
    const _keys = keys(list);
    for(let i=0;i<_keys.length;i++){
        iter(list[_keys[i]], _keys[i]);//값 뿐만이 아니라 키도 인자로 넘기도록 수정
    } 
    return list;
}
/* 수집하기 대표 요소를 변환시키는 함수*/
function map(list , mapper){
    const result = [];
    each(list, (val,key) => result.push(mapper(val,key)));//키도 넘김
    return result;
}

pairs

key:value 쌍으로하는 배열을 반환하는 함수

/*key:value 쌍으로하는 배열을 반환하는 함수*/
function pairs (list){
    return map(list, (val, key)=>[key,val]);
}
console.log(
    _.pairs(users[0]),
)

_.go(
    users,
    _.map(u=>_.pairs(u)),
    console.log
)

마무리

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
//순수함수 예시
function add(a,b){
    return a+b;
}
console.log(add(12,34));
console.log(add(12,34));

//부수효과 예시
let sideEffect1 = 10;
function add2(a,b){
    return add(a,b)+ sideEffect1;
}
console.log(add2(10,20));
sideEffect1 = 20;
console.log(add2(10,20));

let sideEffect2 = 10;
function add3(a,b){
    sideEffect1 = a; // 외부에 영향을 준다
    sideEffect2 = b;
    return add(a,b);
}
console.log(add3(10,20));
console.log(add3(10,20));

//새로운 값을 만든다는 것은 Call By Reference가 대상이다.
const arr = [1,2,3,4,5];
function multiplyArr(arr){
    //새로운 값
    const result = arr.slice();
    for(const idx in result){
        result[idx] = result[idx] * 2;
    }
    return result;
}
console.log(multiplyArr(arr));
console.log(arr);

console.clear();


//일급함수
const get10 = () => 10; //함수를 변수에 담는다.

//함수를 인자로 받는 함수, 함수를 변수에 담았음
const add30 = function(fn){
    return 30 + fn();
}
//인자로 함수를 전달했다.
console.log(add30(get10));


//함수는 단독으로 존재할 수 있다.
function hello(name){
    console.log("hello "+name);
}

//메서드는 클래스 속에 존재한다.
class Hello{
    hello(name){
        console.log("hello "+name);
    }
}
hello("홍길동");
new Hello().hello("홍길동");
console.clear();
</script>
<script>
const users = [
  { id: 10, name: 'ID', age: 36 },
  { id: 20, name: 'BJ', age: 32 },
  { id: 30, name: 'JM', age: 32 },
  { id: 40, name: 'PJ', age: 27 },
  { id: 50, name: 'HA', age: 25 },
  { id: 60, name: 'JE', age: 26 },
  { id: 70, name: 'JI', age: 31 },
  { id: 80, name: 'MP', age: 23 }
];

//함수를 담은 obj
const _ = (()=>{
    return {
        filter:curryr(filter) ,
        reject:curryr(reject) ,
        negate:negate ,
        compact:compact ,
        find : find,
        find_index : find_index,
        some : some,
        every : every,
        contains : contains,
        map:curryr(map) ,
        get:curryr(get) ,
        values:values,
        pluck:pluck,
        pairs:pairs,
        identity:identity,
        each: each,
        curry:curry,
        curryr:curryr,
        reduce: curryr(reduce),
        min: min,
        max: max,
        min_by: curryr(min_by),
        max_by: curryr(max_by),
        group_by: curryr(group_by),
        count_by: curryr(count_by),
        rest: rest,
        pipe: pipe,
        go: go,
    };

/*찾아내기 대표 함수 find
filter와 유사하나 predi에 걸러진 처음으로 만나는 요소를 반환한다.*/
function find(list, predi){
    const _keys = keys(list);
    for(let i=0;i<_keys.length;i++){
        const val = list[_keys[i]];
        if(predi(val)) return val;
    }
    return undefined; //생략 가능, 자동으로 undefined 리턴될 것
}
/*predi에 걸러진 처음으로 만나는 요소의 인덱스를 리턴한다.*/
function find_index(list, predi){
    const _keys = keys(list);
    for(let i=0;i<_keys.length;i++){
        if(predi(list[_keys[i]])) return i;
    }
    return -1; //자바스크립트 표준에 맞춤
}
/* find_index 특화 함수 주어진 조건에 값이 하나라도 존재하는지 검사한다.*/
function some(list, predi){
    //하나라도 해당하면 -1을 리턴할 수 없다.
    return find_index(list, predi||identity) !== -1;
}
/* find_index 특화 함수 주어진 조건에 값이 전부 일치하는지 검사한다.*/
function every(list, predi){
    //주어진 조건 반대에 해당하는 값을 못찾으면, 전부 해당하는 것
    return find_index(list, negate(predi||identity)) === -1;
}

function every2(list, predi){
    return find_index(list, function(val){
        return !predi(val);
    })
}
function every3(list, predi){
    return find_index(list, (val)=> !predi(val) )
}
/*주어진 값이 존재하는지 검사하는 함수*/
function contains(list, data){
    return find_index(list, val => val === data) !== -1;
}

/* 요소를 걸러내는 함수, predi라는 보조함수를 인자로 받는다. */
function filter(list, predi){
    //함수형 스타일은 기존 요소를 손상시키지 않고 항상 새로운 값을 리턴한다.
    const result = [];
    //보조함수를 인자로 받아
    each(list, val =>{
        if(predi(val)) result.push(val);
    });
    return result;
}

/*단순히 filter에 반대로 행동하는 함수
(== 주어진 조건에 해당하는 값을 반환하는 함수*/
function reject(list, predi){
    return filter(list, negate(predi) );
}

/*결과를 반대로 바꾸는 함수*/
function negate(predi){
    return val => !predi(val);
}

/*참으로 판단되는 값을 걸러내는 함수*/
function compact(list){
    return filter(list, identity);
}


/* 수집하기 대표 요소를 변환시키는 함수*/
function map(list , mapper){
    const result = [];
    each(list, (val,key) => result.push(mapper(val,key)));
    return result;
}
/*key:value 쌍으로하는 배열을 반환하는 함수*/
function pairs (list){
    return map(list, (val, key)=>[key,val]);
}

/*요소 값을 반환하는 함수*/
function values(list){
    return map(list,identity);
}
/*자기자신을 그대로 반환하는 함수*/
function identity(arg){
    return arg;
}

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



/* 요소 리스트 값을 하나씩 꺼내어 준다.*/
function each(list, iter){
    const _keys = keys(list);
    for(let i=0;i<_keys.length;i++){
        iter(list[_keys[i]], _keys[i]);
    }
    return list;
}
/*안전하게 값 가져오기*/
function get (obj,key){
    return obj === null ? undefined : obj[key];
}
/* 객체의 keys를 안전하게 리턴하는 함수*/
function keys(obj){
    return is_object(obj) ? Object.keys(obj) : [];
}
/* 객체인지 안전하게 확인하는 함수 */
function is_object(obj){
    return typeof obj === "object" && !!obj;
}


/* 커링 함수 오직 함수만을 인자로 받아 함수를 리턴함, 평가 시점을 조율*/
function curry(fn){
    return function(a,b){
        return arguments.length === 2 ? fn(a,b) : function (b){
            return fn(a,b);
        }
    }
}
/* 단순히 인자 순서만 변경, 화살표 함수로만 표현 */
function curryr(fn){
    return function(a,b,c){
        if(arguments.length === 2) return fn(a,b);
        else if (arguments.length === 3) return fn(a,b,c);
        else return b => fn(b,a);
    };
}

/*요소를 하나로 접어가는 함수
list 요소, iter 보조함수, memo 초기값*/
function reduce(list, iter, memo){
    if(arguments.length===2){
        memo = list[0];
        list = rest(list);
    }
    each(list, val =>{
        //memo에 계속 누적하여 적용한다.
        memo = iter(memo, val);
    });
    return memo;
}

function min(list){
    return reduce(list, (a, b)=> a > b ? b : a);
}
function max(list){
    return reduce(list, (a, b)=> a < b ? b : a);
}
/*보조 함수를 인자로 받아, 그 반환값을 기준으로 평가한다.*/
function min_by(list,iter){
    return reduce(list, (a, b)=> iter(a) > iter(b) ? b : a);
}
function max_by(list,iter){
    return reduce(list, (a, b)=> iter(a) < iter(b) ? b : a);
}

/*주어진 조건으로 그룹핑한 결과를 반환하는 함수*/
function group_by(list,iter){
    return reduce(list, (group, val) => {
        //그룹화하려면 그룹 기준과 그룹화될 리스트 공간이 필요하다.
        //그룹 기준에 충족하는 첫 그룹화라면 빈 배열을 넣어준다.
        (group[iter(val)] = group[iter(val)] || [] ).push(val);
        return group;
    } ,{});
}
/*주어진 조건으로 그룹핑한 결과를 개수로 반환하는 함수*/
function count_by(list,iter){
    return reduce(list, (group, val) => {
        group[iter(val)] = group[iter(val)]+1 || 1;
        return group;
    } ,{});
}


/* 배열 첫 번째 요소를 잘라 나머지 배열을 리턴*/
function rest(list, num){
    return Array.prototype.slice.call(list,num||1);
}
/*리듀스 특화 함수, 함수만을 인자로 받는 함수*/
function pipe(){
    const fns = arguments;
    return function(val){
        return reduce(fns,function(val,fn){
            return fn(val);
        }, val );
    };
}

/*pipe 즉시 실행 버전*/
function go(data){
    //첫번째 데이터만 제외하면 함수 배열이다.
    const fns = rest(arguments);
    //함수 배열이기 때문에 arg1, arg2 ... argN 풀어주려면 apply를 사용한ㄷ.
    return pipe.apply(null,fns)(data);
}

})();

//나이 25세 이상 사용자 걸러내기
//절차지향 코드
const tmp = [];
for(const user of users){
    if(user.age>=25) tmp.push(user);
}
console.log(tmp);
//함수형 코드
console.log(
    _.filter(users, u=>u.age>=25)
);

console.log(
    _.filter(users, u=>u.age-(u.age%10) === 20), //20대만
    _.filter(users, u=>u.name.startsWith("J")) //J로 시작하는 user
);

//map 사용
console.log(
    _.map(users, u=>u.age) ,
    _.map(users, u=>u.name) ,
    _.map(users, u=>u.id) ,
);
_.each(users,u=>console.log(u));
//섞어쓰기, 30세 이상, user 이름
console.log(
    _.map(_.filter(users, u=> u.age >=30), u=>u.name)
    );
console.clear();

//중복이름으로 구분자 _
const _add = _.curryr((a,b)=>a+b);
const _add10 = _add(10);
console.log(_add);
console.log(_add10);
console.log(_add10(20));
console.log(_add(10,20));

const _sub = _.curryr((a,b)=>a-b);
const _sub10 = _sub(10);
console.log( _sub10(20) );

console.clear();

console.log(_.filter);
console.log(_.map);

const upperAge30 = _.filter(u=>u.age>=30);
const userName = _.map(u=>u.name);
console.log(upperAge30);
console.log(userName);

console.log(upperAge30(users));
console.log(userName(users));
console.log(userName(upperAge30(users)));

console.clear();
/*이전 reduce 실습 내용*/
console.log(_.reduce([1,2,3,4,5],(sum,val)=>sum+val,0));
console.log(_.reduce([1,2,3,4,5],(sum,val)=>sum+val,10));
console.log(_.reduce([1,2,3,4,5],(sum,val)=>sum+val,-10));

console.log(_.reduce([1,2,3,4,5],(sum,val)=>sum+val ));

//30세 이상 사용자 age 합
console.log(_.reduce(_.map(upperAge30(users),u=>u.age),(sum,val)=>sum+val ));
console.clear();
//30세 이상 사용자 age 합, pipe 사용
const tmpFilter = _.filter(u=>u.age>=30);
const tmpMap = _.map(u=>u.age);
const tmpReduce = _.reduce((sum,val)=>sum+val);

console.log(tmpFilter);
console.log(tmpMap);
console.log(tmpReduce);
console.clear();

_.pipe(
    tmpFilter,
    tmpMap,
    tmpReduce,
    console.log
)(users);

_.go(users,
    tmpFilter,
    tmpMap,
    tmpReduce,
    console.log
);


_.each([1,2,3,4], n=> console.log(n))
_.each(null, n=> console.log(n))
console.log( _.filter(null, n=> true));
console.log( _.map(null, n=> n));

console.clear();

_.each({a:1,b:2,c:3}, a=>console.log(a));
_.each([1,2,3], a=>console.log(a));

//Object.keys 동작방식
console.log(Object.keys(["a","b","c"]).join("-")  );
console.log(Object.keys("가나다라").join("-")   );
console.log(Object.keys({:"바",:"바",:"나"}).join("-")   );
//Object.values 동일
console.log(Object.values(["a","b","c"]).join("-")  );
console.log(Object.values("가나다라").join("-")   );
console.log(Object.values({:"바",:"바",:"나"}).join("-")   );

//Object.keys 는 for...in 과 유사
function forIn(list){
    const result = [];
    for(const arg in list) result.push(arg);
    return result.join("-");
}
//Object.values 는 for...of 과 유사
function forOf(list){
    const result = [];
    for(const arg of list) result.push(arg);
    return result.join("-");
}
console.log(forIn(["a","b","c"]) );
console.log(forIn("가나다라") );
console.log(forIn({:"바",:"바",:"나"}));
console.log(forOf(["a","b","c"]) );
console.log(forOf("가나다라") );
// console.log(forOf({고:"바",구:"바",마:"나"}));//불가능


//단일 접근 원칙
class Data{
    constructor(name, age){
        this._name = name;
        this._age =age;
    }
    get name(){
        console.log("name 게터 호출");
        return this._name;
    }
    get age(){
        console.log("age 게터 호출");
        return this._age;
    }
    set name(arg){
        console.log("name 세터 호출");
        this._name = arg;
    }
    set age(arg){
        console.log("age 세터 호출");
        this._age = arg;
    }
}
console.clear();
const data1 = new Data("홍길동", 50);
console.log(data1.name); //변수처럼 다룬다
data1.name = "임꺽정"
console.log(data1.name); //변수처럼 다룬다

console.clear();

console.log(
    _.values(users),
    _.values(users[0]),
);

console.log(
    _.pluck(users, "age"),
    _.pluck(users, "name"),
);

console.clear();


const filterEx = user=>user.age>30;

function ex(user){
    return filterEx;
}

console.log(
    _.filter(users, user=>user.age>30),
    _.reject(users, user=>user.age>30),
)
const testArr = [undefined, Infinity, null, NaN, 0, 1, 'true','false',true,false];
console.log(
    _.filter(_.identity)(testArr),
    _.filter(_.negate(_.identity))(testArr),
);
const compactEx = _.filter(_.identity);
console.log(
    compactEx(testArr),//이것과 완전히 같다.
    _.compact(testArr)
)
/*
compactEx
b => fn(b,a)   b는 _.filter  , a는 _.identity  , b로 testArr
_.compact
function compact(list){
    return filter(list, identity);
}
화살표 함수로 바꾸면
list => filter(list, identity)
기호만 바꾸면
b => fn(b,a)
*/

console.clear();

console.log(
    _.find(users, user => user.age >= 30),
    _.filter(users, user => user.age >= 30),
);
console.log(
    _.find_index(users, user => user.age <= 25),
    _.find_index(users, user => user.age > 40),
);
console.clear();

console.log(
    _.some(users, user=>user.age>30),
    _.every(users, user=>user.age>30),
    _.every(users, user=>user.age>10),
);
console.log(
    _.some(users),
    _.every(users),
    _.some(testArr),
    _.every(testArr),
);

console.log(
    _.contains(users, users[2] ),
    _.contains(users[2], "JM" ),
    _.contains(users[2], "홍길동" ),
);
console.clear();
const numArr = [-10,10,20,30,-20,-50,66];

console.log(_.min(numArr));
console.log(_.max(numArr));
console.log(_.min_by(users, user=>user.age));
console.log(_.max_by(users, _.get("age")));

console.clear();

_.go(
    users,
    _.group_by(user=>user.age-(user.age%10)),
    console.log
);
console.log(_.group_by(users,u=>u.age-(u.age%10)));
_.go(
    users,
    _.group_by(user=>user.age%2),
    console.log
);
_.go(
    users,
    _.count_by(user=>user.age-(user.age%10)),
    console.log
);
console.log(_.count_by(users,u=>u.age-(u.age%10)));
_.go(
    users,
    _.count_by(user=>user.age%2),
    console.log
);
console.clear();
console.log(
    _.pairs(users[0]),
)

_.go(
    users,
    _.map(u=>_.pairs(u)),
    console.log
)


</script>


</body>
</html>

function.html
0.01MB

 

 

[무료] 자바스크립트로 알아보는 함수형 프로그래밍 (ES5) - 인프런 | 강의

마플(http://www.marpple.com)의 CTO 유인동님이 알려주는 함수형 프로그래밍에 대한 강좌 입니다. 함수형 프로그래밍으로 라이브러리를 직접 만들어가며 함수형 프로그래밍의 패러다임과 코딩의 즐거

www.inflearn.com

min, max

제일 작은 값을 반환

제일 큰 값을 반환

function min(list){
    return reduce(list, (a, b)=> a > b ? b : a);
}
function max(list){
    return reduce(list, (a, b)=> a < b ? b : a);
}
const numArr = [-10,10,20,30,-20,-50,66];

console.log(_.min(numArr));
console.log(_.max(numArr));

min, max 는 숫자인 값만 처리가 가능하다. 제한적으로 사용만 가능해 추상화 수준이 낮다. 

min_by, max_by

보조함수를 인자로 받아 주어진 값에 적용한다. 적용하여 반환된 값을 기반으로 동작한다.

/*보조 함수를 인자로 받아, 그 반환값을 기준으로 평가한다.*/
function min_by(list,iter){
    return reduce(list, (a, b)=> iter(a) > iter(b) ? b : a);
}
function max_by(list,iter){
    return reduce(list, (a, b)=> iter(a) < iter(b) ? b : a);
}
console.log(_.min(numArr));
console.log(_.max(numArr));
console.log(_.min_by(users, user=>user.age));
console.log(_.max_by(users, _.get("age")));

적절한 보조함수만 주어진다면 어떠한 돌림직한 값도 처리할 수 있다.

 

group_by

주어진 조건으로 그룹핑한 결과를 반환하는 함수

/*주어진 조건으로 그룹핑한 결과를 반환하는 함수*/
function group_by(list,iter){
    return reduce(list, (group, val) => {
        //그룹화하려면 그룹 기준과 그룹화될 리스트 공간이 필요하다.
        //그룹 기준에 충족하는 첫 그룹화라면 빈 배열을 넣어준다.
        (group[iter(val)] = group[iter(val)] || [] ).push(val);
        return group;
    } ,{});
}
_.go(
    users,
    _.group_by(user=>user.age-(user.age%10)),
    console.log
);
console.log(_.group_by(users,u=>u.age-(u.age%10)));

나이 대 별 그룹

count_by

group_by의 개수 버전

/*주어진 조건으로 그룹핑한 결과를 개수로 반환하는 함수*/
function count_by(list,iter){
    return reduce(list, (group, val) => {
        group[iter(val)] = group[iter(val)]+1 || 1;
        return group;
    } ,{});
}
_.go(
    users,
    _.count_by(user=>user.age-(user.age%10)),
    console.log
);
console.log(_.count_by(users,u=>u.age-(u.age%10)));
_.go(
    users,
    _.count_by(user=>user.age%2),
    console.log
);

function.html
0.01MB

 

+ Recent posts