Chapter 14 람다와 스트림

1. 람다식

1.1 람다식이란?

람다식은 메서드를 하나의 식(expression)으로 표현한 것.

- 객체 지향 언어보다는 함수 지향 언어에 가깝다.
- 함수를 간략하면서도 명확한 식으로 표현할 수 있도록 해준다.
- 메서드를 람다식으로 표현하면 메서드의 이름 및 반환 값이 없어지므로 익명 함수 라고도 한다.
- 람다식의 형태는 매개 변수를 가진 코드 블록이지만 런타임 시에는 익명 구현 객체를 생성한다.

http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html

1.2 람다식 작성하기

(타입 매개변수) -> { 실행문; ... }

ex)
- ExFunctionInterfaceTest.java

interface ExFunctionInterface
{
    public void method();
}


public class ExFunctionInterfaceTest
{
    public static void main(String[] args)
    {
        ExFunctionInterface test = new ExFunctionInterface() {

            public void method() {
                System.out.println("test");
            }
        };

        test.method();
    }
}

public class ExFunctionInterfaceTest
{
    public static void main(String[] args)
    {
        ExFunctionInterface test = () -> System.out.println("test");

        test.method();
    }
}
  • 반환 값이 있는 메서드의 경우 return 대신 expression 으로 대신할 수 있다. (expression인 경우 ; 를 붙이지 않는다.)
  • 람다식에 선언된 매개변수 타입은 추론이 가능한 경우 생략 가능(대부분 생략가능)
  • 매개 변수가 하나인 경우 ()를 생략할 수 있다.
  • {} 안 문장이 하나인 경우 생략할 수 있다.

1.3 함수형 인터페이스(Functional Interface)

함수형 인터페이스는 람다식을 다루기 위한 인터페이로 하나의 추상 메서드만 정의되어 있어야 한다. 단, static 메서드와 default 메서드의 개수에는 제약이 없다.

  • 함수형 인터페이스 타입의 매개변수 및 반환 타입이 함수형 인터페이스 타입이라면 람다식을 참조하는 참조변수를 매개변수로 지정하고 람다식을 가리키는 참조변수를 반환하거나 또는 람다식 자체를 반환할 수 있다.
  • 람다식은 Object 타입으로 형변환 할 수 없으며, 오직 함수형 인터페이스로만 형변환이 가능하다.
  • 람다식 내에서 참조하는 지역변수는 final이 붙어 있지 않아도 상수로 간주되며, 외부 지역변수와 같은 이름의 매개변수를 허용하지 않는다.
  • 함수형 인터페이스는 @FuntionalInterface 라는 어노테이션을 붙일 수 있다. (컴파일러에서 추상메서드를 갖춘 인터페이스인지 검사, javadoc 페이지에서 해당 인터페이스가 함수형 인터페이스임을 알 수 있도록 한다)

1.4 java.util.function패키지 (803p)

대부분의 메서드는 타입이 비슷하고 지네릭 메서드로 정의하면 반환 타입이 달라도 문제가 되지 않는다. java.util.function 패키지에는 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓았으며 매번 함수형 인터페이스를 정의하기 보다는 가능하면 이 패키지의 인터페이스를 활용한다.

  • 메서드 이름 통일
  • 재사용성
  • 유지보수

1.5 Function의 합성과 Predicate의 결합

  • Function의 합성 andThen : a.andThen(b) : a함수 적용 후 b함수 적용 compose : a.compose(b) : b함수 적용 후 a함수 적용 identity : 항등 함수 (잘 사용되지 않는 편이나 map()으로 변환 작업할 때 변환없이 그대로 처리하고자할 때 사용)

  • Predicate의 결합 and() : and 조건 or() : or 조건 negate() : not isEqual() : 두 대상 비교

1.6 메서드 참조

메서드를 참조해서 매개변수의 정보 및 리턴 타입을 알아내어 람다식에서 불필요한 매개 변수를 제거하는 것이 목적. 람다식의 매개 변수는 메서드의 매개값을 전달하는 역할만 하기 때문에 메서드 참조를 이용하면 깔끔하게 처리할 수 있다.

(a, b) -> Math.max(a, b);

Math::max
  • 하나의 메서드만 호출하는 람다식은 '클래스이름::메서드이름 또는 '참조변수::메서드이름'으로 바꿀 수 있다.
