람다와 스트림

우수민·2021년 11월 8일
0
post-thumbnail

1. 람다식(Lambda expression)

  • 람다식의 도입으로 인해, 이제 자바는 객체지향언어인 동시에 함수형 언어가 되었다.

1.1 람다식이란?

  • 람다식(Lambda expression)은 간단히 말해서 메서드를 하나의 '식(expression)'으로 표현한 것이다. 람다식은 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해준다.
  • 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 '익명 함수(anonymous function)'이라고도 한다.
int[] arr = new int[5];
Arrays.setAll(arr, (i)->(int)(Math.random()*5)+1);

// 위와 동일 한식
int method(){
    return (int)(Math.random()*5)+1;
}
  • 람다식은 메서드의 매개변수로 전달되어지는 것이 가능하고, 메서드의 결과로 반환될 수도 있다. 람다식으로 인해 메서드를 변수처럼 다루는 것이 개장하다.

메서드는 함수와 같은 의미이지만, 특정 클래스에 반드시 속해야 한다는 제약이 있기 때문에 기존의 함수와 같은 의미의 다른 용어를 선택해서 사용한다. 그러나 이제 다시 람다식을 통해 메서드가 하나의 독립적인 기능을 하리 때문에 함수라는 용어가 사용되게 된다.

1.2 람다식 작성하기

  • 람다식은 '익명 함수'답게 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 통{} 사이에 '->'를 추가한다.
반환 타입 메서드 이름(매개변수 선언){
	문장들
}

-> 변경 ->

(매개변수 선언) -> { 문장들 }

// 예시 (변경 전)
int max(int a, int b){
    return a>b ? a : b;
}
// 예시 (변경 후, 전부 가능)
(int a, int b) ->  a>b ? a : b
(int a, int b) ->  { return a>b ? a : b;}
(a, b) ->  a>b ? a : b

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

  • 람다식은 익명 객체와 동등하다.
MyFunction f = (int a, int b) -> a>b ? a : b; // 익명 객체를 람다식으로 대체
int big = f.max(5,3); // 익명 객체의 메서드를 호출
  • 이처럼 MyFunction인터페이스를 구현한 익명 객체를 람다식으로 대체가 가능한 이유는, 람다식도 실제로는 익명 객체이고, MyFunction인터페이스를 구현한 익명 객체의 메서드 max()와 람다식의 매개변수의 타입과 개수 그리고 반환값이 일치하기 때문이다.
  • 위의 내용처럼, 하나의 메서드가 선언된 인터페이스를 정의해서 람다식을 다루는 것은 기존의 자바의 규칙들을 어기지 않으면서도 자연스럽다. 그래서 인터페이스를 통해 람다식을 다루기로 결정되었으며, 람다식을 다루기 위한 인터페이스를 '함수형 인터페이스(functional interface)'라고 부른다.
@FunctionalInterface
interface MyFunction { // 함수형 MyFunction을 정의
    public abstract int max(int a, int b);
}
  • 단, 함수형 인터체이스에는 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있다. 그래야 람다식과 인터페이스의 메서드가 1:1로 연결될 수 있기 때문이다. 반면에 static메서드와 default메서드의 개수에는 제약이 없다.

    '@FunctionalInterface'를 붙이면, 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인해주므로, 꼭 붙이는 것이 좋다.

함수형 인터페이스 타입의 매개변수와 반환타입

  • 함수형 인터페이스 MyFunction이 아래와 같이 정의되어 있을때, 메서드의 매개변수가 MyFunction타입이면, 이 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 저장해야 한다는 뜻이다.
@FunctionalInterface
interface MyFunction{
    void myMethod(); // 추상 메서드
}

void aMethod(MyFunction f){ // 매개변수의 타입이 함수형 인터페이스
    f.myMethod(); // MyFunction에 정의된 메서드 호출
}
	...
    
MyFunction f = () -> System.out.println("myMethod()");
aMethod(f);

// 또한 참조변수 없이 아래와 같이 직접 람다식을 매개변수로 지정하는 것도 가능하다.
aMethod(() -> System.out.println("myMethod()")); // 람다식을 매개변수로 지정
  • 그리고 메서드의 반환타입이 함수형 인터체이스타입이라면, 이 함수형 인터페이스의 추상메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나 람다식을 직접 반환할 수 있다.
  • 람다식을 참조변수로 다룰 수 있다는 것은 메서드를 통해 람다식을 주고받을 수 있다는 것을 의미한다. 즉, 변수처럼 메서드를 주고받는 것이 가능해진 것이다.
