Stream

Stream이란?

자바 8에서 추가한 스트림(Streams)은 람다를 활용할 수 있는 기술 중 하나. 자바 8 이전에는 배열 또는 컬렉션 인스턴스를 다루는 방법은 for 또는 foreach 문을 돌면서 요소 하나씩을 꺼내서 다루는 방법이었다. 간단한 경우라면 상관없지만 로직이 복잡해질수록 코드의 양이 많아져 여러 로직이 섞이게 되고, 메소드를 나눌 경우 순환참조가 발생할 수 있다.

스트림은 ‘데이터의 흐름’이다. 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있다. 또한 람다를 이용해서 코드의 양을 줄이고 간결하게 표현할 수 있다. 즉, 배열과 컬렉션을 함수형으로 처리할 수 있다.

컬렉션에서 스트림 생성하기

컬렉션의 최고 조상인 Collection에 stream()이 정의되어 있어서 Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 stream()으로 스트림을 생성할 수 있다.

List<Integer> list = Arrays.asList(1,2,3,4,5);
Stream<Integer> intStream = list.stream();  // list를 소스로 하는 컬렉션 생성

intStream.forEach(System.out::print); // 12345
intStream.forEach(System.out::print); // 에러. 스트림이 이미 닫혔다.

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

Stream<T> Stream.of(T... values)  // 가변 인자
Stream<T> Stream.of(T[])
Stream<T> Arrays.stream(T[])
Stream<T> Arrays.stream(T[] array, int startInclusive, int endExclusive)

// ex
Stream<String> strStream = Stream.of("a","b","c"); // 가변인자
Stream<String> strStream = Stream.of(new String[]{"a","b","c"});
Stream<String> strStream = Arrays.stream(new String[]{"a","b","c"});
Stream<String> strStream = Arrays.stream(new String[]{"a","b","c"}, 0, 3);

기본 자료형 스트림

일반 스트림을 사용하여 도시의 모든 인구를 구해보면 아래와 같다.

int sum = cities.stream()
        .map(City::getPopulation)
        .reduce(0, Integer::sum);

코드가 작동하는 데엔 문제는 없지만 내부적으로 박싱/언박싱을 수행하므로 성능이 크게 저하된다.
스트림은 IntStream , DoubleStream , LongStream 세 가지가 존재한다.
다만 일반 스트림에 비해 특별하게 추가된 기능은 없고 박싱/언박싱 관련 효율성의 장점을 갖는다.

변환

일반 스트림 → 기본 자료형 스트림

  • Stream::mapToInt → IntStream 반환
  • Stream::mapToDouble → DoubleStream 반환
  • Stream::mapToLong → LongStream 반환

기본 자료형 스트림 → 일반 스트림

  • IntStream::boxed
  • DoubleStream::boxed
  • LongStream::boxed

기본형 배열로부터 스트림 생성하기

IntStream IntStream.of(int...values)
IntStream IntStream.of(int[])
IntStream Arrays.stream(int[])
IntStream Arrays.stream(int[] array, int startInclusive, endExclusive)

난수 스트림(임의의 수)

IntStream intStream = new Random().ints();  // 무한스트림
intStream.limit(5).forEach(System.out::println);  // 5개의 요소만 출력

// 같은 결과
IntStream intStream = new Random().ints(5);
intStream.forEach(System.out::println);

// 범위 주기
IntStream intStream = new Random().ints(6,5,10);  // 6개 한정 5~9 무한스트림
intStream.forEach(System.out::println);  // 5개의 요소만 출력

// 범위 주기(같은 결과)
IntStream intStream = new Random().ints(5,10);  // 6개 한정 5~9 무한스트림
intStream.limit(6).forEach(System.out::println);  // 5개의 요소만 출력

특정 범위의 정수

IntStream intStream = IntStream.range(1, 5);      // 1,2,3,4
IntStream intStream = IntStream.rangeClosed(1, 5) // 1,2,3,4,5

iterate(), generate()

  • iterate : 이전 요소를 seed로 참조
  • generate : seed를 사용하지 않는다. ``` java Stream str = Stream.iterate(1, n -> n+2); str.limit(10).forEach(System.out::println);

Stream str2 = Stream.generate( ()-> 1+1 ); str2.limit(10).forEach(System.out::println); // 2,2,2,2,2....


### String배열을 int배열로 변환하기
``` java
IntStream intStream = Stream.of(stringArray).mapToInt(Integer::parseInt);
int[] intArray = Stream.of(stringArray).mapToInt(Integer::parseInt).toArray();