생성자의 메서드 참조
  • 생성자를 호출하는 람다식도 메서드 참조로 변환할 수 있다.
Supplier<MyClass> s = () -> new MyClass();
Supplier<MyClass> s = MyClass::new;

Function<Integer, MyClass> f = (i) -> new MyClass(i);
Function<Integer, MyClass> f2 = MyClass::new;

Function<Integer, int[]> f = x -> new int[x];
Function<Integer, int[]> f2 = int[]::new;

2. 스트림(stream)

2.1 스트림이란?

스트림이란

다양한 데이터 소스를 표준화된 방법으로 다루기 위한 라이브러리이다. 스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해놓았다.

스트림을 이용하면, 배열이나 컬렉션 뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있다.

[예제]

String[] strArr = {"aaa", "ddd", "ccc"};
List<String> strList = Arrys.asList(strArr);

문자열 배열 'strArr'과 같은 내용의 문자열을 저장하는 리스트 'strList'가 있을 때, 각각의 데이터를 정렬하고 출력하는 방법

방법1. 예전의 방법

Arrays.sort(strArr);
Collections.sort(strList);

for(String str : strArr) 
  System.out.println(str);

for(String str : strList)
  System.out.println(str);

방법2. 스트림을 활용한 방법

Stream<String> strStreamArr = Arrays.stream(strArr);
Stream<String> strStreamList = strList.stream();

strStremaArr.sorted().forEach(System.out::println);
strStreamList.sorted().forEach(System.out::println);

스트림을 사용한 코드가 더 간결하고 이해하기 쉬우면서 재사용성이 높다.

스트림의 연산

스트림이 제공하는 다양한 연산을 이용해서 복잡한 작업들을 간단하게 처리할 수 있다.

  1. 중간연산 : 연산결과가 스트림. 연속에서 수행 가능.
  2. 최종연산 : 연산결과가 스트림이 아님. 스트림의 요소를 소모하기 때문에 단 한번만 가능.

중간연산

모든 중간연산의 결과는 스트림이지만, 연산 전의 스트림과 같은 것은 아니다.

String[] strArr = {"dd", "aaa", "cc", "cc", "e"}

Stream<String> Stream = Stream.of(strArr);
Stream<String> distenctedStream = stream.distinct();

최종연산

최종연산 설명
void forEach(Consumer <? super T> action) 각 요소에 지정된 작업 수행
long count() 스트림의 요소 개수
Optional < T > max (Comparator <? super T> comparator) 스트림의 최댓값
Optional < T > min (Comparator <? super T> comparator) 스트림의 최솟값
Optional < T > findAny() 아무거나 하나
Optional < T > findFirst() 첫번째 요소
boolean allMatch(Pradicate < T > p) 모두 만족하는지?
boolean anyMatch(Pradicate < T > p) 하나라도 만족하는지?
boolean noneMatch(Pradicate < T > p) 모두 만족하지 않는지?
Object[] toArray() 스트림의 모든 요소를 배열로 return

reduce() : 스트림의 요소를 하나씩 줄여가면서(reducing) 계산한다.

collect() : 스트림의 요소를 수집한다. 주로 요소를 그룹화하거나 결과를 컬렉션에 담아 반환하는데 사용된다.

List<String> sortedList = strStreamList.sorted().collect(Collectors.toList());

스트림의 특징

특징1. 스트림은 데이터 소스를 변경하지 않는다.

스트림은 데이터 소스로부터 데이터를 읽기만 할 뿐, 데이터소스를 변경하지 않는다. 정렬된 결과가 필요할 경우, collect를 활용해서 컬렉션이나 배열에 담아 return할 수 있다.

List<String> sortedList = strStreamList.sorted().collect(Collectors.toList());

특징2. 스트림은 일회용이다.

스트림은 한번 사용하면 닫혀서 다시 사용할 수 없다. 필요하다면 스트림을 다시 생성해야 한다.

strStreamArr.sorted().forEach(System.out::println);
long numOfStr = strStreamArr.count();
[Error]
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed

특징3. 스트림은 작업을 내부 반복으로 처리한다.