@FunctionalInterface
interface MyFunction{
    void run(); // public abstract void run();
}

class LambdaEx1{
    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(){
                System.out.println("f2.run()");
            }
        }
        
        MyFunction f3 = getMyFunction();
        
        f1.run();
        f2.run();
        f3.run();
        
        execute(f1);
        execute(()->System.out.println("run()"));
    }
}

람다식의 타입과 형변환

  • 함수형 인터페이스로 람다식을 참조할 수 있는 것일 뿐, 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아니다.람다식은 익명 객체이고 익명 객체는 타입이 없다.
  • 정확히는 타입은 있지만 컴파일러가 임의로 이름을 정하기 때문에 알 수 없는 것이다. 그래서 대입 연산자의 양변의 타입을 일치시키기 위해 아래와 같이 형변환이 필요하다.
MyFunction f = (MyFunction)(()->{}); // 양변의 타입이 다르므로 형변환이 필요
  • 람다식은 MyFunction인터페이스를 직접 구현하지 않았지만, 이 인터페이스를 구현한 클래스의 객체와 완전히 동일하기 때문에 위와 같은 형변환을 허용한다. 그리고 이 형변환은 생각 가능하다.
  • 람다식은 이름이 없을 뿐 분명히 객체인데도, 아래와 같이 Object타입으로 형변환할 수 있다. 람다식은 오직 함수형 인터페이스로만 형변환이 가능하다.
Object obj = (Object)(()->{}); // 에러. 함수형 인터페이스로만 형변환 가능

외부 변수를 참조하는 람다식

  • 람다식도 익명 객체, 즉 익명 클래스의 인스턴스이므로 람다식에서 외부에 선언된 변수에 접근하는 규칙은 앞서 익명 클래스에서 배운 것과 동일하다.

1.4 java.util.function패키지

  • 대부분의 메서드는 타입이 비슷하다. 매개변수가 없거나 한 개 또는 두 개, 반환 값은 없거나 한 개. 게다가 지네릭 메서드로 정의하면 매개변수나 반환 타입이 달라도 문제가 되지 않는다. 그래서 java.util.function패키지에 이랍ㄴ적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 정의해 두었다.

조건식의 표현에 사용되는 Predicate

  • Predicate는 Function의 변형으로, 반환타입이 boolean이라는 것만 다르다. Predicate는 조건식을 람다식으로 표현하는데 사용된다.
Predicate<String> isEmptyStr = s -> s.length() == 0;
String s = "";

if (isEmptyStr.test(s)) // if(s.length() == 0)
    System.out.println("This is an empty String.");

매개변수가 두 개인 함수형 인터페이스

  • 매개변수의 개수가 2개인 함수형 인터페이스는 이름 앞에 접두사 'Bi'가 붙는다.

매개변수의 타입으로 보통 'T'의 다음 문자인 'U', 'V', 'W'를 매개변수의 타입으로 사용하는 것일 뿐 별다른 의미는 없다.

  • 두 개 이상의 매개변수를 갖는 함수형 인터페이스가 필요하다면 직접 만들어서 써야한다. 만일 3개의 매개변수를 갖는 함수형 인터페이스를 선언한다면 다음과 같을 것이다.
@FunctionalInterface
interface TriFunction<T, U, V, R>{
    R apply(T t, U u, V v)
}

UnaryOperator와 BinaryOperator

  • Function의 또 다른 변형으로 UnaryOperator와 BinaryOperator가 있는데, 매개변수의 타입과 반환 타입의 타입이 모두 일치한다는 점만 제외하고는 Function과 같다.

컬렉션 프레임웍과 함수형 인터페이스

  • 컬렉션 프레임웍의 인터페이스에 다수의 디폴트 메서드가 추가되었는데, 그 중의 일부는 함수형 인터페이스를 사용한다. 다음은 그 메서드들의 목록이다.
import java.util.*;

class LambdaEx4{
    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.println(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.println("{"+k+","+v+"},"));
        System.our.println();
    }
}
import java.util.function.*;
import java.util.*;

class LambdaEx5{
    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; // i의 일의 자리를 없앤다.
        List<Integer> list = new ArrayList<>();
        makeRandomList(s, list);
        System.out.println(list);
        printEventNum(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.println("[");
       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());
   }
}