Integer배열을 String배열로 변환하기

String[] stringArray = Stream.of(arr).map(String::valueOf).toArray(String[]::new);

스트림 중간연산

Stream<T> skip(long n) // 스트림의 일부를 건너뛴다(n개를 건너뛴다)
Stream<T> limit(long maxSize) // 스트림의 일부를 잘라낸다
Stream<T> filter(Predicate<T> predicate) // 조건에 맞지 않는 요소 제거
Stream<T> distinct() // 중복 제거
Stream<T> sorted() // 스트림 요소의 기본 정렬(Comparable)로 정렬한다
Stream<T> sorted(Comparator<T> comparator) // 지정된 comparator로 정렬한다.
Stream<T> sequential() // 직렬스트림
Stream<T> parallel() // 병렬스트림

문자열 스트림 정렬 sorted

// ex 출력결과 : CCaaabccdd
strStream.sorted(); // 기본 정렬
strStream.sorted(Comparator.naturalOrder()); // 기본 정렬
strStream.sorted((s1, s2) -> s1.compareTo(s2)); // 람다식도 가능
strStream.sorted(String::compareTo); // 위 문장과 동일

// ex 출력결과 : ddccbaaaCC
strStream.sorted(Comparator.reverseOrder()); // 기본 정렬의 역순
strStream.sorted(Comparator.<String>naturalOrder().reversed());

// ex 출력결과 : aaabCCccdd
strStream.sorted(String.CASE_INSENSITIVE_ORDER); // 대소문자 구분안함

// ex 출력결과 : ddCCccbaaa
strStream.sorted(String.CASE_INSENSITIVE_ORDER.reversed());

// ex 출력결과 : bddCCccaaa
strStream.sorted(Comparator.comparing(String::length)); // 길이 순 정렬
strStream.sorted(Comparator.comparingInt(String::length)); // no오토박싱

// ex 출력결과 : aaaddCCccb
strStream.sorted(Comparator.comparing(String::length).reversed());
import java.util.*;
import java.util.stream.Stream;

class Student implements Comparable<Student>{
	int age, avg;
	String name;
	public Student(int age,int avg,String name){
		this.age = age;
		this.avg = avg;
		this.name = name;
	}

	public int getAge(){ return this.age; }
	public int getAvg(){ return this.avg; }

	@Override
	public int compareTo(Student s){
		return (s.name).compareTo(this.name);
	}
}

public class Main{
	public static void main(String[] args){
		List<Student> list = new ArrayList<>();
		for(int i = 0 ; i < 10 ; i++){
			list.add( new Student( (int)((Math.random()*10000)%10), (int)(Math.random()*100), "No."+i ) );
		}
		Stream<Student> sList = list.stream();
		sList.sorted(
			Comparator.comparing(Student::getAge) // 1. 나이 내림차순
			.thenComparing(Student::getAvg).reversed() // 2. 성적 내림차순
			.thenComparing(Comparator.naturalOrder()) // 3. 기본정렬
		).forEach( s -> System.out.println("age : "+s.age+", avg : "+s.avg+", name : "+s.name) );
	}
}

map, peek, flatMap

Stream<T> map(Function<T,R> mapper) // 스트림의 요소 변환
Stream<T> peek(Consumer<T> action) // 스트림의 요소를 소비하지 않고 엿보기(forEach)
Stream<T> flatMap() // 스트림 + 스트림 = 스트림으로 변환
import java.util.*;
import java.util.stream.Stream;

public class Main{
	public static void main(String[] args){
		String[] sArr = {"test.bmp"
						,"test.jpeg"
						,"test.gif"
						,"testgif"
						,"test.png"
						,"test.psd"
						,"testpsd"
						,"test.tiff"
						,"test.exif"
						,"test.exif"
						,"test.exif"
						,"testexif"
						,"test.pdf"
						,"test.raw"
						,"testraw"
						,"test.ai"
						,"test.eps"
						,"test.svg"
						,"testsvg"
						,"test.svgz"};

		Stream<String> str1 = Stream.of(sArr);

		str1
		.filter( s -> s.indexOf(".") > 1 ) // 확장자 X 제거
		.peek(System.out::println) // 확인
		.map(String::toUpperCase) // 대문자 변환
		.distinct() // 중복제거
		.forEach(System.out::println) // 출력
		;
	}
}
import java.util.*;
import java.util.stream.Stream;