내부 반복이란, 반복문을 메서드의 내부에 숨길 수 있다는 것을 의미한다. forEach() 는 스트림에 정의된 메서드 중의 하나로 매개변수에 대입된 람다식을 데이터소스의 모든 요소에 적용한다. 즉, forEach()는 메서드 안에 for문을 넣어버린 것이다.

//수행할 작업을 매개변수로 받는다.
strStreamArr.sorted().forEach(System.out::println);

특징4. 지연된 연산

스트림 연산에서는 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다. 스트림에 대해 sort()나 distinct()같은 중간 연산을 호출해도 즉각적으로 수행되지 않는다는 것이다.

중간연산을 호출하는 것은 단지 어떤 작업이 수행되어야하는지를 지정해주는 것일 뿐이다. 최종연산이 수행되어서야 스트림의 요소들이 중간연산을 거치고 최종연산에 소모된다.

특징5. 기본형 스트림

오토박싱, 언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 IntStream, LongStream, DoubleStream 이 제공된다.

일반적으로 Stream< Integer > 대신 IntStream을 사용하는 것이 더 효율적이고, IntStream에는 int타입으로 작업하는데 유용한 메서드들이 포함되어있다.

특징6. 병렬스트림

앞서 13장에서 fork&join framework으로 병렬처리하는 것에 대해 배웠는데, 스트림은 내부적으로 이 framework를 이용해서 연산을 자동적으로 병렬로 수행한다.

parallel() 메서드를 호출하면 병렬로 연산이 수행되고, sequential() 메서드를 호출하면 병렬로 처리되지 않게 된다. 모든 스트림은 기본적으로 병렬 스트림이 아니기 때문에 sequential() 메서드는 parallel()를 취소할 때만 사용한다.

int sum = strStream.parallel().mapToInt(s -> s.length()).sum();

2.2 스트림 만들기

컬렉션

Stream <T> Collection.stream()

컬렉션 클래스들은 모두 stream()메서드로 스트림을 생성할 수 있다. stream()은 해당 컬렉션을 소스로 하는 스트림을 반환한다.

List<Integer> list = Arrays.asList(1,2,3,4,5);
Stream<Integer> intStream = list.stream();

배열

String[] strArr = {"aaa", "ddd", "ccc"};

Stream<String> strStream = Stream.of(strArr); 
Stream<String> strStream = Arrays.stream(strArr);

특정범위의 정수

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

int보다 큰 범위의 스트림을 생성하려면 LongStream을 사용하면 된다.

난수

난수를 생성하는데 사용하는 Random클래스에는 아래와 같은 메서드들이 포함되어있다. 이 메서드들은 해당 타입의 난수들로 이루어진 스트림을 반환한다.

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

IntStream intStream = new Random().ints(5); // 크기가 5인 난수 스트림을 반환

람다식

Stream 클래스의 iterate(), generate()는 람다식을 매개변수로 받아서, 이 람다식에 의해 계산되는 결과값들을 요소로 하는 무한 스트림을 생성한다.

iterate() : 이전 결과에 대해 종속적

Stream<Integer> evenStream = Stream.iterate(0, n->n+2);
// 0, 2, 4, 6, ...

generate() : 이전 결과에 대해 독립적

Stream<Double> randomStream = Stream.generate(Math::random);
  • 이전 결과를 이용해서 다음 요소를 계산하지 않는다.

iterate()와 generate()에 의해 생성된 스트림은 아래와 같이 기본형 스트림 타입의 참조변수로 다룰 수 없다.

IntStream evenStream = Stream.iterate(0, n->n+2);  //error
DoubleStream randomStream = Stream.generate(Math::random);  //error

굳이 필요하다면, 아래와 같이 mapToInt()와 같은 메서드로 변환을 해야한다. (Stream, IntStream 변환)

IntStream evenStream = Stream.iterate(0, n->n+2).mapToInt(Integer::valueOf);
Stream<Integer> stream = evenStream.boxed();

파일

 Stream<Path>     Files.list(Path dir) // Path는 파일 또는 디렉토리

빈 스트림

요소가 하나도 없는 비어있는 스트림을 생성할 수도 있다. 스트림에 연산을 수행한 결과가 하나도 없을 때 null보다는 빈 스트림을 return하는 것이 낫다.

Stream emptyStream = Stream.empty();

두 스트림 연결

String[] str1 = {"123", "456" };
String[] str2 = {"aaa", "bbb", "cc"};