1.5 Function의 합성과 Predicate의 결합

원래 Function인터페이스는 반드시 두개의 타입을 지정해 줘야 하기 때문에, 두 타입이 같아도 Function< T >라고 쓸수 없다. Function<T,T>라고 써야 한다.

  • Function
default <V> Function<T,V> andThen(Function<? super R,? extends V> after)
default <V> Function<V,R> compose(Function<? super V,? extends T> before)
static <T> Function<T,T> identity() 
  • Predicate
default Predicate<T> and(Predicate<? super T> other)
default Predicate<T> or(Predicate<? super T> other)
default Predicate<T> negate()
static <T> Predicate<T> isEqual(Object targetRef)

1.6 메서드 참조

  • 람다식으로 메서드를 이처럼 간결하게 표현할 수 있는데 더욱 간결하게 표현할 수 있는 방법이 있다.
  • 항상 그런 것은 아니고, 람다식이 하나의 메서드만 호출하는 경우에는 '메서드 참조(method reference)'라는 방법으로 람다식을 간략히 할 수 있다.
  • 예를 들어 문자열을 정수로 변환하는 람다식은 아래와 같이 작성할 수 있다.
Function<String, Interger> f = (String s) -> Integer.parseInt(s);

// 보통은 이렇게 람다식을 작성하는데, 이 람다식을 메서드로 표현하면 아래와 같다.
Interger wrapper(String s){ // 이 메서드의 이름은 의미없다.
    return Integer.parseInt(s);
}

// 더 간략히 표현하면 아래와 같다.
Function<String, Integer> f = Integer::parseInt; //메서드 참조

하나의 메서드만 호출하는 람다식은 '클래스이름::메서드이름' 또는 '참조변수::메서드이름'으로 바꿀 수 있다.

생성자의 메서드 참조

  • 생성자를 호출하는 람다식도 메서드 참조로 변환할 수 있다.
Supplier<MyClass> s = () -> new MyClass(); // 람다식
Supplier<MyClass> s = () -> MyClass::new; // 람다식

2. 스트림(stream)

2.1 스트림이란?

  • 스트림은 데이터 소스를 추상화하고, 데이터를 다루는 데 자주 사용되는 메서드를 정의해 놓았다. 데이터 소스를 추상화하였다는 것은, 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 되었다는 것과 코드의 재사용성이 높아진다는 것을 의미한다.
  • 스트림을 이용하면, 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있다.

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

  • 스트림은 데이터 소스로 부터 데이터를 읽기만할 뿐, 데이터 소스를 변경하지 않는다는 차이가 있다. 필요하다면, 정렬된 결과를 컬렉션이나 배열에 담아서 반환할 수도 있다.
List<String> sortedList = strStream2.sorted().collect(Collectors.toList());

스트림은 일회용이다.

  • 스트림은 Iterator처럼 일회용이다. Iterator로 컬렉션의 요소를 모두 읽고 나면 다시 사용할 수 없는 것처럼, 스트림도 한번 사용하면 닫혀서 다시 사용할 수 없다. 필요하다면 스트림을 다시 생성해야 한다.

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

  • 스트림을 이용한 작업이 간결할 수 있는 비결중의 하나가 바로 '내부 반복'이다. 내부 반복이라는 것은 반복문을 메서드의 내부에 숨길 수 있다는 것을 의미한다. forEach()는 스트림에 정의된 메서드 중의 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용한다.
stream.forEach(System.out::println);
//System.out::println == (str)->System.out.println(str)

스트림의 연산

  • 스트림이 제공하는 다양한 연산을 이용해서 복잡한 작업들을 간단히 처리할 수 있다. 마치 데이터베이스에 SELECT문으로 질의하는 것과 같은 느낌이다.
  • 스트림이 제공하는 연산은 중간 연산과 최종 연산으로 분류할 수 있는데, 중간 연산은 연산결과를 스트림으로 반환하기 때문에 중간 연산을 연속해서 연결할 수 있다. 반면에 최종 연산은 스트림의 요소를 소모하면서 연산을 수행하므로 단 한번만 연산이 가능하다.

중간 연산 : 연산 결과가 스트림인 연산. 스트림에 연속해서 중간 연산할 수 있음
최종 연산 : 연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 단 한번만 가능