public class Main{
	public static void main(String[] args){
		Stream<String[]> str = Stream.of(
			new String[]{"abc","def","jkl"},
			new String[]{"ABC","GHI","JKL"}
		);
		
		str.flatMap( s -> Stream.of(s) ) // 두 배열 합치기
		.map(String::toLowerCase) // 소문자 변환
		.distinct() // 중복 제거
		.sorted() // 정렬
		.forEach(System.out::println);
	}
}
import java.util.*;
import java.util.stream.Stream;

public class Main{
	public static void main(String[] args){
		String[] sArr = {
			"Belive or not It is true"
			, "Do or do not There is no try"
		};

		Stream<String> str = Stream.of(sArr);
		str.flatMap( s -> Stream.of(s.split(" +")) ) // 문장 스트림으로 쪼개기
		.distinct() // 중복제거
		.sorted() // 정렬
		.forEach(System.out::println);
	}
}

Optional이란?

Java8에서는 Optional<T> 클래스를 사용해 NPE를 방지할 수 있도록 도와준다. Optional<T>는 null이 올 수 있는 값을 감싸는 Wrapper 클래스로, 참조하더라도 NPE가 발생하지 않도록 도와준다. Optional 클래스는 아래와 같은 value에 값을 저장하기 때문에 값이 null이더라도 바로 NPE가 발생하지 않으며, 클래스이기 때문에 각종 메소드를 제공해준다.

// 초기화
String Str = "ab";
Optional<String> optVal = Optional.of(str);
Optional<String> optVal = Optional.of("abc");
Optional<String> optVal = Optional.of(null); // NullPointerException 발생(이렇게 사용하면 안됨)
Optional<String> optVal = Optional.ofNullable(null); // OK(사용 가능)

Optional<String> optVal = null; // 널로 초기화. 바람직하지 않음
OPtional<String> optVal = Optional.<String>empty(); // 빈 객체로 초기화

// 값 가져오기
Optional<String> optVal = Optional.of("abc");

String str1 = optVal.get(); // optVal에 저장된 값을 반환. null이면 예외발생(잘 안씀)
String str2 = optVal.orElse(""); // optVal에 저장된 값이 null일 때는, ""를 반환
String str3 = optVal.orElseGet(String::new); // 람다식 사용 가능() -> new String()
String str4 = optVal.orElseThrow(NullPointerException::new); // 널이면 예외 발생(예외 종류 지정가능)

// isPresent() - Optional 객체의 값이 Null이면 false, 아니면 true를 반환
if(Optional.ofNullable(str).isPresent()) { // if(str!==null)
	System.out.println(str);
}

// ifPresent(Consumer) - 널이 아닐때만작업 수행, 널이면아무 일도 안 함
Optional.ofNullable(str).ifPresent(System.out::println);

연습 코드

import java.util.*;
import java.util.stream.*;

class OptionalEx1 {
	public static void main(String[] args) {
		Optional<String>  optStr = Optional.of("abcde");
		Optional<Integer> optInt = optStr.map(String::length);
		System.out.println("optStr="+optStr.get());
		System.out.println("optInt="+optInt.get());

		int result1 = Optional.of("123")
							  .filter(x->x.length() >0)
							  .map(Integer::parseInt).get();

		int result2 = Optional.of("")
							  .filter(x->x.length() >0)
							  .map(Integer::parseInt).orElse(-1);

		System.out.println("result1="+result1);
		System.out.println("result2="+result2);

		Optional.of("456").map(Integer::parseInt)
					      .ifPresent(x->System.out.printf("result3=%d%n",x));

		OptionalInt optInt1  = OptionalInt.of(0);   // 0을 저장
		OptionalInt optInt2  = OptionalInt.empty(); // 빈 객체를 생성

		System.out.println(optInt1.isPresent());   // true
		System.out.println(optInt2.isPresent());   // false
		
		System.out.println(optInt1.getAsInt());   // 0
//		System.out.println(optInt2.getAsInt());   // NoSuchElementException
		System.out.println("optInt1 ="+optInt1);
		System.out.println("optInt2="+optInt2);
	    System.out.println("optInt1.equals(optInt2)?"+optInt1.equals(optInt2));
	
		Optional<String> opt  = Optional.ofNullable(null); // null을 저장
		Optional<String> opt2 = Optional.empty();          // 빈 객체를 생성
		System.out.println("opt ="+opt);
		System.out.println("opt2="+opt2);
		System.out.println("opt.equals(opt2)?"+opt.equals(opt2)); // true

		int result3 = optStrToInt(Optional.of("123"), 0);
		int result4 = optStrToInt(Optional.of(""), 0);

		System.out.println("result3="+result3);
		System.out.println("result4="+result4);
	}