Stream<String> strs1 = Stream.of(str1);
Stream<String> strs2 = Stream.of(str2);
Stream<String> strs3 = Stream.concat(strs1, strs2);
123
456
aaa
bbb
cc

2.3 스트림의 중간연산

중간연산

중간연산 설명
Stream < T > distinct() 중복제거
Stream < T > filter(Predicate < T > predicate 조건에 안 맞는 요소 제외
Stream < T > limit(long maxSize) 스트림의 일부 잘라내기
Stream < T > skip(ling n) 스트림의 일부 건너뛰기
Stream < T > peek(Consumer< T > action) 스트림의 요소에 작업수행
Stream < T > sorted() 스트림의 요소 정렬

sorted() 는 지정된 Comparator로 스트림을 정렬하는데, int 값을 반환하는 람다식을 사용하는 것도 가능하다.

Stream<String> strStream = Stream.of("dd", "aaa", "CC", "cc", "b");
strStream.sorted().forEach(System.out::print);
CCaaabccdd

위의 문자열 스트림을 다양한 벙법으로 정렬한 후 forEach로 출력한 결과를 보여준다. String.CASE_INSENSITIVE_ORDER는 String클래스에 정의된 Comparator이다.

JDK1.8부터 Comparator인터페이스에 static 메서드와 디폴트 메서드가 많이 추가되었는데, 이 메서드들을 이용하면 정렬이 쉬워진다.

https://docs.oracle.com/javase/8/docs/api/java/util/Comparator.html

예를 들어 학생스트림을 반별, 성적순, 그리고 이름순으로 정렬하여 출력하려면 다음과 같이 한다.

student.Stream.sorted(Comparator.comparing(Student::getBan)
                                .thenComparing(Student::getScore)
                                .thenComparing(Student::getName)
                                .forEach(System.out::println);

변환 : map()

스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑다내거나 특정 형태로 변환해야될 때 사용한다.

Stream < R > map (Function < T, R > mapper)
DoubleStream mapToDouble (ToDoubleFunction < T > mapper)
IntStream mapToInt (ToIntFunction < T > mapper)
LongStream mapToLong (ToLongFunction < T > mapper)

mapToInt(), mapToLong(), mapToDouble()

스트림의 요소를 숫자로 반환하는 경우, IntStream같은 기본형 스트림으로 변환하는 것이 더 유용할 수 있다. 기본형 스트림은 숫자를 다루는 데 편리한 메서드들을 제공하기 때문이다.

IntStream studentScoreStream = studentStream.mapToInt(Student::getTotalScore);
int allTotalScore = studentScoreStream.sum(); // IntStream의 sum()

추가적으로 이 메서드들은 최종연산이기 때문에 호출 후 스트림이 닫힌다. 따라서 sum(), average() 를 연속해서 호출할 수 없다.

이러한 경우에 스트림을 또 생성해야하므로 불편하다. 그래서 summarystatics()라는 메서드가 따로 제공된다.

IntSummaryStatistics stat = scoreStream.summaryStatistics();

long totalCount = stat.getCount();
long totalScore = stat.getSum();
double avgScore = stat.getAverage();

mapToObj(), boxed()

반대로 IntStream을 Stream < T >로 변환할 때는 mapToObj()를, Stream로 변환할 때는 boxed()를 사용한다.

flatMap()

스트림의 요소가 배열이거나 map의 연산결과가 배열인경우, Stream < T [] >를 Stream < T >로 다루는 것이 더 편리할 때 flatMap()을 사용한다.

faltMap()은 map()과 달리 스트림의 스트림이 아닌 스트림으로 만들어준다.

Stream < R > flatMap (Function < T, Stream < R > > mapper)
DoubleStream flatMapToDouble (Function< T, DoubleStream > mapper)
IntStream flatMapToInt (Function< T, IntStream > mapper)
LongStream flatMapToLong (Function< T, LongStream > mapper)

2.4 Optional< T >와 OptionalInt

Optional은 지네릭클래스로, T타입의 객체를 감싸는 래퍼클래스이다. Optional 타입의 객체에는 모든 타입의 참조변수를 담을 수 있다.

최종 연산의 결과를 그냥 반환하는게 아니라 Optional객체에 담아서 반환을 하면, 반환된 결과가 null인지 매번 if문으로 체크하는 대신 Optional에 정의된 메서드를 통해 간단히 처리할 수 있다.

Optional 객체생성

String str = "abc";
Optional <String> optVal = Optional.of(str);

참조변수의 값이 null일 가능성이 있으면 of()대신 ofNullable()을 사용해야 한다. of()는 매개변수의 값이 null이면 NullPointerException을 발생시키기 때문이다.

Optional <String> optVal = Optional.of(null);         //Error
Optional <String> optVal = Optional.ofNullable(null); //OK

Optional < T > 참조변수의 초기화

Optional <String> optVal = null;                     //null로 초기화
Optional <String> optVal = Optional.<String>empty(); //빈 객체로 초기화

Optional 객체의 값 가져오기

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()

if(Optional.ofNullable(str).isPresent()) { 
  System.out.println(str);
}
  • Optional 객체의 값이 null이면 false, 아니면 true를 반환한다.

ifPresent()

Optional.ofNullable(str).ifPresent(System.out::println);

OptionalInt, OptionalLong, OptionalDouble

public final class OptionalInt {
  ...
  private final boolean isPresent; // 값이 저장되어 있으면 true
  private final int value; // int타입의 변수
}
  • 래퍼클래스별 값 가져오기

2.5 스트림의 최종연산

최종연산

최종연산 설명
void forEach(Consumer <? super T> action) 각 요소에 지정된 작업 수행
long count() 스트림의 요소 개수
Optional < T > max (Comparator <? super T> comparator) 스트림의 최댓값
Optional < T > min (Comparator <? super T> comparator) 스트림의 최솟값
Optional < T > findAny() 아무거나 하나
Optional < T > findFirst() 첫번째 요소
boolean allMatch(Pradicate < T > p) 모두 만족하는지?
boolean anyMatch(Pradicate < T > p) 하나라도 만족하는지?
boolean noneMatch(Pradicate < T > p) 모두 만족하지 않는지?
Object[] toArray() 스트림의 모든 요소를 배열로 return

reduce()

스트림의 요소를 하나씩 줄여가면서(reducing) 계산하고 최종결과를 반환한다. 처음 두 요소를 가지고 연산한 결과를 가지고 그 다음 요소와 연산한다. 이 과정에서 스트림의 요소를 하나씩 소모하게 되며, 스트림의 모든 요소를 소모하게 되면 그 결과를 반환한다.

Optional < T > reduce (BinaryOperator < T > accumulator)
int count = intStream.reduce(0, (a,b) -> a+1);
int sum = intStream.reduce(0, (a,b) -> a+b);

앞서 소개한 최종연산 count(), sum() 등은 내부적으로 모두 reduce를 이용해서 작성된 것이다.

2.6 collect()

collect()

스트림의 요소를 수집한다. 주로 요소를 그룹화하거나 결과를 컬렉션에 담아 반환하는데 사용된다.

List<String> sortedList = strStreamList.sorted().collect(Collectors.toList());

Collect(), Collector, Collectors

Collect()

Collector를 매개변수로 하는 스트림의 최종 연산이다. 스트림연산에서 요소를 그룹화하거나 결과를 컬렉션에 담아 반환할 때 사용된다.

Object collect (Collector collector)

Collector

collect에 필요한 메서드를 정의해놓은 인터페이스이다.

public interface Collector <T, A, R> {     
//T(요소)를 A에 누적한 다음 결과를 R로 변환해서 return

  Supplier < A >   supplier();   
  //StringBuilder::new   
  //결과를 저장할 공간(A)을 제공

  BiConsumer < A, T > accumulator();   
  //(sb, s) -> sb.append(s) 
  //스트림의 요소를 수집(collect)할 방법을 제공

  BinaryOperator < A > combiner();   
  //(sb1, sb2) -> sb1.append(sb2) 
  //두 저장공간(A)을 결합할 방법을 제공(병렬 스트림)

  Function < A, R > finisher();   
  //sb -> sb.toString()  
  //최종변환
  //변환할 필요가 없는 경우, x->x

  Set<Characteristics> characteristics;  
  //컬렉터의 특성이 담긴 set return
}

Collectors

다양한 기능의 메소드를 제공한다.

변환 : mapping(), toList(), toSet(), toMap(), toCollection(), ...

ArrayList<String> list = names.stream().collect(Collectors.toCollection(ArrayList::new)); 
// Stream<String> → ArrayList<String>

통계 : counting(), summingInt(), averagingInt(), maxBy(), minBy(), summarizingInt(), ...

long totalScore = stuStream.mapToInt(Student::getTotalScore).sum(); 
// IntStream의 sum()
long totalScore = stuStream.collect(summingInt(Student::getTotalScore));
// Collectors의 summingInt

문자열 결합 : joining()

String studentNames = stuStream.map(Student::getName).collect(joining());

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

리듀싱 : reducing()

Collector reducing(BinaryOperator<T> op)
Collector reducing(T identity, BinaryOperator<T> op)
Collector reducing(U identity, Function<T,U> mapper, BinaryOperator<U> op)
//map + reduce
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));

