람다식(Lambda Expression)
함수(메서드)를 간단한 '식(expression)'으로 표현하는 방법이다.

익명 함수(이름이 없는 함수, anonymous function)이다.
람다식으로 인해 메서드를 변수처럼 다루는 것이 가능하다.
람다식 작성하기
메서드의 이름과 반환타입을 제거하고 '->'를 블록{} 앞에 추가한다.

반환값이 있는 경우, 식이나 값만 적고 return문 생략 가능 (끝에 ';'안 붙임)
매개변수의 타입이 추론 가능하면 생략 가능 (대부분의 경우 생략가능)
주의사항
매개변수가 하나인 경우, 괄호() 생략 가능 (타입이 없을 때만)
블록 안의 문장이 하나뿐 일 때, 괄호{} 생략 가능 (끝에 ';' 안 붙임)

단, 하나뿐인 문장이 return문일 경우 괄호{} 생략 불가

람다식은 익명 함수? 익명 객체!
람다식은 익명 함수가 아니라 익명 객체이다.

객체의 메서드를 호출하려면 참조 변수가 필요한 것처럼, 람다식(익명 객체)을 다루기 위한 참조변수가 필요하다.
타입 f = (int a, int b) -> a > b ? a : b; //참조변수의 타입을 뭘로해야 할까?
람다식(익명 객체)의 참조변수는 함수형 인터페이스로 다루어야 한다.
함수형 인터페이스(Functional Interface)
인터페이스를 통해 람다식을 다루기로 결정되었으며, 람다식을 다루기 위한 인터페이스를 ‘함수형 인터페이스’라고 부르기로 했다.
함수형 인터페이스에는 오직 하나의 추상메서드만 정의되어 있어야 한다는 제약이 있다. 그래야 람다식과 인터페이스의 메서드가 1:1로 연결될 수 있기 때문이다.
//@Functional Interface를 붙이면 컴파일러가 함수형 인터페이스를 올바르게 선언했는 지 확인한다.
@FunctionalInterface
interface MyFunction { // 함수형 인터페이스 MyFunction을 정의
public abstract int max(int a, int b);
}
함수형 인터페이스 타입의 참조변수로 람다식을 참조할 수 있다.
단, 함수형 인터페이스의 메서드와 람다식의 매개변수 개수와 반환타입이 일치해야 한다.
함수형 인터페이스 타입의 매개변수, 반환 타입
람다식을 참조변수로 다룰 수 있다는 것은 메서드를 통해 람다식을 주고받을 수 있다는 것을 의미한다. 즉, 변수처럼 메서드를 주고받는 것이 가능해진다. 사실상 메서드가 아니라 익명 객체를 주고받는 것이다.
함수형 인터페이스 타입의 매개변수
함수형 인터페이스 타입의 반환 타입
함수형 인터페이스 타입의 매개변수, 반환 타입 예제
@FunctionalInterface
interface MyFunction {
void run(); // public abstract void run();
}
class Ex14_1 {
static void execute(MyFunction f) { // 매개변수의 타입이 MyFunction인 메서드
f.run();
}
static MyFunction getMyFunction() { // 반환 타입이 MyFunction인 메서드
MyFunction f = () -> System.out.println("f3.run()");
return f;
}
public static void main(String[] args) {
// 람다식으로 MyFunction의 run()을 구현
MyFunction f1 = ()-> System.out.println("f1.run()");
MyFunction f2 = new MyFunction() { // 익명클래스로 run()을 구현
public void run() { // public을 반드시 붙여야 함
System.out.println("f2.run()");
}
};
MyFunction f3 = getMyFunction();
f1.run();
f2.run();
f3.run();
execute(f1);
execute( ()-> System.out.println("run()") );
}
}
/* 출력 결과
f1.run()
f2.run()
f3.run()
f1.run()
run()
*/
java.util.function패키지
java.util.function패키지에 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해놓았다.
매개변수가 두 개인 함수형 인터페이스
두 개 이상의 매개변수를 갖는 함수형 인터페이스가 필요하다면 아래와 같이 직접 만들어서 써야한다.
@FunctionalInterface
interface TriFunction<T,U,V,R>{
R apply(T t, U u, V v);
}
매개변수의 타입과 반환타입이 일치하는 함수형 인터페이스
java.util.function패키지 예제
import java.util.function.*;
import java.util.*;
class Ex14_2 {
public static void main(String[] args) {
Supplier<Integer> s = ()-> (int)(Math.random()*100)+1;
Consumer<Integer> c = i -> System.out.print(i+", ");
Predicate<Integer> p = i -> i%2==0;
Function<Integer, Integer> f = i -> i/10*10; // 1의 일의 자리를 없앤다.
List<Integer> list = new ArrayList<>();
makeRandomList(s, list);
System.out.println(list);
printEvenNum(p, c, list);
List<Integer> newList = doSomething(f, list);
System.out.println(newList);
}
static <T> List<T> doSomething(Function<T, T> f, List<T> list) {
List<T> newList = new ArrayList<T>(list.size());
for(T i : list) {
newList.add(f.apply(i));
}
return newList;
}
static <T> void printEvenNum(Predicate<T> p, Consumer<T> c, List<T> list) {
System.out.print("[");
for(T i : list) {
if(p.test(i))
c.accept(i);
}
System.out.println("]");
}
static <T> void makeRandomList(Supplier<T> s, List<T> list) {
for(int i=0;i<10;i++) {
list.add(s.get());
}
}
}
/* 출력 결과
[14, 21, 38, 28, 76, 62, 30, 30, 66, 59]
[14, 38, 28, 76, 62, 30, 30, 66, ]
[10, 20, 30, 20, 70, 60, 30, 30, 60, 50]
*/
Function의 합성
andThen() : Function 타입의 두 람다식을 하나로 합성
compose(): Function 타입의 두 람다식을 하나로 합성
andThen()과 다른 점은 andThen()은 f+g이고, compose()는 g+f로 실행 순서만 다르다.
Predicate의 결합
and(), or(), negate()로 두 Predicate를 하나로 결합 (default 메서드)
등가비교를 위한 Predicate의 작성에는 isEqual()를 사용 (static 메서드)
컬렉션 프레임웍과 함수형 인터페이스
함수형 인터페이스를 사용하는 컬렉션 프레임웍의 메서드
컬렉션 프레임웍과 함수형 인터페이스 예제
import java.util.*;
class Ex14_4 {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
for(int i=0;i<10;i++)
list.add(i);
// list의 모든 요소를 출력
list.forEach(i->System.out.print(i+","));
System.out.println();
// list에서 2 또는 3의 배수를 제거한다.
list.removeIf(x-> x%2==0 || x%3==0);
System.out.println(list);
list.replaceAll(i->i*10); // list의 각 요소에 10을 곱한다.
System.out.println(list);
Map<String, String> map = new HashMap<>();
map.put("1", "1");
map.put("2", "2");
map.put("3", "3");
map.put("4", "4");
// map의 모든 요소를 {k,v}의 형식으로 출력한다.
map.forEach((k,v)-> System.out.print("{"+k+","+v+"},"));
System.out.println();
}
}
/*출력 결과
0,1,2,3,4,5,6,7,8,9,
[1, 5, 7]
[10, 50, 70]
{1,1},{2,2},{3,3},{4,4},
*/
메서드 참조
하나의 메서드만 호출하는 람다식은 ‘메서드 참조(method reference)’라는 방법으로 간단히 할 수 있다.
하나의 메서드만 호출하는 람다식은 ‘클래스이름::메서드이름’ 또는 ‘참조변수::메서드이름’으로 바꿀 수 있다.
static 메서드 참조
인스턴스 메서드 참조
특정 객체의 인스턴스 메서드 참조
new연산자(생성자, 배열)와 메서드 참조
스트림(stream)
스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓았다. 데이터 소스를 추상화하였다는 것은, 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 되었다는 것과 코드의 재사용성이 높아진다는 것을 의미한다.
스트림을 이용하면 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있다.
String[] strArr = {"aaa", "ddd", "ccc"};
List<String> strList = Arrays.asList(strArr);
//스트림을 생성
Stream<String> strStream1 = strList.stream();
Stream<String> strStream2 = Arrays.stream(strArr);
//스트림을 사용하면 정렬하고 출력하는 방식이 동일하다.
strStream1.sorted().forEach(System.out::println);
strStream2.sorted().forEach(System.out::println);
스트림의 특징
스트림은 데이터 소스를 변경하지 않는다.
스트림은 데이터 소스로부터 데이터를 읽기만할 뿐, 데이터 소스를 변경하지 않는다.
필요하다면, 정렬된 결과를 컬렉션이나 배열에 담아서 반환할 수 있다.
//정렬된 결과를 새로운 List에 담아서 반환한다.
List<String> sortedList = strStream2.sorted().collect(Collectors.toList());
스트림은 일회용이다.
스트림은 Iterator처럼 일회용이다. Iterator로 컬렉션의 요소를 모두 읽고 나면 다시 사용할 수 없는 것처럼, 스트림도 한번 사용하면 닫혀서 다시 사용할 수 없다. 필요하다면 스트림을 다시 생성해야한다.
strStream1.sorted().forEach(System.out::println);
int numOfStr = strStream1.count(); //에러. 스트림이 이미 닫혔음.
스트림은 작업을 내부 반복으로 처리한다.
내부 반복이라는 것은 반복문을 메서드의 내부에 숨겼다는 것을 의미한다.
forEach()는 스트림에 정의된 메서드 중의 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용한다.
for(String str : strList)
System.out.println(str);
stream.forEach(System.out::println);
//메서드 참조 System.out::println를 람다식으로 표현하면
//(str) -> System.out.println(str)과 같다.
//forEach()는 메서드 안에 for문을 넣어버린 것이다.
//수행할 작업은 매개변수로 받는다.
void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action); //매개변수의 널 체크
for(T t : src) { //내부 반복
action.accept(T);
}
}
지연된 연산
최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다.
스트림에 대해 distict()나 sort()같은 중간 연산을 호출해도 즉각적인 연산이 수행되지 않는다.
중간 연산을 호출하는 것은 단지 어떤 작업이 수행되어야하는지를 지정해주는 것일 뿐이다.
최종 연산이 수행되어야 비로소 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모된다.
Stream<Integer>와 IntStream
오토박싱& 언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림, IntStream, LongStream, DoubleStream이 제공된다. Stream<Integer>대신 IntStream을 사용하는 것이 더 효율적이다.
병렬 스트림
스트림으로 데이터를 다룰 때의 장점 중 하나는 바로 병렬 처리가 쉽다는 것이다. 병렬 스트림은 내부적으로 Java에서 제공하는 fork&join프레임웍을 이용해서 자동적으로 연산을 병렬로 수행한다. 그저 스트림에 parallel()이라는 메서드를 호출해서 병렬로 연산을 수행하도록 지시하면 된다.
스트림 만들기 - 컬렉션
컬렉션의 최고 조상인 Collection에 stream()이 정의되어 있다.
자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 stream()메서드로 스트림을 생성할 수 있다.
stream()은 해당 컬렉션을 소스(source)로 하는 스트림을 반환한다.
Stream<E> stream() // Collection 인터페이스의 메서드
List<Intger> list = Arrays.asList(1,2,3,4,5); // 가변인자
Stream<Integer> intStream = list.stream(); // list를 소스로 하는 스트림 생성
스트림 만들기 - 배열
배열을 소스로 하는 스트림을 생성하는 메서드는 Stream과 Arrays에 static 메서드로 정의되어 있다.
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)
//예시
Stream<String> Stream.of("a", "b", "c");
Stream<String> Stream.of(new String[]{"a", "b", "c"});
Stream<String> Arrays.stream(new String[]{"a", "b", "c"});
Stream<String> Arrays.stream(new String[]{"a", "b", "c"}, int 0, int 3);
int, long, double과 같은 기본형 배열을 소스로 하는 스트림을 생성하는 메서드도 있다.
IntStream IntStream.of(int... values) // Stream이 아니라 IntStream
IntStream IntStream.of(int[])
IntStream Arrays.stream(int[])
IntStream Arrays.stream(int[] array, int startInclusive, int endExclusive)
스트림 만들기 - 임의의 수
Random클래스에는 난수들로 이루어지는 스트림을 반환하는 인스턴스 메서드들이 포함되어 있다.
IntStream ints()
LongStream longs()
DoubleStream doubles()
이 메서드들이 반환하는 스트림은 크기가 정해지지 않은 ‘무한 스트림(infinite stream)’이므로 limit()도 같이 사용해서 스트림의 크기를 제한해주어야 한다. limit()은 스트림의 개수를 지정하는데 사용된다.
IntStream intStream = new Random().ints(); // 무한 스트림
intStream.limit(5).forEach(System.out::println); // 5개의 요소만 출력
아래의 메서드들은 매개변수로 스트림의 크기를 지정해서 ‘유한 스트림’을 생성해서 반환하므로 limit()을 사용하지 않아도 된다.
IntStream ints(long streamSize)
LongStream longs(long streamSize)
DoubleStream doubles(long streamSize)
//예시
IntStream intStream = new Random().ints(5); // 크기가 5인 난수 스트림을 반환
위 메서드들에 의해 생성된 스트림의 난수는 아래의 범위를 갖는다.
Integer.MIN_VALUE <= ints() <= Integer.MAX_VALUE
Long.MIN_VALUE <= longs() <= Long.MAX_VALUE
0.0 <= doubles() <= 1.0
스트림 만들기 - 특정 범위의 정수
IntStream과 LongStream은 다음과 같이 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 range()와 rangeClosed()를 가지고 있다.
- range()의 경우 경계 끝인 end가 범위에 포함되지 않는다.
- rangeClosed()의 경우는 end가 범위에 포함된다.
IntStream IntStream.range(int begin, int end)
IntStream IntStream.rangeClosed(int begin, int end)
//예시
IntStream intStream = IntStream.range(1, 5); // 1, 2, 3, 4
IntStream intStream = IntStream.rangeClosed(1, 5); // 1, 2, 3, 4, 5
지정된 범위(begin~end)의 난수를 발생시키는 스트림을 얻는 메서드는 아래와 같다.
단, end는 범위에 포함되지 않는다.
IntStream ints(int begin, int end)
LongStream longs(long begin, long end)
DoubleStream doubles(double begin, double end)
IntStream ints(long streamSize, int begin, int end)
LongStream longs(long streamSize, long begin, long end)
DoubleStream doubles(long streamSize, double begin, double end)
스트림 만들기 - 람다식 iterate( ), generate( )
Stream클래스의 iterate()와 generate()는 람다식을 매개변수로 받아서, 이 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성한다.
- iterate()는 씨앗값(seed)으로 지정된 값부터 시작해서, 람다식 f에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복한다.
- generate()는 iterate()와 달리 이전 결과를 이용해서 다음 요소를 계산하지 않는다. generate()에 정의된 매개변수의 타입은 Supplier<T>이므로 매개변수가 없는 람다식만 허용된다.
static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
static <T> Stream<T> generate(Supplier<T> s)
//예시
Stream<Integer> evenStream = Stream.iterate(0, n->n+2); // 0, 2, 4, 6
Stream<Integer> oneStream = Stream.generate(()->1); // 1, 1, 1, 1
iterate()와 generate()에 의해 생성된 스트림을 아래와 같이 기본형 스트림 타입의 참조변수로 다룰 수 없다. 굳이 필요하다면 mapToInt()와 같은 메서드로 변환을 해야 한다.
IntStream evenStream = Stream.iterate(0, n->n+2); //에러
DoubleSTream randomStream = Stream.generate(Math::random); //에러
IntStream evenStream = Stream.iterate(0, n->n+2).mapToInt(Integer::valueOf);
Stream<Integer> stream = evenStream.boxed(); //IntStream -> Stream<Integer>
스트림 만들기 - 파일과 빈 스트림
java.nio.file.Files의 list()는 지정된 디렉토리(dir)에 있는 파일의 목록을 소스로 하는 스트림을 생성해서 반환한다.
Stream<Path> Files.list(Path dir)
java.nio.file.Files의 lines()는 파일의 한 행(line)을 요소로 하는 스트림을 생성하는 메서드이다.
BufferedReader클래스의 lines()는 파일 뿐만 아니라 다른 입력대상으로부터도 데이터를 행단위로 읽어올 수 있다.
Stream<String> Files.lines(Path path)
Stream<String> Files.lines(Path path, Charset cs)
Stream<String> lines() // BufferedReader클래스의 메서드
빈 스트림
요소가 하나도 없는 비어있는 스트림을 생성할 수 있다.
스트림에 연산을 수행한 결과가 하나도 없을 때, null보다 빈 스트림을 반환하는 것이 낫다.
Stream emptyStream = Stream.empty(); //empty()는 빈 스트림을 생성해서 반환한다.
long count = emptyStream.count(); // count의 값은 0
스트림의 연산
스트림에 정의된 메서드 중에서 데이터 소스를 다루는 작업을 수행하는 것을 연산(operation)이라고 한다.
스트림이 제공하는 연산은 중간 연산과 최종 연산으로 분류할 수 있다.
- 중간 연산 : 연산 결과가 스트림인 연산. 스트림에 연속해서 중간 연산할 수 있음
- 최종 연산 : 연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 단 한번만 가능
스트림의 연산 - 중간연산
중간 연산은 map()과 flatMap()이 핵심이다.
중간 연산 | 설명 |
Stream<T> distinct( ) | 중복을 제거 |
Stream<T> filter(Predicate<T> predicate) | 조건에 안맞는 요소를 제외 |
Stream<T> limit(long maxsize) | 스트림의 일부를 잘라낸다. |
Stream<T> skip(long n) | 스트림의 일부를 건너뛴다. |
Stream<T> peek(Consumer<T> action) | 스트림의 요소에 작업을 수행 |
Stream<T> sorted(Comparator<T> comparator) | 스트림의 요소를 정렬한다. |
Stream<R> map(Function<T,R> mapper) IntStream mapToInt(ToIntFunction mapper) |
스트림의 요소를 변환한다. |
스트림의 연산 - 최종연산
최종 연산은 reduce()와 collect()가 핵심이다.
최종 연산 | 설명 |
void forEach(Consumer action) void forEachOrdered(Consumer<? super T> action) |
각 요소에 지정된 작업 수행 |
long count( ) | 스트림의 요소의 개수 반환 |
Optional<T> max(Comparator<? super T> comparator) Optional min(Comparator comparator) |
스트림의 최대값/최소값을 반환 |
Optional<T> findAny( )Optional<T> findFirst( ) | 스트림의 요소 하나를 반환 |
boolean allMatch(Predicate<T> p) boolean anyMatch(Predicate p) boolean noneMatch(Predicate<> p) |
주어진 조건에 대한 확인을 수행 |
Object[] toArray( )A[] toArray(IntFunction<A[ ]> generator) | 스트림의 모든 요소를 배열로 변환 |
Optional<T> reduce(BinaryOperator<T> accumulator) | 스트림의 요소를 하나씩 줄여가면서 계산 |
R collect(Collector<T,A,R> collector) | 스트림의 요소를 수집한다. 주로 요소를 그룹화하거나 분할한 결과를 컬렉션에 담아 반환하는데 사용 |
스트림의 중간연산 - skip( ), limit( )
skip(): 스트림의 일부를 건너뛴다.
limit(): 스트림의 일부를 잘라낸다.
Stream<T> skip(long n)
Stream<T> limit(long maxSize)
//예시
IntStream intStream = IntStream.rangeClosed(1, 10); //1~10의 요소를 가진 스트림
intStream.skip(3).limit(5).forEach(System.out::print); // 45678
스트림의 중간연산 - filter( ), distinct( )
distinct(): 스트림에서 중복된 요소들을 제거한다.
filter(): 주어진 조건(Predicate)에 맞지 않는 요소를 걸러낸다.
Stream<T> filter(Predicate<? super T> predicate)
Stream<T> distinct()
//예시
Intstream intStream = IntStream.of(1, 2, 2, 3, 3, 3, 4, 5, 5, 6);
intStream.distinct().forEach(System.out::print); // 123456
Intstream intStream = IntStream.rangeClosed(1, 10); // 1~10
intStream.filter(i -> i%2==0).forEach(System.out::print); // 246810
filter()를 다른 조건으로 여러 번 사용할 수도 있다.
Intstream intStream = IntStream.rangeClosed(1, 10); // 1~10
//아래의 두 문장은 동일한 결과를 얻는다.
intStream.filter(i->i%2!=0&&i%3!=0).forEach(System.out::print); // 157
intStream.filter(i->i%2!=0).filter(i->i%3!=0).forEach(System.out::print);
스트림의 중간연산 - sorted( )
sorted() : 스트림을 정렬할 때 사용한다.
Stream<T> sorted() //Stream 요소의 Comparable로 정렬한다.
Stream<T> sorted(Comparator<? super T> comparator)
sorted()는 지정된 Comparator로 스트림을 정렬하는데, Comparator대신 int값을 반환하는 람다식을 사용하는 것도 가능하다. Comparator를 지정하지 않으면 스트림 요소의 기본 정렬 기준(Comparable)으로 정렬한다. 단, 스트림의 요소가 Comparable을 구현한 클래스가 아니면 예외가 발생한다.
Stream<String> strStream = Stream.of("dd", "aaa", "CC", "cc", "b");
//모두 출력 결과는 CCaaabccdd
strStream.sorted().forEach(System.out::print); //기본정렬
strStream.sorted(Comparator.naturalOrder()); //Comparator을 이용한 기본정렬
strStream.sorted((s1,s2)->s1.compareTo(s2)); //람다식도 가능
strStream.sorted(String::compareTo); //위의 문장과 동일
스트림의 중간연산 - Comparator의 메서드
JDK1.8부터 Comparator인터페이스에 static메서드와 디폴트 메서드가 많이 추가되었는데, 이 메서드들을 이용하면 정렬이 쉬워진다. 이 메서드들은 모두 Comprator를 반환하며, 가장 기본적인 메서드는 comparing()이다.
- 스트림의 요소가 Comparable을 구현한 경우, 매개변수 하나짜리를 사용하면 된다.
- 그렇지 않은 경우, 추가적인 매개변수로 정렬기준(Comparator)을 따로 지정해 줘야한다.
comparing(Function<T, U> keyExtractor)
comparing(Function<T, U> keyExtractor, Comparator<U> keyComparator)
비교 대상이 기본형인 경우, comparing()대신 아래 메서드들을 사용하면 오토박싱과 언박싱 과정이 없어서 더 효율적이다.
comparingInt(ToIntFunction<T> keyExtractor)
comparingLong(ToLongFunction<T> keyExtractor)
comparingDouble(ToDoubleFunction<T> keyExtractor)
정렬 조건을 추가할 때는 thenComparing()을 사용한다.
thenComparing(Comparator<T> other)
thenComparing(Function<T, U> keyExtractor)
thenComparing(Function<T, U> keyExtractor, comparator<U> keyComp)
//예시
//학생 스트림(studentStream)을 반(ban)별, 성적(totalScore)순, 이름(name)순으로 정렬
studentStream.sorted(Comprator.comparing(Student::getBan)
.thenComparing(Student::getTotalScore)
.thenComparing(Student::getName))
.forEach(System.out::println);
예시
import java.util.*;
import java.util.stream.*;
class Ex14_5 {
public static void main(String[] args) {
Stream<Student> studentStream = Stream.of(
new Student("이자바", 3, 300),
new Student("김자바", 1, 200),
new Student("안자바", 2, 100),
new Student("박자바", 2, 150),
new Student("소자바", 1, 200),
new Student("나자바", 3, 290),
new Student("감자바", 3, 180)
);
studentStream.sorted(Comparator.comparing(Student::getBan) // 반별 정렬
.thenComparing(Comparator.naturalOrder())) // 기본 정렬
.forEach(System.out::println);
}
}
class Student implements Comparable<Student> {
String name;
int ban;
int totalScore;
Student(String name, int ban, int totalScore) {
this.name =name;
this.ban =ban;
this.totalScore =totalScore;
}
public String toString() {
return String.format("[%s, %d, %d]", name, ban, totalScore);
}
String getName() { return name;}
int getBan() { return ban;}
int getTotalScore() { return totalScore;}
// 총점 내림차순을 기본 정렬로 한다.
public int compareTo(Student s) {
return s.totalScore - this.totalScore;
}
}
/* 출력 결과
[김자바, 1, 200]
[소자바, 1, 200]
[박자바, 2, 150]
[안자바, 2, 100]
[이자바, 3, 300]
[나자바, 3, 290]
[감자바, 3, 180]
*/
스트림의 중간연산 - map( )
스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑아내거나 특정 형태로 변환해야 할 때 사용한다.
매개변수로 T타입을 R타입으로 변환해서 반환하는 함수(mapper)를 지정해야 한다.
Stream<R> map(Function<? super Y, ? extends R> mapper)
map()도 filter()처럼 하나의 스트림에 여러 번 적용할 수 있다.
예시
import java.io.*;
import java.util.stream.*;
class Ex14_6 {
public static void main(String[] args) {
File[] fileArr = { new File("Ex1.java"), new File("Ex1.bak"),
new File("Ex2.java"), new File("Ex1"), new File("Ex1.txt")
};
Stream<File> fileStream = Stream.of(fileArr);
// map()으로 Stream<File>을 Stream<String>으로 변환
Stream<String> filenameStream = fileStream.map(File::getName);
filenameStream.forEach(System.out::println); // 모든 파일의 이름을 출력
fileStream = Stream.of(fileArr); // 스트림을 다시 생성
//map() 여러 번 사용 가능
fileStream.map(File::getName) // Stream<File> → Stream<String>
.filter(s -> s.indexOf('.')!=-1) // 확장자가 없는 것은 제외
.map(s -> s.substring(s.indexOf('.')+1)) // 확장자만 추출
.map(String::toUpperCase) // 모두 대문자로 변환
.distinct() // 중복 제거
.forEach(System.out::print); // JAVABAKTXT
System.out.println();
}
}
/* 출력 결과
Ex1.java
Ex1.bak
Ex2.java
Ex1
Ex1.txt
JAVABAKTXT
*/
스트림의 중간연산 - peek( )
peek()을 사용하면 연산과 연산 사이에 올바르게 처리되었는지 확인할 수 있다.
forEach()와 달리 스트림의 요소를 소모하지 않으므로 연산 사이에 여러 번 끼워 넣어도 문제가 되지 않는다.
filter()나 map()의 결과를 확인할 때 유용하게 사용될 수 있다.
fileStream.map(File::getName) // Stream<File> → Stream<String>
.filter(s -> s.indexOf('.')!=-1) // 확장자가 없는 것은 제외
.peek(s->System.out.printf("filename=%s%n", s)) //파일명을 출력한다.
.map(s -> s.substring(s.indexOf('.')+1)) // 확장자만 추출
.peek(s->System.out.printf("extension=%s%n", s)) //확장자를 출력한다.
.forEach(System.out::println);
스트림의 중간연산 - flatMap( )
Stream의 타입이 Stream<T[]>인 경우, Stream<T[]>를 Stream<T>로 변환해줄 때 flatMap()을 사용한다. flatMap을 사용하지 않고 아래와 같이 map()과 Arrays.stream(T[])를 함께 사용하면 Stream<T>가 아니라 Stream<Stream<T>>를 결과로 얻게 된다.
스트림의 중간연산 - flatMap( ) 예제
import java.util.*;
import java.util.stream.*;
class Ex14_7 {
public static void main(String[] args) {
Stream<String[]> strArrStrm = Stream.of(
new String[]{"abc", "def", "jkl"},
new String[]{"ABC", "GHI", "JKL"}
);
// Stream<Stream<String>> strStrmStrm = strArrStrm.map(Arrays::stream);
Stream<String> strStrm = strArrStrm.flatMap(Arrays::stream);
strStrm.map(String::toLowerCase)
.distinct()
.sorted()
.forEach(System.out::println);
System.out.println();
String[] lineArr = {
"Believe or not It is true",
"Do or do not There is no try",
};
Stream<String> lineStream = Arrays.stream(lineArr);
//" +"는 정규식 표현으로 하나 이상의 공백을 의미한다.
lineStream.flatMap(line -> Stream.of(line.split(" +")))
.map(String::toLowerCase)
.distinct()
.sorted()
.forEach(System.out::println);
System.out.println();
}
}
/* 출력 결과
abc
def
ghi
jkl
believe
do
is
it
no
not
or
there
true
try
*/
Optional〈T〉
Optional는 ‘T타입의 객체’를 감싸는 래퍼 클래스이다. 모든 타입의 객체를 담을 수 있다.
JDK1.8부터 추가되었다.
최종 연산의 결과를 Optional객체에 담아서 반환을 하면, 반환된 결과가 null인지 매번 if문으로 체크하는 대신 Optional에 정의된 메서드를 통해서 간단히 처리할 수 있다.
Optional〈T〉객체 생성하기
Optional객체를 생성할 때는 of()또는 ofNullable()을 사용한다.
- of()는 매개변수의 값이 null이면 NullPointException이 발생한다.
- ofNullable()은 NullPointException이 발생하지 않는다.
Optional<String> optVal = Optional.of(null); //NullPointerException 발생
Optional<String> optVal = Optional.ofNullable(null); //OK
Optional<T> 타입의 참조변수를 기본값으로 초기화할 때는 empty()를 사용한다.
null로 초기화하는 것이 가능하지만, empty()로 초기화하는 것이 바람직하다.
Optional<String> optVal = null; // 널로 초기화. 바람직하지 않음
Optional<String> optVal = Optional.<String>empty(); // 빈 객체로 초기화
Optional〈T〉객체의 값 가져오기
Optional객체에 저장된 값을 가졍로 때는 get()을 사용한다.
값이 null일 때는 NoSuchElementException이 발생한다.
이를 대비하여 orElse()로 대체할 값을 지정할 수 있다.
Optional<String> optVal = Optional.of("abc");
String str1 = optVal.get(); // optVal에 저장된 값을 반환. null이면 예외발생
String str2 = optVal.orElse(""); //optVal에 저장된 값이 null일 때는, ""를 반환.
//여기서는 null이 아니므로 "abc"를 반환한다.
orElseGet()은 orElse()의 변형으로 null을 대체할 값을 반환하는 람다식을 지정할 수 있다.
orElseThrow()는 null일 때 지정된 예외를 발생시킨다.
T orElseGet(Supplier<? extends T> other)
T orElseThrow(Supplier<? extends X> exceptionSupplier)
//예시
String str3 = optVal2.orElseGet(String::new); // () -> new String()와 동일
String str4 = optVal2.orElseThrow(NullPointerException::new); // null이면 예외발생
isPresent()는 Optional객체의 값이 null이면 false를, 아니면 true를 반환한다.
isPresent(Consumer<T> block)은 값이 있으면 주어진 람다식을 실행하고, 없으면 아무 일도 하지 않는다.
if(str!=null){
System.out.println(str);
}
//isPresent()를 이용해 아래와 같이 바꿀 수 있다.
if(Optional.ofNullable(str).ifPresent()){
System.out.println(str);
}
//더 간단히 아래와 같이 바꿀 수 있다.
Optional.ofNullable(str).ifPresent(System.out::println);
OptionalInt, OptionalLong, OptionalDouble
IntStream과 같은 기본형 스트림의 최종 연산의 일부는 Optional대신 기본형을 값으로 하는 OptionalInt, OptionalLong, OptionalDouble을 반환한다.
OptionalInt이 아무런 값을 갖지 않으면 기본으로 저장되는 값은 0이다.
0이 저장된 것과 값이 없는 것은 isPreset()로 구분 가능하다.
OptionalInt opt = OptionalInt.of(0); //OptionalInt에 0을 저장
OptionalInt opt2 = OptionalInt.empty(); //OptionalInt에 0을 저장
opt.isPresent(); //true
opt2.isPresent(); //false
스트림의 최종연산 - forEach( )
forEach()는 주어진 함수를 스트림 요소 각각에 대해 실행한다.
peek()와 달리 스트림의 요소를 소모하는 최종연산이다.
반환 타입이 void이므로 스트림의 요소를 출력하는 용도로 많이 사용된다.
void forEach(Consumer<? super T> action)
스트림의 최종연산 - 조건검사
스트림의 요소에 대해 지정된 조건에 모든 요소가 일치하는 지, 일부가 일치하는지, 아니면 어떤 요소도 일치하지 않는지 확인하는데 사용할 수 있는 메서드들이다. 이 메서드들은 모두 매개변수로 Predicate를 요구하며, 연산결과로 boolean을 반환한다.
boolean allMatch (Predicate<? super T> predicate) 모든 요소가 일치하면 참
boolean anyMatch (Predicate<? super T> predicate) 하나의 요소라도 일치하면 참
boolean noneMatch (Predicate<? super T> predicate) 모든 요소가 불일치하면 참
//예시
//학생들의 성적 정보 스트림 stuStream에서 총점이 낙제점(총점 100점 이하)인 학생이 있는지 확인
boolean noFailed = stuStream.anyMatch(s->s.getTotalScore()<=100)
findFirst()는 스트림의 요소 중에서 건에 일치하는 첫 번째 것을 반환한다. 주로 filter()와 함께 사용된다. 병렬 스트림의 경우 findFirst() 대신 findAny()를 사용해야 한다.
Optional<T> findFirst() 조건에 일치하는 첫 번째 요소를 반환
Optional<T> findAny() 조건에 일치하는 요소를 하나 반환(병렬 스트림)
//예시
Optional<Student> stu = stuStream.filter(s->s.getTotalScore()<=100).findFirst();
Optional<Student> stu = parallelStream.filter(s->s.getTotalScore()<=100).findAny();
스트림의 최종연산 - reduce( )
reduce()는 스트림의 요소를 줄여나가면서 연산을 수행하고 최종 결과를 반환한다. 그래서 매개변수의 타입이 BinaryOperator<T>인 것이다. 처음 두 요소를 가지고 연산한 결과를 가지고 그 다음 요소와 연산한다. 이 과정에서 스트림의 요소를 하나씩 소모하며, 스트림의 모든 요소를 소모하게 되면 그 결과를 반환한다.
Optional<T> reduce(BinaryOperator<T> accumulator)
매개변수로 초기값(identity)을 갖는 reduce()도 있다. 이 메서드들은 초기값과 스트림의 첫 번째 요소로 연산을 시작한다. 스트림의 요소가 하나도 없는 경우, 초기값이 반환되므로, 반환 타입이 Optional<T>가 아니라 T이다.
T reduce(T identity, BinaryOperator<T> accumulator)
U reduce(U identity, BiFunction<U, T, U> accmulator, BinaryOperator<U> combiner)
//예시
int count = intStream.reduce(0, (a,b) -> a+1); //count()
int sum = intStream.reduce(0, (a,b) -> a+b); //sum()
int max = intStream.reduce(Integer.MIN_VALUE, (a,b) -> a>b ? a:b); //max()
int min = intStream.reduce(Integer.MAX_VALUE, (a,b) -> a<b ? a:b); //min()
스트림의 최종연산 - reduce( )의 이해
reduce()가 내부적으로 어떻게 동작하는지 이해를 돕기 위해, reduce()로 스트림의 모든 요소를 다 더하는 과정을 for문으로 표현해 보았다.
// intStream.reduce(identity, (a,b) -> a+b); 을 for문으로 표현
int a = identity; //초기값을 a에 저장
for(int b : stream)
a = a+b; //모든 요소의 값을 a에 누적한다.
reduce() 내부는 아래와 같이 되어있다. 초기값(identity)와 어떤 연산(Biary Operator)으로 스트림의 요소를 줄여나갈 것인지 결정하면 된다.
T reduce(T identity, BinaryOperator<T> accumulator){
T a = identity;
for(T b : stream)
a = accmulator.apply(a, b);
return a;
}
스트림의 최종연산 - reduce( ) 예제
import java.util.*;
import java.util.stream.*;
class Ex14_9 {
public static void main(String[] args) {
String[] strArr = {
"Inheritance", "Java", "Lambda", "stream",
"OptionalDouble", "IntStream", "count", "sum"
};
Stream.of(strArr).forEach(System.out::println);
/*출력 결과
Inheritance
Java
Lambda
stream
OptionalDouble
IntStream
count
sum
*/
boolean noEmptyStr = Stream.of(strArr).noneMatch(s->s.length()==0);
Optional<String> sWord = Stream.of(strArr)
.filter(s->s.charAt(0)=='s').findFirst();
System.out.println("noEmptyStr="+noEmptyStr);
System.out.println("sWord="+ sWord.get());
/*출력 결과
noEmptyStr=true
sWord=stream
*/
// Stream<String>을 IntStream으로 변환
IntStream intStream1 = Stream.of(strArr).mapToInt(String::length);
IntStream intStream2 = Stream.of(strArr).mapToInt(String::length);
IntStream intStream3 = Stream.of(strArr).mapToInt(String::length);
IntStream intStream4 = Stream.of(strArr).mapToInt(String::length);
int count = intStream1.reduce(0, (a,b) -> a + 1);
int sum = intStream2.reduce(0, (a,b) -> a + b);
OptionalInt max = intStream3.reduce(Integer::max);
OptionalInt min = intStream4.reduce(Integer::min);
System.out.println("count="+count);
System.out.println("sum="+sum);
System.out.println("max="+ max.getAsInt());
System.out.println("min="+ min.getAsInt());
/*출력 결과
count=8
sum=58
max=14
min=3
*/
}
}
collect( )와 Collectors
collect()는 스트림의 요소를 수집하는 최종 연산이다.
- collect(): 스트림의 최종연산. 매개변수로 컬렉터를 필요로 한다.
- Collector: 인터페이스. 컬렉터는 이 인터페이스를 구현해야한다.
- Collectors: 클래스. static메서드로 미리 작성된 컬렉터를 제공한다.
collect()는 매개변수의 타입이 Collector인데, 매개변수가 Collector를 구현한 클래스의 객체이어야 한다는 뜻이다. 이 객체에 구현된 방법대로 collect()는 스트림의 요소를 수집한다.
Object collect(Collector collector) // Collector를 구현한 클래스의 객체를 매개변수로
Object collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)
스트림을 컬렉션, 배열로 변환
스트림의 모든 요소를 컬렉션에 수집하려면, Collectors클래스의 toList()와 같은 메서드를 사용하면 된다. 특정 컬렉션을 지정하려면, toCollection()에 원하는 컬렉션의 생성자 참조를 매개변수로 넣어주면 된다.
List<String> names = stuStream.map(Student::getName).collect(Collectors.toList());
ArrayList<String> list = names.stream().collect(Collectors.toCollection(ArrayList::new));
Map은 키와 값을 쌍으로 저장해야하므로 객체의 어떤 필드를 키로 사용할지와 값으로 사용할지를 지정해줘야 한다.
//Person의 주민번호(regId)를 키로, Person객체를 값으로 저장
//p->p 대신 항등 함수로 Function.identity()를 쓸 수도 있다.
Map<String, Person> map = personStream.collect(Collectors.toMap(p->p.getRegId(), p->p));
스트림에 저장된 요소들을 ‘T[]’타입의 배열로 변환하려면, toArray()를 사용하면 된다.
단, 해당 타입의 생성자 참조를 매개변수로 지정해줘야 한다.
지정하지 않으면 반횐되는 배열의 타입은 ‘Object[]’이다.
Student[] stuNames = studentStream.toArray(Student[]::new); // OK
Student[] stuNames = studentStream.toArray(); // 에러
Object[] stuNames = studentStream.toArray(); // OK
스트림의 통계 - counting( ), summingInt( )
최종 연산들이 제공하는 통계 정보를 collect()로 똑같이 얻을 수 있다.
groupingBy()와 함께 사용할 때 유용하다.
//count()를 Collectors의 counting()을 사용해서 구현.
long count = stuStream.count();
long count = stuStream.collect(Collectors.counting());
//sum()을 Collectors의 summingInt()를 사용해서 구현.
long totalScore = stuStream.mapToInt(Student::getTotalScore).sum();
long totalScore = struStream.collect(Collectors.summingInt(Student::getTotalScore));
//max()를 Collectors의 maxBy()를 이용해 구현
Optional<Student> topStudent =
stuStream.max(Comprator.comparingInt(Student::getTotalScore));
Optional<Student> topStudent =
stuStream.collect(Collectors.maxBy(Comprator.comparingInt(Student::getTotalScore)));
//summaryStatistics()를 Collectors의 summarizingInt()를 이용해 구현
IntSummaryStatistics stat =
stuStream.mapToInt(Student::getTotalScore).summaryStatistics();
IntSummaryStatistics stat =
stuStream.collect(Collectors.summarizingInt(Student::getTotalScore));
IntSummaryStatistics 객체에서 max, min, average, count 정보를 가져올 수 있다.
List<String> langs =
Arrays.asList("java", "kotlin", "haskell", "ruby", "javascript");
IntSummaryStatistics stats = langs.stream()
.mapToInt((lang) -> (lang.length()))
.summaryStatistics();
System.out.println("Max: " + stats.getMax());
System.out.println("Min: " + stats.getMin());
System.out.println("Average: " + stats.getAverage());
System.out.println("Count: " + stats.getCount());
/* 출력 결과
Max: 10
Min: 4
Average: 6.2
Count: 5
*/
스트림을 리듀싱 - reducing( )
리듀싱 역시 collect()로 가능하다.
Collectors.reducing()에는 아래와 같이 3가지 종류가 있다.
Collector reducing(BinaryOperator<T> op)
Collector reducing(T identity, BinaryOperator<T> op) //identity는 초기값
//map()과 reduce()를 하나로 합쳤다.
Collector reducing(U identity, Function<T,U> mapper, BinaryOperator<U> op)
IntStream에는 매개변수 3개짜리 collect()만 정의되어 있으므로 boxed()를 통해 IntStream을 Stream로 변환해야 매개변수 1개짜리 collect()를 쓸 수 있다.
IntStream intStream = new Random().ints(1,46).distinct().limit(6); // 1~46 랜덤하게 6개
//Collector reducing(BinaryOperator<T> op) 예시
OptionalInt max = intStream.reduce(Integer::max);
Optional<Integer> max = intStream.boxed().collect(Collectors.reducing(Integer::max));
//Collector reducing(T identity, BinaryOperator<T> op) 예시
long sum = intStream.reduce(0, (a,b) -> a + b);
long sum = intStream.boxed().collect(Collectors.reducing(0, (a,b) -> a + b));
//Collector reducing(U identity, Function<T,U> mapper, BinaryOperator<U> op) 예시
int grandTotal = stuStream.map(Student::getTotalScore).reduce(0, Integer::sum));
int grandTotal =
stuStream.collect(Collectors.reducing(0,Student::getTotalScore,Integer::sum));
스트림을 문자열로 결합 - joining( )
joining()은 문자열 스트림의 모든 요소를 하나의 문자열로 연결해서 반환한다.
구분자를 지정해줄 수도 있고, 접두사와 접미사도 지정가능하다.
스트림의 요소가 문자열이 아닌 경우에는 먼저 map()을 이용해서 스트림의 요소를 문자열로 변환해야 한다. 만일 map()없이 스트림에 바로 joinning()하면, 스트림의 요소에 toString()을 호출한 결과를 결합한다.
String studentNames = stuStream.map(Student::getName).collect(Collectors.joining());
String studentNames = stuStream.map(Student::getName).collect(Collectors.joining(","));
String studentNames =
stuStream.map(Student::getName).collect(Collectors.joining(",","[","]"));
//map이 없으면 Student의 toString()으로 결합
String studentInfo = stuStream.collect(joining(","));
스트림의 그룹화와 분할
그룹화는 groupingBy()를 이용해 스트림의 요소를 특정 기준으로 그룹화하는 것을 의미한다.
분할은 partitioningBy()를 이용해 스트림의 요소를 두 가지, 지정된 조건에 일치하는 그룹과 일치하지 않는 그룹으로의 분할을 의미한다.
groupingBy()는 스트림의 요소를 Function으로, partitioningBy()는 Predicate로 분류한다.
Collector partitioningBy(Predicate predicate)
Collector partitioningBy(Predicate predicate, Collector downstream)
Collector groupingBy(Function classifier)
Collector groupingBy(Function classifier, Collector downstream)
Collector groupingBy(Function classifier, Supplier mapFactory, Collector downstream)
스트림을 두 개의 그룹으로 나눠야 한다면, partitioningBy()로 분할하는 것이 더 빠르다.
그 외에는 groupingBy()를 쓰면 된다.
그룹화와 분할의 결과는 Map에 담겨 반환된다.
스트림의 분할 - partitioningBy( )
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import static java.util.stream.Collectors.*;
import static java.util.Comparator.*;
class Student2 {
String name;
boolean isMale; // 성별
int hak; // 학년
int ban; // 반
int score;
Student2(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);
}
// groupingBy()에서 사용
enum Level { HIGH, MID, LOW } // 성적을 상, 중, 하 세 단계로 분류
}
class Ex14_10 {
public static void main(String[] args) {
Student2[] stuArr = {
new Student2("나자바", true, 1, 1, 300),
new Student2("김지미", false, 1, 1, 250),
new Student2("김자바", true, 1, 1, 200),
new Student2("이지미", false, 1, 2, 150),
new Student2("남자바", true, 1, 2, 100),
new Student2("안지미", false, 1, 2, 50),
new Student2("황지미", false, 1, 3, 100),
new Student2("강지미", false, 1, 3, 150),
new Student2("이자바", true, 1, 3, 200),
new Student2("나자바", true, 2, 1, 300),
new Student2("김지미", false, 2, 1, 250),
new Student2("김자바", true, 2, 1, 200),
new Student2("이지미", false, 2, 2, 150),
new Student2("남자바", true, 2, 2, 100),
new Student2("안지미", false, 2, 2, 50),
new Student2("황지미", false, 2, 3, 100),
new Student2("강지미", false, 2, 3, 150),
new Student2("이자바", true, 2, 3, 200)
};
System.out.printf("1. 단순분할(성별로 분할)%n");
Map<Boolean, List<Student2>> stuBySex = Stream.of(stuArr)
.collect(partitioningBy(Student2::isMale));
List<Student2> maleStudent = stuBySex.get(true);
List<Student2> femaleStudent = stuBySex.get(false);
for(Student2 s : maleStudent) System.out.println(s);
for(Student2 s : femaleStudent) System.out.println(s);
/* 출력 결과
1. 단순분할(성별로 분할)
[나자바, 남, 1학년 1반, 300점]
[김자바, 남, 1학년 1반, 200점]
[남자바, 남, 1학년 2반, 100점]
[이자바, 남, 1학년 3반, 200점]
[나자바, 남, 2학년 1반, 300점]
[김자바, 남, 2학년 1반, 200점]
[남자바, 남, 2학년 2반, 100점]
[이자바, 남, 2학년 3반, 200점]
[김지미, 여, 1학년 1반, 250점]
[이지미, 여, 1학년 2반, 150점]
[안지미, 여, 1학년 2반, 50점]
[황지미, 여, 1학년 3반, 100점]
[강지미, 여, 1학년 3반, 150점]
[김지미, 여, 2학년 1반, 250점]
[이지미, 여, 2학년 2반, 150점]
[안지미, 여, 2학년 2반, 50점]
[황지미, 여, 2학년 3반, 100점]
[강지미, 여, 2학년 3반, 150점]
*/
System.out.printf("%n2. 단순분할 + 통계(성별 학생수)%n");
Map<Boolean, Long> stuNumBySex = Stream.of(stuArr)
.collect(partitioningBy(Student2::isMale, counting()));
System.out.println("남학생 수 :"+ stuNumBySex.get(true));
System.out.println("여학생 수 :"+ stuNumBySex.get(false));
/* 출력 결과
2. 단순분할 + 통계(성별 학생수)
남학생 수 :8
여학생 수 :10
*/
System.out.printf("%n3. 단순분할 + 통계(성별 1등)%n");
Map<Boolean, Optional<Student2>> topScoreBySex = Stream.of(stuArr)
.collect(partitioningBy(Student2::isMale,
maxBy(comparingInt(Student2::getScore))
));
System.out.println("남학생 1등 :"+ topScoreBySex.get(true));
System.out.println("여학생 1등 :"+ topScoreBySex.get(false));
/* 출력 결과
3. 단순분할 + 통계(성별 1등)
남학생 1등 :Optional[[나자바, 남, 1학년 1반, 300점]]
여학생 1등 :Optional[[김지미, 여, 1학년 1반, 250점]]
*/
//collectingAndThen을 사용해서 Optional<Student>가 아닌 Student를 반환 결과로 받음
Map<Boolean, Student2> topScoreBySex2 = Stream.of(stuArr)
.collect(partitioningBy(Student2::isMale,
collectingAndThen(
maxBy(comparingInt(Student2::getScore)), Optional::get
)
));
System.out.println("남학생 1등 :"+ topScoreBySex2.get(true));
System.out.println("여학생 1등 :"+ topScoreBySex2.get(false));
/*
남학생 1등 :[나자바, 남, 1학년 1반, 300점]
여학생 1등 :[김지미, 여, 1학년 1반, 250점]
*/
System.out.printf("%n4. 다중분할(성별 불합격자, 100점 이하)%n");
Map<Boolean, Map<Boolean, List<Student2>>> failedStuBySex =
Stream.of(stuArr).collect(partitioningBy(Student2::isMale,
partitioningBy(s -> s.getScore() <= 100))
);
List<Student2> failedMaleStu = failedStuBySex.get(true).get(true);
List<Student2> failedFemaleStu = failedStuBySex.get(false).get(true);
for(Student2 s : failedMaleStu) System.out.println(s);
for(Student2 s : failedFemaleStu) System.out.println(s);
/* 출력 결과
4. 다중분할(성별 불합격자, 100점 이하)
[남자바, 남, 1학년 2반, 100점]
[남자바, 남, 2학년 2반, 100점]
[안지미, 여, 1학년 2반, 50점]
[황지미, 여, 1학년 3반, 100점]
[안지미, 여, 2학년 2반, 50점]
[황지미, 여, 2학년 3반, 100점]
*/
}
}
스트림의 그룹화 - groupingBy( )
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import static java.util.stream.Collectors.*;
import static java.util.Comparator.*;
class Student3 {
String name;
boolean isMale; // 성별
int hak; // 학년
int ban; // 반
int score;
Student3(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
}
}
class Ex14_11 {
public static void main(String[] args) {
Student3[] stuArr = {
new Student3("나자바", true, 1, 1, 300),
new Student3("김지미", false, 1, 1, 250),
new Student3("김자바", true, 1, 1, 200),
new Student3("이지미", false, 1, 2, 150),
new Student3("남자바", true, 1, 2, 100),
new Student3("안지미", false, 1, 2, 50),
new Student3("황지미", false, 1, 3, 100),
new Student3("강지미", false, 1, 3, 150),
new Student3("이자바", true, 1, 3, 200),
new Student3("나자바", true, 2, 1, 300),
new Student3("김지미", false, 2, 1, 250),
new Student3("김자바", true, 2, 1, 200),
new Student3("이지미", false, 2, 2, 150),
new Student3("남자바", true, 2, 2, 100),
new Student3("안지미", false, 2, 2, 50),
new Student3("황지미", false, 2, 3, 100),
new Student3("강지미", false, 2, 3, 150),
new Student3("이자바", true, 2, 3, 200)
};
System.out.printf("1. 단순그룹화(반별로 그룹화)%n");
Map<Integer, List<Student3>> stuByBan = Stream.of(stuArr)
.collect(groupingBy(Student3::getBan));
//key는 반이고, 값은 반별 Student 객체 리스트이다.
for(List<Student3> ban : stuByBan.values()) {
for(Student3 s : ban) {
System.out.println(s);
}
System.out.println();
}
/* 출력 결과
1. 단순그룹화(반별로 그룹화)
[나자바, 남, 1학년 1반, 300점]
[김지미, 여, 1학년 1반, 250점]
[김자바, 남, 1학년 1반, 200점]
[나자바, 남, 2학년 1반, 300점]
[김지미, 여, 2학년 1반, 250점]
[김자바, 남, 2학년 1반, 200점]
[이지미, 여, 1학년 2반, 150점]
[남자바, 남, 1학년 2반, 100점]
[안지미, 여, 1학년 2반, 50점]
[이지미, 여, 2학년 2반, 150점]
[남자바, 남, 2학년 2반, 100점]
[안지미, 여, 2학년 2반, 50점]
[황지미, 여, 1학년 3반, 100점]
[강지미, 여, 1학년 3반, 150점]
[이자바, 남, 1학년 3반, 200점]
[황지미, 여, 2학년 3반, 100점]
[강지미, 여, 2학년 3반, 150점]
[이자바, 남, 2학년 3반, 200점]
*/
System.out.printf("%n2. 단순그룹화(성적별로 그룹화)%n");
Map<Student3.Level, List<Student3>> stuByLevel = Stream.of(stuArr)
.collect(groupingBy(s-> {
if(s.getScore() >= 200) return Student3.Level.HIGH;
else if(s.getScore() >= 100) return Student3.Level.MID;
else return Student3.Level.LOW;
}));
TreeSet<Student3.Level> keySet = new TreeSet<>(stuByLevel.keySet());
for(Student3.Level key : keySet) {
System.out.println("["+key+"]");
for(Student3 s : stuByLevel.get(key))
System.out.println(s);
System.out.println();
}
/* 출력 결과
2. 단순그룹화(성적별로 그룹화)
[HIGH]
[나자바, 남, 1학년 1반, 300점]
[김지미, 여, 1학년 1반, 250점]
[김자바, 남, 1학년 1반, 200점]
[이자바, 남, 1학년 3반, 200점]
[나자바, 남, 2학년 1반, 300점]
[김지미, 여, 2학년 1반, 250점]
[김자바, 남, 2학년 1반, 200점]
[이자바, 남, 2학년 3반, 200점]
[MID]
[이지미, 여, 1학년 2반, 150점]
[남자바, 남, 1학년 2반, 100점]
[황지미, 여, 1학년 3반, 100점]
[강지미, 여, 1학년 3반, 150점]
[이지미, 여, 2학년 2반, 150점]
[남자바, 남, 2학년 2반, 100점]
[황지미, 여, 2학년 3반, 100점]
[강지미, 여, 2학년 3반, 150점]
[LOW]
[안지미, 여, 1학년 2반, 50점]
[안지미, 여, 2학년 2반, 50점]
*/
System.out.printf("%n3. 단순그룹화 + 통계(성적별 학생수)%n");
Map<Student3.Level, Long> stuCntByLevel = Stream.of(stuArr)
.collect(groupingBy(s-> {
if(s.getScore() >= 200) return Student3.Level.HIGH;
else if(s.getScore() >= 100) return Student3.Level.MID;
else return Student3.Level.LOW;
}, counting()));
for(Student3.Level key : stuCntByLevel.keySet())
System.out.printf("[%s] - %d명, ", key, stuCntByLevel.get(key));
System.out.println();
for(List<Student3> level : stuByLevel.values()) {
System.out.println();
for(Student3 s : level) {
System.out.println(s);
}
}
/* 출력 결과
3. 단순그룹화 + 통계(성적별 학생수)
[LOW] - 2명, [MID] - 8명, [HIGH] - 8명,
[안지미, 여, 1학년 2반, 50점]
[안지미, 여, 2학년 2반, 50점]
[이지미, 여, 1학년 2반, 150점]
[남자바, 남, 1학년 2반, 100점]
[황지미, 여, 1학년 3반, 100점]
[강지미, 여, 1학년 3반, 150점]
[이지미, 여, 2학년 2반, 150점]
[남자바, 남, 2학년 2반, 100점]
[황지미, 여, 2학년 3반, 100점]
[강지미, 여, 2학년 3반, 150점]
[나자바, 남, 1학년 1반, 300점]
[김지미, 여, 1학년 1반, 250점]
[김자바, 남, 1학년 1반, 200점]
[이자바, 남, 1학년 3반, 200점]
[나자바, 남, 2학년 1반, 300점]
[김지미, 여, 2학년 1반, 250점]
[김자바, 남, 2학년 1반, 200점]
[이자바, 남, 2학년 3반, 200점]
*/
System.out.printf("%n4. 다중그룹화(학년별, 반별)");
Map<Integer, Map<Integer, List<Student3>>> stuByHakAndBan =
Stream.of(stuArr)
.collect(groupingBy(Student3::getHak,
groupingBy(Student3::getBan)
));
for(Map<Integer, List<Student3>> hak : stuByHakAndBan.values()) {
for(List<Student3> ban : hak.values()) {
System.out.println();
for(Student3 s : ban)
System.out.println(s);
}
}
/* 출력 결과
4. 다중그룹화(학년별, 반별)
[나자바, 남, 1학년 1반, 300점]
[김지미, 여, 1학년 1반, 250점]
[김자바, 남, 1학년 1반, 200점]
[이지미, 여, 1학년 2반, 150점]
[남자바, 남, 1학년 2반, 100점]
[안지미, 여, 1학년 2반, 50점]
[황지미, 여, 1학년 3반, 100점]
[강지미, 여, 1학년 3반, 150점]
[이자바, 남, 1학년 3반, 200점]
[나자바, 남, 2학년 1반, 300점]
[김지미, 여, 2학년 1반, 250점]
[김자바, 남, 2학년 1반, 200점]
[이지미, 여, 2학년 2반, 150점]
[남자바, 남, 2학년 2반, 100점]
[안지미, 여, 2학년 2반, 50점]
[황지미, 여, 2학년 3반, 100점]
[강지미, 여, 2학년 3반, 150점]
[이자바, 남, 2학년 3반, 200점]
*/
System.out.printf("%n5. 다중그룹화 + 통계(학년별, 반별 1등)%n");
Map<Integer, Map<Integer, Student3>> topStuByHakAndBan =
Stream.of(stuArr)
.collect(groupingBy(Student3::getHak,
groupingBy(Student3::getBan,
collectingAndThen(
maxBy(comparingInt(Student3::getScore))
, Optional::get
)
)
));
for(Map<Integer, Student3> ban : topStuByHakAndBan.values())
for(Student3 s : ban.values())
System.out.println(s);
/* 출력 결과
5. 다중그룹화 + 통계(학년별, 반별 1등)
[나자바, 남, 1학년 1반, 300점]
[이지미, 여, 1학년 2반, 150점]
[이자바, 남, 1학년 3반, 200점]
[나자바, 남, 2학년 1반, 300점]
[이지미, 여, 2학년 2반, 150점]
[이자바, 남, 2학년 3반, 200점]
*/
System.out.printf("%n6. 다중그룹화 + 통계(학년별, 반별 성적그룹)%n");
Map<String, Set<Student3.Level>> stuByScoreGroup = Stream.of(stuArr)
.collect(groupingBy(s-> s.getHak() + "-" + s.getBan(),
mapping(s-> {
if(s.getScore() >= 200) return Student3.Level.HIGH;
else if(s.getScore() >= 100) return Student3.Level.MID;
else return Student3.Level.LOW;
} , toSet())
));
Set<String> keySet2 = stuByScoreGroup.keySet();
for(String key : keySet2) {
System.out.println("["+key+"]" + stuByScoreGroup.get(key));
}
/* 출력 결과
6. 다중그룹화 + 통계(학년별, 반별 성적그룹)
[1-1][HIGH]
[2-1][HIGH]
[1-2][LOW, MID]
[2-2][LOW, MID]
[1-3][MID, HIGH]
[2-3][MID, HIGH]
*/
} // main의 끝
}
스트림의 변환
스트림 간의 변환을 찾기 쉽게 정리했다.
'Programming Language > 자바 기초' 카테고리의 다른 글
13. 쓰레드 (0) | 2022.03.14 |
---|---|
12. 지네릭스, 열거형, 애너테이션 (0) | 2022.03.14 |
11. 컬렉션 프레임웍 (0) | 2022.03.14 |
10. 날짜와 시간 & 형식화 (0) | 2022.03.14 |
9. java.lang 패키지와 유용한 클래스 (0) | 2022.03.14 |