	static int optStrToInt(Optional<String> optStr, int defaultValue) {
		try {
			return optStr.map(Integer::parseInt).get();
		} catch (Exception e){
			return defaultValue;
		}			
	}
}

OptionalInt, OptionalLong, OptionalDouble
기본형 값을 감싸는 래퍼클래스이다.(Optional< T >보다 성능이 좋아서 사용)

스트림 최종연산

void forEach(Consumer<? super T> action) // 병렬스트림인 경우 순서가 보장되지 않음
void forEachOrdered(Consumer<? super T> action) // 병렬스트림인 경우에도 순서가 보장됨
boolean allMatch(Predicate<T> p) // 모두 만족시키면 true
boolean anyMatch(Predicate<T> p) // 하나라도 만족시키면 true
boolean noneMatch(Predicate<T> p) // 모두 만족하지 않으면 true
Optional<T> findAny() // 아무거나 하나 반환(병렬/ 보통 중간연산으로 filter를 쓴다)
Optional<T> findFirst() // 첫 번째 요소 반환(직렬)

reduce()

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

Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
U reduce(U identity, BiFunction<U, T, U> accumulator, BinaryOperator<U> combiner)
속성
identity - 초기값
accumulator - 이전 연산결과와 스트림의 요소에 수행할 연산
combiner - 병렬처리된 결과를 합치는데 사용할 연산(병렬 스트림)
import java.util.*;
import java.util.stream.*;


public class Main{
	public static void main(String[] args){
		String[] strArr = new String[]{
			"Inheritance"
			, "Java"
			, "Lambda"
			, "stream"
			, "OptionalDouble"
			, "IntStream"
			, "count"
			, "sum"
		};

		// 1.
		// 병렬로 처리
		// 출력
		Stream.of(strArr).parallel().forEach(System.out::println);
		System.out.println();
		// 2.
		// 병렬처리
		// 순서있게 출력
		Stream.of(strArr).parallel().forEachOrdered(System.out::println);
		System.out.println();
		// 3.
		// 문자열 0인게 있는지 bool
		Boolean isLen_0 = Stream.of(strArr).anyMatch( s-> s.length() == 0);
		System.out.println(isLen_0);
		// 4.
		// 문자열 3인게 있는지 bool
		Boolean isLen_3 = Stream.of(strArr).anyMatch( s-> s.length() == 3);
		System.out.println(isLen_3);
		// 5.
		// s로 시작하는 단어 제외
		// 첫번째 출력
		Optional findFst = Stream.of(strArr).filter( s -> s.charAt(0) != 's' ).findFirst();
		System.out.println(findFst.orElse(""));
		// 6.
		// s로 시작하는 단어 제외
		// 병렬 출력
		// 아무거나 출력
		Optional findAny = Stream.of(strArr).parallel().filter( i -> i.charAt(0) != 's' ).findAny();
		System.out.println(findAny.orElse(""));
		// 7. 
		// 문자열 갯수 intstream 반환 4개 만들기
		IntStream intStream1 = Stream.of(strArr).mapToInt(String::length);
		IntStream intStream2 = Stream.of(strArr).mapToInt(String::length);
		IntStream intStream3 = Stream.of(strArr).mapToInt(String::length);
		IntStream intStream3_1 = Stream.of(strArr).mapToInt(String::length);
		IntStream intStream4 = Stream.of(strArr).mapToInt(String::length);
		// 8.
		// 카운트
		int count = intStream1.reduce(0, (a,b) -> a+1 );
		// 9.
		// 단어길이합산
		int sum = intStream2.reduce(0, (a,b) -> a+b );
		// 10.
		// Max
		int max = intStream3.reduce(0,(a,b) -> a>b?a:b );
		OptionalInt maxOp = intStream3_1.reduce(Integer::max);
		// 11.
		// Min
		int min = intStream4.reduce(Integer.MAX_VALUE,(a,b) -> a>b?b:a);
		// 12.
		// 8번부터 출력
		System.out.println(count);
		System.out.println(sum);
		System.out.println(max);
		System.out.println(maxOp.getAsInt());
		System.out.println(min);

	}
}