그룹화와 분할 : partitioningBy(), groupingBy(), collectingAndThen()

  • prtitioningBy() : 스트림의 요소를 2분할 한다.
Map < Boolean, Long > stuNumBySex = stuStream.collect(partitioningBy(Student::isMale)); // 학생들을 성별로 분할

List<Student> maleStudent = stuBySex.get(true); // Map에서 남학생 목록을 얻는다. List<Student> femaleStudent = stuBySex.get(false); // Map에서 여학생 목록을 얻는다.
  • groupingBy() : 스트림의 요소를 그룹화한다.
Map<Integer, List<Student>> stuByBan = 
stuStream.collect(groupingBy(Student::getBan, toList())); // 학생을 반별로 그룹화
//다중 그룹화
Map<Integer, Map<Integer, List<Student>>> stuByHakAndBan = 
stuStream.collect(groupingBy(Student::getGrade), 
          groupingBy(Student::getBan))
));

2.7 Collector구현하기

Collector인터페이스를 구현하는 클래스를 작성한다.

public interface Collector < T, A, B > {
  //T(요소)를 A에 누적한 다음, 결과를 R로 변환해서 return

  Supplier < A >   supplier();   
  //StringBuilder::new   
  //결과를 저장할 공간(A)을 제공

  BiConsumer < A, T > accumulator();   
  //(sb, s) -> sb.append(s) 
  //스트림의 요소를 수집(collect)할 방법을 제공