지연된 연산

  • 스트림 연산에서 한 가지 중요한 점은 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다는 것이다. 스트림에 대해 distinct()나 sort()같은 중간 연산을 호출해도 즉각적인 연산이 수행되는 것은 아니라는 것이다.
  • 중간 연산을 호출하는 것은 단지 어떤 작업이 수행되어야하는지를 지정해주는 것일 뿐이다. 최종 연산이 수행되어야 비로소 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모된다.

Stream< Interger >와 IntStream

  • 요소의 타입이 T인 스트림은 기본적으로 Stream< T >이지만, 오토박싱&언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림, IntStream, LongStream, DoubleStream이 제공된다.
  • 일반적으로 Stream< Integer >대신 IntStream을 사용하는 것이 더 효율적이고, Instream에는 int타입의 값으로 작업하는데 유용한 메서드들이 포함되어 있다.

병렬 스트림

  • 스트림으로 데이터를 다룰 때의 장점 중 하나가 바로 병렬 처리가 쉽다는 것이다.
    • parallel() : 병렬로 연산은 수행하도록 지시
    • sequential() : 병렬로 처리되지 않도록 지시

parallel()과 Sequential()은 새로운 스트림을 생성하는 것이 아니라, 그저 스트림의 속성을 변경할 뿐이다.

int sum = strStream.parallel() // strStream을 병렬 스트림으로 전환
                   .mapToInt(s->s.length())
                   .sum();

2.2 스트림 만들기

컬렉션

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

배열

-배열을 소스로 하는 스트림을 생성하는 메서드는 다음와 같이 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);

특정 범위의 정수

  • IntStream과 LongStream은 다음과 같이 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 range()와 rangeClosed()를 가지고 있다.

IntStream IntStream.range(int begin, int end)
IntStream IntStream.rangeClosed(int begin, int end)

  • range()의 경우 경계의 끝인 end가 범위에 포함되지 않고, rangeClosed()의 경우에는 포함된다.
  • int보다 큰 범위의 스트림을 생성하려면 LongStream에 있는 동일한 이름의 메서드를 사용하면 된다.

임의의 수

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

  • 이 메서드들이 반환하는 스트림은 크기가 정해지지 않은 '무한 스트림(infinite stream)'이므로 limit()도 같이 사용해서 스트림의 크기를 제한해 주어야 한다. limit()은 스트림의 개수를 지정하는데 사용되며, 무한 스트림을 유한 스트림으로 만들어 준다.

람다식 - iterate(), generate()

  • Stream클래스의 iterate()와 generate()는 람다식을 매개변수로 받아서, 이 람다식에 의해 계산되는 값들을 요소하는 무한 스트림을 생성한다.
  • iterate()는 seed으로 지정된 값부터 시작해서, 람다식 f에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복한다.
  • generate()도 iterate()처럼, 람다식에 의해 계산되는 값을 요소로 하는 무한 스트림을 생성해서 반환하지만, iterate()와 달리, 이전 결과를 이용해서 다음 요소를 계산하지 않는다.

빈 스트림

  • 요소가 하나도 없는 비어있는 스트림을 생성할 수도 있다. 스트림에 연산을 수행한 결과가 하나도 없을 때, null보다 빈 스트림을 반환하는 것이 낫다.
Stream emptyStream = Stream.empty();
long count = emptyStream.count();

2.3 스트림의 중간연산

스트림 자르기 - skip(), limit()

IntStream intStream = IntStream.rangeClosed(1, 10);
intStream.skip(3).limit(5).forEach(System.out::println);

스트림의 요소 걸러내기 - filter(), distinct()

정렬 - sorted()

변환 - map()

조희 - peek()

  • 연산과 연산 사이에 올바르게 처리되었는지 확인하고 싶다면, peek()를 사용할 수 있다. forEach()와 달리 스트림의 요소를 소모하지 않으므로 연산 사이에 여러번 끼워 넣어도 문제가 되지 않는다.

mapToInt(), mapToLong(), mapToDouble()

  • map()은 연산의 결과로 Stream< T >타입의 스트림을 반환하는데, 스트림의 요소를 숫자로 변환하는 경우 IntStream과 같은 기본형 스트림으로 변환한는 것이 더 유용할 수 있다.