collect()와 Collectors

collect()는 Collector를 매개변수로 하는 스트림의 최종연산
reduce와 차이점 - collect는 그룹별 하기 용이하다

object collect(Collector collector) // Collector를 구현한 클래스의 객체를 매개변수로
object collect(Supplier supplier, BiConsumer accumultor, BiConsumer combiner) // 잘 안쓰임

Collector는 수집(collect)에 필요한 메서드를 정의해 놓은 인터페이스

public interface Collector<T, A, R>{ // T(요소)를 A에 누적한 다음, 결과를 R로 변환해서 반환
    Supplier<A> supplier(); // StringBuilder::new 누적할 곳
    BiConsumer<A, T> accumulator(); // (sb, s) -> sb.append(s) 누적방법
    BinaryOperator<A> combiner(); // (sb1, sb2) -> sb1.append(sb2) 결합방법(병렬)
    Function<A, R> finisher(); // sb -> sb.toString() 최종변환
    Set<Characteristics> characteristics(); // 컬렉터의 특성이 담긴 set을 반환
    ...
}

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

  • 변환 : mapping(), toList(), toSet() toMap(), toCollection()…
  • 통계 : counting(), summingInt(), averagingInt(), maxBy(), minBy(), summarizingInt()…
  • 문자열 결합 : joining()
  • 리듀싱 : reducing()
  • 그룹화와 분할 : groupingBy(), partitioningBy(), collectingAndThen()

변환 - toList(), toSet() toMap(), toCollection(), toArray();

List<String> names = stuStream.map(Student::getName).collect(Collectors.toList());
ArrayList<String> list = names.stream().collect(Collectors.toCollection(ArrayList::new));
Map<String, Person> map = personStream.collect(Collectors.toMap(p->p.getRegId(), p->p));

// toArray()
Student[] stuNames = studentStream.toArray(Student[]::new);
Object[] stuNames = studentStream.toArray();

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

long count = stuStream.count();
long count = stuStream.collect(counting());

long totalScore = stuStream.mapToInt(Student::getTotalScore).sum();
long totalScore = stuStream.collect(summingInt(Strudent::getTotalScore));

OptionalInt topScore = studentStream.mapToInt(Student::getTotalScore).max();
Optional<Student> topStudent = stuStream.max(Comparator.comparingInt(Student::getTotalScore));
Optional<Student> topStudent = stuStream.collect(maxBy(Comparator.comparingInt(Student::getTotalScore)));

스트림을 리듀싱 - reducing()

Collector reducing(BinaryOperator<T> op)
Collector reducing(T identity, BinaryOperator<T> op)
Collector reducing(U identity, Function<T,U> mapper,BinaryOperator<U> op)

// ex)
IntStream intStream = new Random().ints(1, 46).distinct().limit(6);

OptionalInt max = intStream.reduce(Integer::max);
Optional<Integer> max = intStream.boxed().collect(reducing(Integer::max));

long sum = intStream.reduce(0, (a,b) -> a+b);
long sum = intStream.boxed().collect(reducing(0, (a,b) -> a+b));

int grandTotal = stuStream.map(Student::getTotalScore).reduce(0, Integer::sum);
int grandTotal = stuStream.collect(reducing(0, Student::getTotalScore, Integer::sum));

문자열 스트림의 요소를 모두 연결 - joining()