  BinaryOperator < A > combiner();   
  //(sb1, sb2) -> sb1.append(sb2) 
  //두 저장공간(A)을 결합할 방법을 제공(병렬 스트림)

  Function < A, R > finisher();   
  //sb -> sb.toString()  
  //최종변환
  //변환할 필요가 없는 경우, x->x

  Set<Characteristics> characteristics;  
  //컬렉터의 특성이 담긴 Set return
}

characteristics()

컬렉터가 수행할 작업의 속성정보를 제공한다.

Characteristics.CONCURRENT      //병렬로 처리할 수 있는 작업
Characteristics.UNORDERED       //스트림 요소의 순서가 유지될 필요가 없는 작업
public Set < Characteristics > characteristics() {
  return Collections.unmodifiableSet(EnumSet.of(
      Collector.Characteristics.CONCURRENT, Collector.Characteristics.UNORDERED   ));
}

지정할 특성이 없으면 빈 set을 return한다.

return Collections.emptySet();

Example. ConcatCollector

문자열 스트림의 모든 요소를 연결하는 컬렉터이다.

class ConcatCollector implements Collector<String, StringBuilder, String> { 
   public Supplier< StringBuilder > supplier() {
     return () -> new StringBuilder(); // return StringBuilder::new;
   }

   public BiConsumer< StringBuilder, String > accumulator() {
     return (sb, s) -> sb.append(s);
   }

   public Function< StringBuilder, String > supplier() {
     return sb -> new StringBuilder(); 
   }

   public BinaryOperator< StringBuilder > combiner() {
     return (sb1, sb2) -> sb1.append(sb2);
   }

   public Set< Characteristics > characteristics() {
     return Collections.emptySet();
   }
public static void main(String[] args) {
  String[] strArr = { "aaa","bbb","ccc" };
  Stream<String> strStream = Stream.of(strArr);
  String result = strStream.collect(new ConcatCollector()); 

  System.out.println("result="+result); 
  //result=aaabbbccc
}

2.8 스트림의 변환