flatMap() - Stream<T[]>를 Stream< T >로 변환

  • 스트림의 요소가 배열이거나 map()의 연산결과가 배열인 경우, 즉 스트림의 타입이 Stream<T[]>인 경우, Stream< T >로 다루는 것이 더 편리할 때가 있다. 그럴 때는 map() 대신 flatMap()을 사용한다.
import java.util.*;
import java.util.stream.*;

class StreamEx4{
    Stream<String[]> strArrStrm = Stream.of{
        new String[] {"abc", "def", "jkl"},
        new String[] {"ABC", "DEF", JKL""}
    };
    
//  Stream<Stream<String>> strStrmStrm = strArrStrm.map(Arrarys::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();
    
    Stream<String> strStrm1 = Stream.of("AAA", "ABC", "bBb", "Dd");
    Stream<String> strStrm2 = Stream.of("bbb", "aaa", "ccc", "dd");
    
    Stream<Stream<String>> strStrmStrm = Stream.of(strStrm1, strStrm2);
    Stream<String> strStream = strStrmStrm
                                    .map(s -> s.toArray(String[]::new))
                                    .flatMap(Arrays::stream);
    strStream.map(String::toLowerCase)
             .distinct()
             .forEach(System.out::println);
    }
}

2.4 Optional< T >와 OptionalInt

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

Optional객체 생성하기

  • Optional객체를 생성할 때는 of() 또는 ofNullable()을 사용한다.

Optional객체의 값 가져오기

  • Optional객체에 저장된 값을 가져올 때는 get()을 사용한다. 값이 null일 때는 NoSuchElementException이 발생하며, 이를 대비해서 orElse()로 대체할 값을 지정할 수 있다.

OptionalInt, OptionalLong, OptionalDouble

  • IntStream과 같은 기본형 스트림에는 Optional도 기본형을 값으로 하는 OptionalInt, OptionalLong, OptionalDouble을 반환한다.

2.5 스트림의 최종 연산

  • 최종 연산은 스트림의 요소를 소모해서 결과를 만들어낸다. 그래서 최종 연산후에는 스트림이 닫히게 되고 더 이상 사용할 수 없다. 최종 연산의 결과는 스트림 요소의 합과 같은 단일 값이거나, 스트림의 요소가 담긴 배열 또는 컬렉션일 수 있다.

조건 검사 - allMatch(), anyMatch(), noneMatch(), findFirst(), findAny()

  • 스트림의 요소에 대해 지정된 조건에 모든 요소가 일치하는 지, 일부가 일치하는지 아니면 어떤 요소도 일치하지 않는지 확인하는데 사용할 수 있는 메서드들이다. 이 메서드들은 모두 매개변수로 Predicate를 요구하며, 연산결과로 boolean을 반환한다.

통계 - count(), sum(), average(), max(), min()

  • IntStream과 같은 기본형 스트림에는 스트림의 요소들에 대한 통계정보를 얻을 수 있는 메서드들이 있다. 그러나 기본형 스트림이 아닌 경우에는 통계와 관련된 메서드들이 아래의 3개뿐이다.

기본형 스트림의 min(), max()와 달리 매개변수로 Comparator를 필요로 한다는 차이가 있다.

리듀싱 - reduce()

  • 스트림의 요소를 줄어나가면서 연산을 수행하고 최종결과를 반환한다. 그래서 매개변수의 타입이 BinaryOperator< T >인 것이다. 처음 두 요소를 가지고 연산한 결과를 가지고 그 다음 요소와 연산한다. 이 과정에서 스트림의 요소를 하나씩 소모하게 되며, 스트림의 모든 요소를 소모하게 되면 그 결과를 반환한다.

2.6 collect()

  • 스트림의 최종 연산중에서 가장 복잡하면서 유용하게 활용될 수 있는 것이 collect()이다.
  • collect()는 스트림의 요소를 수집하는 최종 연산으로 리듀싱(reducing)과 유사하다.
  • collect()가 스트림의 요소를 수집하려면, 어떻게 수집할 것인가에 대한 방법이 정의되어 있어야 하는데, 이 방법을 정의한 것인 바로 컬렉터(collector)이다.
  • 컬렉터는 Collector인터페이스를 구현한 것으로, 직접 구현할 수도 있고 미리 작성된 것을 사용할 수도 있다.

collect() : 스트림의 최종연산, 매개변수로 컬렉터를 필요로 한다.
Collector : 인터페이스, 컬렉터는 이 인터체이스를 구현해야 한다.
Collectors : 클래스, static메서드로 미리 작성된 컬렉터를 제공한다.