String studentNames = stuStram.map(Student::getNAme).collect(joining());
String studentNames = stuStram.map(Student::getNAme).collect(joining(","));
String studentNames = stuStram.map(Student::getNAme).collect(joining(",","[","]"));
String studentInfo = stuStream.collect(joining(","));

스트림의 요소를 2분할 - partitioningBy()

Collector partitioningBy(Prediate predicate)
Collector partitioningBy(Predicate predicate, Collector downstream)
import java.util.*;
import java.util.stream.*;
import java.util.stream.Collectors.*;
import java.util.Comparator.*;

class Student {
	String name;
	boolean isMale; // 성별
	int hak;		// 학년
	int ban;		// 반
	int score;

	Student(String name, boolean isMale, int hak, int ban, int score) {
		this.name	= name;
		this.isMale	= isMale;
		this.hak	= hak;
		this.ban	= ban;
		this.score  = score;
	}

	String	getName()  { return name;}
	boolean isMale()   { return isMale;}
	int		getHak()   { return hak;}
	int		getBan()   { return ban;}
	int		getScore() { return score;}

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

	enum Level {
		HIGH, MID, LOW
	}
}


public class Main{
	public static void main(String[] args){
		Student[] stuArr = {
			new Student("나자바", true,  1, 1, 300),	
			new Student("김지미", false, 1, 1, 250),	
			new Student("김자바", true,  1, 1, 200),	
			new Student("이지미", false, 1, 2, 150),	
			new Student("남자바", true,  1, 2, 100),	
			new Student("안지미", false, 1, 2,  50),	
			new Student("황지미", false, 1, 3, 100),	
			new Student("강지미", false, 1, 3, 150),	
			new Student("이자바", true,  1, 3, 200),	

			new Student("나자바", true,  2, 1, 300),	
			new Student("김지미", false, 2, 1, 250),	
			new Student("김자바", true,  2, 1, 200),	
			new Student("이지미", false, 2, 2, 150),	
			new Student("남자바", true,  2, 2, 100),	
			new Student("안지미", false, 2, 2,  50),	
			new Student("황지미", false, 2, 3, 100),	
			new Student("강지미", false, 2, 3, 150),	
			new Student("이자바", true,  2, 3, 200)	
		};

		// 1. 단순분할(성별로 분할)
		Map<Boolean,List<Student>> ex1Map = Stream.of(stuArr)
			.collect(Collectors.partitioningBy(Student::isMale));
		ex1Map.get(true).stream().forEach(System.out::println);
		ex1Map.get(false).stream().forEach(System.out::println);
		System.out.println();

		// 2. 단순분할 + 통계(성별 학생수)
		Map<Boolean,Long> ex2Map = Stream.of(stuArr)
			.collect(Collectors.partitioningBy(s -> s.isMale() , Collectors.counting()));
		System.out.println(ex2Map.get(true));
		System.out.println(ex2Map.get(false));

		// 3. 단순분할 + 통게(설별 1등)
		Map<Boolean,Optional<Student>> ex3Map = Stream.of(stuArr)
			.collect(
				Collectors.partitioningBy(
					s-> s.isMale() 
					, Collectors.maxBy(
						Comparator.comparingInt(s -> s.getScore())
					)
				)
			);
		System.out.println(ex3Map.get(true));
		System.out.println(ex3Map.get(false));
		// 4. 다중분할(성별 불합격자, 100점 이하) 
		Map<Boolean,Map<Boolean,List<Student>>> ex4Map = Stream.of(stuArr)
			.collect(
				Collectors.partitioningBy(
					s->s.isMale(), 
						Collectors.partitioningBy( t-> t.getScore() > 100 )
				)
			);
	}
}

스트림의 요소를 다분할 - groupingBy()

Collector groupingBy(Function classifier)
Collector groupingBy(Function classifier, Collector downstream)
Collector groupingBy(Function classifier, Supplier mapFactory,Collector downstream)
import java.util.*;
import java.util.stream.*;
import java.util.stream.Collectors.*;
import java.util.Comparator.*;

class Student {
	String name;
	boolean isMale; // 성별
	int hak;		// 학년
	int ban;		// 반
	int score;

	Student(String name, boolean isMale, int hak, int ban, int score) { 
		this.name	= name;
		this.isMale	= isMale;
		this.hak	= hak;
		this.ban	= ban;
		this.score  = score;
	}

	String	getName()  { return name;}
	boolean isMale()   { return isMale;}
	int		getHak()   { return hak;}
	int		getBan()   { return ban;}
	int		getScore() { return score;}

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

	enum Level {
		HIGH, MID, LOW
	}
}


public class Main{
	public static void main(String[] args){
		Student[] stuArr = {
			new Student("나자바", true,  1, 1, 300),	
			new Student("김지미", false, 1, 1, 250),	
			new Student("김자바", true,  1, 1, 200),	
			new Student("이지미", false, 1, 2, 150),	
			new Student("남자바", true,  1, 2, 100),	
			new Student("안지미", false, 1, 2,  50),	
			new Student("황지미", false, 1, 3, 100),	
			new Student("강지미", false, 1, 3, 150),	
			new Student("이자바", true,  1, 3, 200),	

			new Student("나자바", true,  2, 1, 300),	
			new Student("김지미", false, 2, 1, 250),	
			new Student("김자바", true,  2, 1, 200),	
			new Student("이지미", false, 2, 2, 150),	
			new Student("남자바", true,  2, 2, 100),	
			new Student("안지미", false, 2, 2,  50),	
			new Student("황지미", false, 2, 3, 100),	
			new Student("강지미", false, 2, 3, 150),	
			new Student("이자바", true,  2, 3, 200)	
		};

		// 1. 단순그룹화(반별로 그룹화)
		Map<Integer,List<Student>> ex1 = Stream.of(stuArr)
			.collect(Collectors.groupingBy(s -> s.getBan()));
		for(int k : ex1.keySet()){
			for(Student s : ex1.get(k)){
				System.out.println(s.toString());
			}
		}

		// 2. 단순그룹화(성적별로 그룹화)
		Map<Student.Level,List<Student>> ex2 = Stream.of(stuArr)
			.collect(Collectors.groupingBy(
					s-> {
						if( s.getScore() >= 200 ) return Student.Level.HIGH;
						else if( s.getScore() >= 100 ) return Student.Level.MID;
						else return Student.Level.LOW;
					}
				)
			);
		for(Student.Level k : ex2.keySet()){
			for(Student s : ex2.get(k)){
				System.out.println(s.toString());
			}
		}
		// 3. 단순그룹화 + 통계(성적별 학생수)
		Map<Student.Level,Long> ex3 = Stream.of(stuArr)
			.collect(Collectors.groupingBy(
					s-> {
						if( s.getScore() >= 200 ) return Student.Level.HIGH;
						else if( s.getScore() >= 100 ) return Student.Level.MID;
						else return Student.Level.LOW;
					}, Collectors.counting()
				)
			);
		for(Student.Level k : ex3.keySet()){
			System.out.println(ex3.get(k));
		}
		// 4. 다중그룹화(학년별, 반별)
		Map<Integer,Map<Integer,List<Student>>> ex4 = Stream.of(stuArr).collect(
			Collectors.groupingBy( s-> s.getHak(), Collectors.groupingBy( s-> s.getBan()))
		);
		// 5. 다중그룹화 + 통계(학년별, 반별 1등)
		Map<Integer,Map<Integer,Student>> ex5 = Stream.of(stuArr).collect(
			Collectors.groupingBy( s-> s.getHak(),
				Collectors.groupingBy(Student::getBan,
					Collectors.collectingAndThen(
						Collectors.maxBy(Comparator.comparingInt(Student::getScore)), Optional::get
					)
				)
			)
		);
		// 6. 다중그룹화 + 통계(학년별, 반별 성적그룹)
		Map<String, Set<Student.Level>> stuByScoreGroup = Stream.of(stuArr)
			.collect(Collectors.groupingBy(s-> s.getHak() + "-" + s.getBan(),
					Collectors.mapping(s-> {
						 if(s.getScore() >= 200) return Student.Level.HIGH;
					else if(s.getScore() >= 100) return Student.Level.MID;
						 else                    return Student.Level.LOW;
					} , Collectors.toSet())
			));

		Set<String> keySet2 = stuByScoreGroup.keySet();

		for(String key : keySet2) {
			System.out.println("["+key+"]" + stuByScoreGroup.get(key));
		}
	}
}

© 2023 Lee. All rights reserved.