sort() 할 때, Comparator가 필요한 것처럼 collect()할 때는 Collector가 필요하다.

스크림을 컬렉션과 배열로 변환 - toList(), toSet(), toMap(), toCollection(), toArray()

  • 스트림의 모든 요소를 컬렉션에 수집하려면, Collectors클래스의 toList()와 같은 메서드를 사용하면 된다. List나 Set이 아닌 특정 컬렉션을 지정하려면, toCollection()에 해당하는 컬렉션의 생성자 참조를 매개변수로 넣어주면 된다.
List<String> names = stuStream.map(Student::getName)
                              .collect(Collector.toList());
ArrayList<String> list = names.stream() 
                              .collect(Collectors.toCollection(ArrayList::new));

// Map은 키와 쌍으로 저장해야하므로 객체의 어떤 필드를 키로 사용할지와 값으로 사용할지를 지정해줘야함
Map<String, Person> map = personStream
                              .collect(Collectors.toMap(p->p.getRegId(), p->p));

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

  • 앞서 나온 최종 연산들이 제공하는 통계 정보를 collect()로 똑같이 얻을 수 있다.

리듀싱 - reducing()

문자열 결합 - joining()

  • 문자열 스트림의 모든 요소를 하나의 문자열로 연결해서 반환한다. 구분자를 지정해줄 수도 있고, 접두사와 접미사도 지정가능하다.
  • 스트림의 요소가 String이나 StringBuffer처럼 CharSequence의 자손인 경우에만 결합이 가능하므로 스트림의 요소가 문자열이 아닌 경우에는 먼저 map()을 이용해서 스트림의 요소를 문자열로 반환해야 한다.

그룹화의 분할 - groupingBy(), partitioningBy()

  • 그룹화는 스트림의 요소를 특정 기준으로 그룹화하는 것을 의미하고, 분할은 스트림의 요소를 두 가지, 지정된 조건에 일치하는 그룹과 일치하지 않는 그룹으로의 분할을 의미한다. 아래의 메서드 정의에서 알 수 있듯이, groupingBy()는 스트림의 요소를 Function으로, partitioningBy()는 Predicate로 분류한다.
Collector groupingBy(Function classifier)
Collector groupingBy(Function classifier, Collector downstream)
Collector groupingBy(Function classifier, Supplier mapFactory, Collector downstream)

Collctor partitioningBy(Predicate predicate)
Collctor partitioningBy(Predicate predicate, Collector downstream)
  • 스트림을 두개의 그룹으로 나눠야 한다면, 당연히 partitioningBy()로 분할하는 것이 빠르다. 그 외는 groupingBy()를 쓰면 된다.

2.7 Collector구현하기

  • 컬렉터를 작성한다는 것은 Collector인터페이스를 구현한다는 것을 의미하는데, Collector인터페이스는 다음과 같이 정의되어 있다.
public interface Collector<T, A, R>{
    Supplier<A> supplier();
    BiConsumer<A, T> accumulator();
    BinaryOperator<A> combiner();
    Function<A, R> finisher();
    
    Set<Characteristics> characteristics(); // 컬렉터의 특성이 담긴 Set을 반환
    ...
}
  • 직접 구현해야 하는 것은 위의 5개의 메서드인데, characteristics()를 제외하면 모두 반환 타입이 함수형 인터페이스이다. 즉, 4개의 람다식을 작성하면 되는 것이다.

supplier() : 작업 결과를 저장할 공간을 제공
accumulator() : 스트림의 요소를 수집(collect)할 방법을 제공
combiner() : 두 저장공간을 병합할 방법을 제공(병렬 스트림)
finisher() : 결과를 최종적으로 변환할 방법을 제공

public Function finisher(){
    return Function.identity(); // 항등 함수를 반환
}
  • 마지막으로 characteristics()는 컬렉터가 수행하는 작업의 속성에 대한 정보를 제공하기 위한 것이다.

Characteristics.CONCURRENT : 병렬로 처리할 수 있는 작업
Characteristics.UNORDERED : 스트림의 요소의 순서가 유지될 필요가 없는 작업
Characteristics.IDENTITY_FINISH : finisher()가 항등 함수인 작업


  • 참고 : 자바의 정석 3판
profile
데이터 분석하고 있습니다

0개의 댓글