반응형

CompletableFuture와 리액티브 프로그래밍 컨셉의 기초

  • Thread, Future, 자바가 풍부한 동시성 API를 제공하도록 강요하는 진화의 힘
  • 비동기 API
  • 동시 컴퓨팅의 박스와 채널 뷰
  • CompletableFuture 콤비네이터로 박스를 동적으로 연결
  • 리액티브 프로그래밍용 자바 9플로 API의 기초를 이루는 발행 구독 프로토콜
  • 리액티브 프로그래밍과 리액티브 시스템

15.1 동시성을 구현하는 자바 지원의 진화

멀티코어 CPI에서 효과적으로 프로그래밍을 실행할 필요성이 커지면서 이후 자바 버젼에서는 개선된 동시성 지원이 추가됨.

자바는 Future를 조합하는 기능을 추가하면서 동시성을 강화

 

15.1.2 Executor와 스레드 풀

자바5는 Executor 프레임워크와 스레드 풀을 통해 스레드의 힘을 높은 수준으로 끌어올리는 즉 자바 프로그래머가 태스크 제출과 실행을 분리할 수 있는 기능 제공.

 

스레드의 문제

자바 스레드는 직접 운영체제 스레드에 접근한다. 운영체제가 지원하는 스레드 수를 초과해 사용하면 자바 애플리케이션이 예상치 못한 방식으로 크래시될 수 있으므르 기존스레드가 실행되는 상태에서 계속 새로운 스레드를 만드는 상황이 일어나지 않도록 주의해야 한다.

 

스레드 풀 그리고 스레드 풀이 더 좋은이유

자바 ExecutorService 는 태스크를 제출하고 나중에 결과를 수집할 수 있는 인터페이스를 제공한다.

워커스레드라 불리는 nThreads를 포함하는 ExecutorService 를 만들고 이들을 스레드 풀에 저장한다. 스레드 풀에서 사용하지 않은 스레드로 제출된 태스크를 먼저 온 순서대로 실행한다. 태스크 실행이 종료되면 이들 스레드를 풀로 반환한다.

장점은 하드웨어에 맞는 수의 태스크를 유지함과 동시에 수 천개의 태스크를 스레드 풀에 아무 오버헤드 없이 제출할 수 있다.

태스크(Runnable 이나 Callable) 를 제공하면 스레드가 이를 실행

 

스레드 풀 그리고 스레드 풀이 나쁜 이유

잠을 자거나 I/O를 기다리거나 네트워크 연결을 기다리는 태스크가 있다면 주의.

블록(자거나 이벤트를 기다리는)할 수 있는 태스크는 스레드 풀에 제출하지 말아야 한다.

 

15.1.3 스레드의 다른 추상화 : 중첩되지 않은 메서드 호출

  • 스레드 실행은 메서드를 호출한 다음의 코드와 동시에 실행되므로 데이터 경쟁 문제를 일으키지 않도록 주의해야한다.
  • 기존 실행 중이던 스레드가 종료되지 않은 상황에서 자바의 main() 메서드가 반환하면 

    - 애플리케이션을 종료하지 못하고 모든 스레드가 실행을 끝낼 때까지 기다린다.

    - 애플리케이션 종료를 방해하는 스레드를 강제종료 시키고 애플리케이션을 종료한다.

 

15.2 동기 API와 비동기 API

자바 5의 Future는 자바 8의 CompletableFuture로 이들을 조합하여 비동기 구현 가능

 

15.2.3 잠자기 는 해로운것으로 간주

10초 동안 워커 스레드를 점유한 상태에서 아무것도 안하는 코드

work1();
Thread.sleep(1000);
work2();

다른 작업이 실행될 수 있도록 허용하는 코드(스레드를 사용할 필요가 없이 메모리만 조금 더 사용)

work1();
scheduledExecutorService.schedule(ScheduledExecutorServiceExample::work2, 10, TimeUnit.SECONDS);

태스크가 실행되면 귀중한 자원을 점유하므로 태스크가 끝나서 자원을 해제하기 전까지 태스크를 계속 실행해야 한다.

태스크를 블록하는 것보다는 다음 작업을 태스크로 제출하고 현재 태스크는 종료하는 것이 바람직.

반응형
반응형

디폴트 메서드

자바8은 기본 구현을 포함하는 인터페이스를 정의하는 두 가지 방법을 제공한다.

  • 정적 메서드(static method)
    • 인터페이스 내부에 정적 메서드 사용
  • 디폴트 메서드(default method)
    • 인터페이스의 기본 구현을 제공할 수 있도록 디폴트 메서드 사용

즉, 자바8 에서는 메서드 구현을 포함하는 인터페이스를 정의할 수 있다.

인터페이스를 구현하는 클래스는 자동으로 인터페이스에 추가된 새로운 메서드의 디폴트 메서드를 상속받게 된다. 이렇게 하면 기존의 코드 구현을 바꾸도록 강요하지 않으면서도 인터페이스를 바꿀 수 있다.

디폴트 메서드는 다중 상속 동작 이라는 유연성을 제공하면서 프로그램 구성에도 도움을 준다.
디폴트 메소드를 이용하면 자바 API의 호환성을 유지하면서 라이브러리를 바꿀 수 있다. 
기존 구현을 고치지 않고도 인터페이스를 바쑬 수 있다. - 다중상속 동작이라는 유연성 제공

  • 특징
    • 디폴트 메서드는 default 키워드로 시작한다.
    • 메서드 바디를 포함한다 {}
public interface Sized {
  int size();
  default boolean isEmpty() {
    return size() == 0;
  }
}

13.3 디폴트 메서드 활용 패턴

디폴트 메소드 이용하는 방식

  1. 선택형 메서드(optional method)
  2. 동작 다중 상속(multiple inheritance of behavior)

13.3.1 선택형 메서드

13.3.2 동작 다중상속

  • 클래스는 한 개만 상속 받을 수 있지만, 인터페이스는 여러 개 구현할 수 있다.
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {}

13.4 해석규칙

규칙

  1. 클래스가 항상 이긴다. 클래스나 슈퍼클래스에서 정의한 메서드가 디폴트 메서드보다 우선권을 가짐
  2. 클래스 제외하고 서브인터페이스가 이긴다. 상속관계를 갖는 인터페이스에서 같은 시그니처를 갖는 메서드를 정의할 때는 서브인터페이스가 이긴다. 즉 B 가 A를 상속받는다면 B가 A를 이긴다.
  3. 위에서 결정이 안된다면 여러 인터페이스를 상속받는 클래스가 명시적으로 디폴트 메서드를 오버라이드하고 호출해야 한다.

클래스와 메서드 관계로 디폴트 메서드를 선택할 수 없는 경우에는 명시적으로 선택해야한다 -> super

- X.super.m(....)

반응형
반응형

동작 파라미터화 코드 전달하기

소비자의 요구사항은 항상 바뀌기 마련입니다. 이런 변화하는 요구사항에 대해 효과적으로 대응하기 위해서 동작 파라미터화(behavior parameterization)
을 이용하면됩니다.

동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미합니다. 코드 블록은 나중에 프로그램에서 호출한다. 즉, 코드 블록의 실행은 나중으로 미뤄진다.

EXAMPLE

기존의 농장 재고목록 애플리케이션에 리스트에서 녹색(green) 사과만 필터링하는 기능을 추가한다고 가정하자.

첫 번째 시도 : 녹색사과 필터링

사과 색을 정의하는 ENUM 클래스

enum Color { RED, GREEN }

첫 번째 시도

public static List<Apple> filterGreenApples(List<Apple inventory) {
  List<Apple> result = new ArrayList<>();
  for(Apple apple : inventory) {
    if(GREEN.equals(apple.getColor)) {
      result.add(apple);
    }
  }
}

만약 농부가 빨간 사과도 필터링 하고 싶다고하면 어떻게 할까? 메서드를 하나 더만들 수 도 있지만 만약 또 농부가 노란색, 어두운 빨간색등 요구사항이
점차 늘어난다면 메서드가 많이 생길 것이다. 이런경우 다음과 같은 좋은 규칙이 있다.

거의 비슷한 코드가 반복 존재한다면 그 코드를 추상화 한다.

두 번째 시도 : 색을 파라미터화

public static List<Apple> filterApplesByColor(List<Apple> inventory, Color color) {
  List<Apple> result = new ArrayList<>();
  for(Apple apple : inventory) {
    if(apple.getColor().equals(color)) {
      result.add(apple);
    }
  }
}

이제 농부가 어떤 색을 원하든 해당 메서드만 호출하면, 색으로 필터링 할 수 있다. 하지만 농부가 색 이외에도 무거운 사과와 가벼운 사과, 무게로
사과를 필터링하고 싶다고하면 어떻게 해야 할까?

색을 파라미터화 한 것처럼 무게를 파라미터화 하면 된다.

public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) {
  List<Apple> result = new ArrayList<>();
  for(Apple apple : inventory) {
    if(apple.getWeight() > weight) {
      result.add(apple);
    }
  }
  return result;
}

위 코드도 좋은 해결책이지만, 각 사과에 필터링 조건을 적용하는 부분의 코드가 색 필터링 코드와 대부분 중복된다.

이는 소프트웨어 공학의 DRY(Don't Repeat yourself) 같은 것을 반복하지말 것 원칙을 어기는 것이다. 이를 해결하기 위해서 색과 무게를 filter라는
메서드로 합치는 방법도 있다. 따라서 색이나 무게 중 어던 것을 기준으로 필터링할 지 가리키는 플래그를 추가할 수 있다.
(하지만 실전에서는 절대 이 방법을 사용하지 말아야 한다.)

절대 사용하지 말아야 하는 방법

  • 절대 사용하지 말아야 하는 방법
public static List<Apple> filterApples(List<Apple> inventory, Color color, int weight, boolean flag) {
  List<Apple> result = new ArrayList<>();
  for(Apple apple : inventory) {
    if((flag && apple.getColor().equals(color)) || (!flag && apple.getWeight() > weight)) {
      result.add(apple);
    }
  }
  return result;
}

위 메서드를 아래처럼 호출 할 수 있다.

List<Apple> greenApples = filterApples(inventory, GREEN, 0, true);
List<Apple> heavyApples = filterApples(inventory, null, 150, false);

하지만 정말 마음에 들지 않는 코드이다. 대체 true와 false는 무엇을 의미하는 것일까?... 게대가 앞으로 요구사항이 바뀌었을때 유연하게 대처할 수 없다.
에를 들어 사과으 크기, 모양, 출하지 등으로 사과를 필터링 하고싶어지는 경우 어떻게 해야할까?

아래에서 동작 파라미터화를 이용해서 유연성을 얻는 방법에 대해 설명한다.

동작 파라미터화

사과의 어떤 속성에 기초해서 불리언 값을 반환(예를 들어 사과가 녹색인가? 150그램 이상인가?). 참 또는 거짓을 반환하는 함수를 프레디케이트 라고 한다.
선택 조건을 결정하는 인터페이스를 정하자.

public interface ApplePredicate {
  boolean test (Apple apple);
}

다음 예제 처럼 다양한 선택 조건을 대표하는 여러 버전의 ApplePredicate를 정의할 수 있다.

public class AppleHeavyWeightPredicate implements ApplePredicate {
  public boolean test(Apple apple) {
    return apple.getWeight() > 150;
  }
}

public class AppleGreenColorPredicate implements ApplePredicate {
  public boolean test(Apple apple) {
    return GREEN.equals(apple.getColor());
  }
}

위 조건에 따라 filter 메서드가 다르게 동작할 것이라고 예상할 수 있다. 이를 전략 디자인패턴(strategy design patter)이라고 부른다.

전략 디자인 패턴은 각 알고리즘(전략이라 부르는)을 캡슐화 하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다.

여기서는 ApplePredicate가 알고리즘 패밀리이며, 이를 구현한 클래스들이 전략이다.

네 번째 시도 : 추상적 조건으로 필터링

public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
  List<Apple> result = new ArrayList<>();
  for(Apple apple : inventory) {
    if(p.test(apple)) {
      result.add(apple);
    }
  }
  return result;
}

첫 번째 코드에 비해 더 유연한 코드를 얻었으며 동시에 가독성도 좋아졌을 뿐 아니라 사용하기도 쉬워 졌다.

예를 들어 농부가 150그램이 넘는 빨간 사과를 검색해달라고 부탁하면 우리는 ApplePredicate를 적절하게 구현하는 클래스만 만들면 된다.

public class AppleRedAndHeavyPredicate implements ApplePredicate {
  public boolean test(Apple apple) {
    return RED.equals(apple.getColor()) && apple.getWeight() > 150;
  }
}

우리가 전달한 ApplePredicate 객체에 의해 filterApples 메서드의 동작이 결정된다. 즉, 우리는 filterApples 메서드의 동작을 파라미터화 한 것이다.

즉, 우리는 전략 디자인 패턴(Strategy Design Pattern)과 동작 파라미터화를 통해서 필터 메서드에 전략(Strategy)을 전달 함으로써 더 유연한 코드를
만들었다.

한 개의 파라미터, 다양한 동작

지금까지 살펴본 것첢 컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다는 것이 동작 파라미터화의 강점이다.

QUIZ

사과 리스트를 인수로 받아 다양한 방법으로 문자열을 생성(커스터마이즈된 다양한 toString) 메서드와 같이) 할 수 있도록 파라미터화된 prettyPrintApple 메서드를 구현하시오. 예를 들어 prettyPrintApple 메서드가 각각의 사과 무게를 출력하도록 지시할 수 있다. 혹은 각각의 사과가 무거운지, 가벼운지
출력하도록 지시할 수 있다.

  • interface 만들기

참 또는 거짓을 반환하는 함수를 프레디케이트 라고 했습니다. 위 문제에서 String 타입을 반환하라고 했으니 interface 네이밍을 xxxPredicate보다는
Formatter라든지 이 외의 다른 좋은 네이밍으로 정하는게 좋습니다.

public interface AppleFormatter {
  String accept(Apple a);
}

public class AppleFancyFormatter implements AppleFormatter implements AppleFormatter {
  public String accept(Apple apple) {
    String characteristic = apple.getWeight() > 150 ? "heavy" : "light";
    return "A " + characteristic + " " + apple.getColor() + " apple";
  }
}

public class AppleSimpleFormatter implemetns AppleFormatter { 
  public String accept(Apple apple) {
    return "An apple of " + apple.getWeight() + "g";
  }
}

public static void prettyPrintApple(List<Apple> inventory, AppleFormatter foramtter) {
  for(Apple apple : inventory) {
    String output = formatter.accept(apple);
    System.out.println(output);
  }
}

복잡한 과정 간소화

위에서 보여준 예시는(Predicate 인터페이스를 선언하고, 이를 구현하여 인스턴스화 하여 사용하는 것) 상당히 번거로운 작업이며, 시간 낭비이다.

이를 개선 하기 위해서 자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스라는 기법을 제공한다.

익명 클래스는 자바의 지역 클래스와 비슷한 개념이다.

다섯 번째 시도 : 익명 클래스 사용

List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
  public boolean test(Apple apple) {
    return RED.equals(apple.getColor());
  }
}

익명 클래스의 단점은 아직도 코드의 줄이 길다는 것이며, 많은 개발자들이 익명 클래스 사용에 익숙하지 않는다는 점 이다.

여섯 번째 시도 : 람다 표현식 사용

List<Apple> result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));

일곱 번째 시도 : 리스트 형식으로 추상화

public interface Predicate<T> {
  boolean test(T t);
}

public static <T> List<T> filter(List<T> list, Predicate<T> p) {
  List<T> result = new ArrayList<>();
  for(T e : list) {
    if(p.test(e)) {
      result.add(e);
    }
  }
  return result;
}
List<Apple> redApples = filter(inventory, (Apple apple) -> RED.equals(apple.getColor()));
List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);

Comparator로 정렬하기

java.util.Comparator 객체를 이용해서 sort의 동작을 파라미터화 할 수 있다.

public interface Comparator<T> {
  int compare(T o1, T o2);
}

무게가 적은 순서로 목록에서 사과를 정렬

inventory.sort(new Comparator<Apple>() {
    public int comapre(Apple a1, Apple a2) {
      return a1.getWeight().compareTo(a2.getWeight());
    }
}
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
반응형
반응형

자바 8, 9, 10, 11 무슨 일이 일어나고 있는가?

자바 역사를 통틀어 가장 큰 변화가 자바 8에서 일어났다.

  • 자바 8에서 제공하는 새로운 기술
    • 스트림 API
    • 메서드에 코드를 전달하는 방법
    • 인터페이스의 디폴트 메서드

스트림을 이용하면 에러를 자주 일으키며, 멀티코어 CPU를 이용하는 것보다 비용이 훨씬 비싼 키워드 synchronized를 사용하지 않아도 된다. 자바 8에
추가된 스트림 API 덕분에 다른 두 가지 기능, 즉 메서드에 코드를 전달하는 기법(메서드 참조와 람다)과 인터페이스의 디폴트 메서드가 존재 할 수
있음을 알 수 있다.

하지만 스트림 API 때문에 메서드에 코드를 전달하는 기법이 생긴것은 아니다.

메서드에 코드를 전달하는 기법을 사용하면 동작 파라미터화(behavior parameterization)를 구현할 수 있다. 메서드에 코드를 전달(뿐만 아니라 결과를
반환하고 다른 자료구조로 전달할 수 도 있음)하는 자바 8 기법은 함수형 프로그래밍(functional-style programming)에서 위력을 발휘한다.

자바 8에서 함수형 프로그래밍을 도입하면서 객체지향 프로그래밍과, 함수형 프로그래밍의 장점을 누릴 수 있게 되었다.

자바 8 설계의 밑바탕을 이루는 세가지 프로그래밍 개념

스트림 처리(stream processing)

첫 번째 프로그래밍 개념은 스트림 처리다. 스트림이란 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임. 이론적으로 프로그램은 입력 스트림에서
데이터를 한 개씩 읽어 들이며 마찬가지로 출력 스트림으로 데이터를 한 개씩 기록한다. 즉, 어떤 프로그램의 출력 스트림은 다른 프로그램의 입력 스트림이 될
수 있다.

자바 8에는 java.util.stream 패키지에 스트림 API가 추가 되었다. 스트림 패키지에 정의된 Stream는 T형식으로 구성된 일련의 항목을 의미한다.

스트림 API는 파이프라인을 만드는 데 필요한 많은 메서드를 제공한다. 스트림 API의 핵심은 기존에는 한 번에 한 항목을 처리했지만, 이제 자바 8에서는
우리가 하려는 작업을 데이터베이스 질의 처럼 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있다는 것이다. 또한 파이프라인을 이용해서 입력
부분을 여러 CPU 코어에 쉽게 할당할 수 있다는 부가적인 이득도 얻을 수 있다. 스레드라는 복잡한 작업을 사용하지 않으면서도 공짜로 병렬성을 얻을 수 있다.

동작 파라미터화(behavior parameteriation)로 메서드에 코드 전달하기

자바 8에 추가된 두 번째 프로그램 개념은 코드 일부를 API로 전달하는 기능이다. 기존에 자바는 메서드를 다른 메서드의 파라미터로 전달 할 수 없었다.
물론 정렬기능을 위해서 익명함수 형태로 Comparator를 구현하는 방법도 있지만 복잡하다. 자바 8에서는 메서드를 다른 메서드의 파라미터로 전달 할 수 있다.
이러한 기능을 이론적으로 동작 파라미터화라고 부른다. 동작 파라미터화가 중요한 이유는 스트림 API는 연산의 동작을 파라미터화할 수 있는 코드를 전달한다는 사상에 기초하기 때문이다.

병렬성과 공유 가변 데이터

세 번째 프로그래밍의 개념은 병렬성을 공짜로 얻을 수 있다라는 말에서 시작된다. 병렬성을 공짜로 얻기 위해서는 다른 한가지를 포기해야하는데,
스트림 메서드로 전달하는 코드의 동작 방식을 조금 바꿔야 한다. 처음에는 불편하지만 나중에는 편하게 느껴질 것이다.

스트림 메서드로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전하게 실행될 수있어야 한다.

보통 다른 코드와 동시에 실행 하더라도 안전하게 실행할 수 있는 코드를 만들려면 가변 데이터(shared mutable data)에 접근하지 않아야 한다.
이러한 함수를 순수(pure) 함수, 부작용 없는 함수(side-effect-free), 상태 없는(stateless) 함수 라고 부른다.

자바 함수

일급 시민과 이급 시민

프로그래밍 언어의 핵심은 값을 바꾸는 것이다. 역사적으로 그리고 전통적으로 프로그래밍 언어에서는 이 값을 일급(first-class) 값 또는 시민(citizens)
이라고 부른다. 자바 프로그래밍 언어의 다양한 구조체(메서드, 클래스 같은)가 값을 구조를 표현하는데 도움이 될 수 있다. 하지만 프로그램을 실행하는 동안 이러한 모든 구조체를 자유롭게 전달할 수는 없다. 이렇게 전달할 수 없는 구조체는 이급 시민이다.

자바 8에서는 이급 시민을 일급 시민으로 바꿀 수 있는 기능을 추가했다. 이미 스몰토크, 자바스크립트 같은 다양한 언어에서 일급 시민으로 가득찬 세계를
성공적으로 만들어 가고있다.

메서드와 람다를 일급 시민으로

메서드 참조(method reference) ::

  • 디렉터리에서 모든 숨겨진 파일을 필터링 하는 코드
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
  public boolean accept(File file) {
    return file.isHidden(); // 숨겨진 파일 필터링
  }
});

하지만 코드가 마음에 들지 않는다. 단 세 행의 코드지만 각 행이 무슨 작업을 하는지 투명하지 않다. 자바 8에서는 위 코드를 아래처럼 구현할 수 있다.

File[] hiddenFiles = new File(".").listFiles(File::isHidden);

자바 메서드 참조 :: (이 메서드를 값으로 사용하라는 의미)를 이용해서 listFiles에 직접 전달할 수 있다.

람다 : 익명함수

자바 8에서는 메서드를 일급 값으로 취급할 뿐 아니라 람다(또는 익명함수 anonymous functions)를 포함하여 함수도 값으로 취급할 수 있다.

코드 넘겨주기 : 예제

Apple 클래스와 getColor 메서드가 있고 Apples 리스트를 포함하는 변수 inventory가 있다고 가정하자. 이때 모든 녹색 사과를 선택해서 리스트를
반환하는 프로그램을 구현해보자.

  • 자바 8 이전 방식
public static List<Apple> filterGreenApples(List<Apple> inventory) {
  List<Apple> result = new ArrayList<>();
  for(Apple apple : inventory) {
    if(GREEN.equals(apple.getColor())) {
      result.add(apple);
    }
  }
  return result;
}

하지만 누군가는 사과를 무게 150그람 이상으로 필터링 하고 싶을 수 있다. 그러면 우리는 다음처럼 코드를 구현할 수 있을 것이다.

public static List<Apple> filterHeavyApples(List<Apple> inventory) {
  List<Apple> result = new ArrayList<>();
  for(Apple apple : inventory) {
    if(apple.getWeight() > 150) {
      result.add(apple);
    }
  }
  return result;
}

소프트웨어공학적인면에서 복붙의 단점은 어떤 코드에 버그가 있다면 복붙한 모든 코드를 고쳐야 한다.

자바 8을 기준으로는 아래처럼 고칠 수 있다.

public static boolean isGreenApple(Apple apple) {
  return GREEN.equals(apple.getColor());
}

public static boolean isHeavyApple(Apple apple) {
  return apple.getWeight() > 150;
}

// 명확히 하기위해 적어놓음 
// 보통은 java.util.function에서 임포트함
public interface Predicate<T> {
  boolean test(T t);
}

// 메서드가 p라는 이름의 프레디케이트 파라미터로 전달됨
static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p {
  List<Apple> result = new ArrayList<>();
  for(Apple apple : inventory) {
    if(p.test(apple)) {
      result.add(apple);
    }
  }
  return result;
}

// 아래처럼 메서드를 호출할 수 있다.
filterApples(inventory, Apple:isGreenApple);
filterApples(inventory, Apple:isHeavyApple);

프레디케이트(Predicate) 란?

수학에서는 인수로 값을 받아 true나 false를 반환하는 함수를 프레디케이트라고 한다. 나중에 설명하겠지만 자바 8에서도 Function<Apple, Boolean> 같이 코드를 구현할 수 있만 Predicat을 사용하는 것이 더 표준적인 방식이다.(또한 boolean을 Boolean으로 변환하는 과정이 없으므로 더 효율적이다.)

메서드 전달에서 람다로

메서드를 값으로 전달하는 것은 분명 유용한 기능이다. 하지만 isHeavyApple, isGreenApple 처럼 한 두 번만 사용할 메서드를 매번 정의하는 것은
귀찮은 일이다. 자바 8에서는 이 문제도 간단히 해겨랄 수 있다. 바로 람다를 이용하면 된다.

filterApples(inventory, (Apple a) -> GREEN.equals(a.getColor()));

filterApples(inventory, (Apple a) -> a.getWieght() > 150);

즉, 한 번만 사용할 메서드는 따로 정의를 구현할 필요가 없다. 하지만 람다가 몇 줄 이상으로 길어진다면 익명 람다 보다는 코드가 수행하는 일을 잘 설명하는 이름을 가진 메서드를 정의하고 메서드 참조를 활용하는 것이 바람직 하다. 코드의 명확성이 우선시 되어야 한다.

스트림

거의 모든 자바 애플리켕션은 컬렉션을 만들고 활용한다. 하지만 컬렉션으로 모든 문제가 해결되는 것은 아니다.

예를 들어 고가의 트랜잭션(transaction)(거래) 만 필터링한 다음에 통화로 결과를 그룹화 해야 한다고 가정하자. 아래와 같은 많은 기본 코드를 구현해야한다.

Map<Curreny, List<Transaction>> transactionByCurrencies = new HashMap<>(); // 그룹화된 트랜잭션을 더할 Map 생성
for(Transaction transaction : transactions) {
  if(transaction.getPrice() > 1000) {
    Curreny curreny = transacation.getCurrency(); // 트랜잭션의 통화를 추출
    List<Transcation> transactionsForCurrency = transactionsByCurrencies.get(currency);
    if(transactionsForCurrency == null) {
      transactionsForCurrency = new ArrayList<>();
      transactionsByCurrencies.put(currenc, transcationsForCurrency);
    }
    transactionsForCurrency.add(transacation);
  }
}

위의 예제는 중첩된 제어 흐름 문장이 많아서 코드를 한 번에 이해하기 어렵다.

스트림 API를 이용하면 다음처럼 문제를 해결할 수 있다.

import static java.util.stream.Collectors.groupingBy;
Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream()
                                                              .filter((Transcations t) -> t.getPrice() > 1000); // 고가의 트랜잭션 필터링
                                                              .collect(groupingBy(Transcation::geturrency)); // 통화로 그룹화

내부 반복과 외부 반복

외부 반복(external iteration)은 for-each루프를 이용해서 각 요소를 반보갛면서 작업을 수행하는 것들을 말한다. 반면 내부 반복(internal iteration)은 스트림 API와 같이 루프를 신경 쓸 필요 없이, 스트림 API라는 라이브러리 내부에서 모든 데이터가 처리되는 것을 말한다.

멀티 스레딩은 어렵다

자바 8은 스트림(API, java.util.stream)로 컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제멀티코어 활용 어려움이라는 두 가지 문제를 모두 해결했다. 컬렉션은 어떻게 데이터를 저장하고 접근할지에 중점을 두는 반면 스트림은 데이터에 어떤 계산을 할 것인지 묘사하는 것에 중점을 둔다.

포킹단계(forking step)

예를들어 두 CPU를 가진 환경에서 리스트를 필터링할 때 한 CPU는 앞 부분을 처리하고, 다른 CPU는 리스트의 뒷 부분을 처리하도록 요청할 수 있는데 이 과정을 포킹 단계라고 한다. 각각의 cpu는 자신이 맡은 절반의 리스트를 처리하고, 마지막으로 하나의 cpu가 두 결과를 정리한다.

자바 8에서 제공하는 두 가지 요술방망이

흔히 사람들은 자바의 병렬성은 어렵고 synchronized는 쉽게 에러를 일으킨다고 생각한다. 자바8은 어떤 요술방망이를 제공할까?

자바 8은 두 가지 요술 방망이를 제공한다. 우선 라이브러리에서 분할을 처리한다. 즉, 큰 스트림을 병렬로 처리할 수 있도록 작은 스트림으로 분할한다.
또한 filter 같은 라이브러리 메서드로 전달된 메서드가 상호작용을 하지 않는다면 가변 공유 객체를 통해 공짜로 병렬성을 누릴 수 있다.
상호작용을 하지 않는다는 제약은 프로그래머 입장에서 상당히 자연스러운 일이다. 함수형 프로그래밍에서 함수란 함수를 일급값으로 사용한다라는 의미도 있지만, 부가적으로 프로그램이 실행되는 동안 컴포넌트 간에 상호작용이 일어나지 않는다라는 의미도 포함한다.

디폴트 메서드와 자바 모듈

자바 9의 모듈 시스템은 모듈을 정의하는 문법을 제공하므로 이를 이용해 패키지 모음을 포함하는 모듈을 정의할 수 있다.
또한 자바 8에서는 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메서드를 지원한다.

디폴트 메서드는 특정 프로그램을 구현하는 데 도움을 주는 기능이 아니라 미래에 프로그램이 쉽게 변화할 수 있는 환경을 제공하는 기능이다.

어떻게 기존의 구현을 고치지 않고도 이미 공개된 인터페이스를 변경할 수 있을까라는 딜레마를 디폴트 메서드가 해소시켜준다.

기존에는 인터페이스에 메서드가 하나 추가되면 인터페이스를 사용하는 모든 곳에서 메서드를 추가해야하지만, 디폴트 메서드는 구현하지 않아도 되는 메서드이다. 메서드 본문(bodies)은 클래스 구현이 아니라 인터페이스 일부로 포함된다.(그래서 이를 디폴트 메서드라고 한다.)

함수형 프로그래밍에서 가져온 다른 유용한 아이디어

1965년에 널 참조를 발명했던 일을 회상하며 그 결정은 정말 뼈아픈 실수였다고 반성하고 있다.. 단지 구현이 편리하단 이유로 널 참조를 만들어야 겠다는
유혹을 뿌리치지 못했다. -> 토니 호아레(Tony Hoare)는 2009년 QCon London의 프레젠테이션에서 위 와같은 말을 했다.

자바 8에서는 NullPointer 예외를 피할 수 있도록 도와주는 Optional<T> 클래스를 제공한다. Optional는 값을 갖거나 갖지 않을 수 있는 컨테이너 객체이다.

반응형
반응형

스트림으로 데이터 수집

  • Collectors 클래스로 컬렉션 만들고 사용하기
  • 하나의 값으로 데이터 스트림 리듀스 하기
  • 특별한 리듀싱 요약 연산
  • 데이터 그룹화와 분할
  • 자신만의 커스텀 컬렉터 개발

컬렉션(Collection), 컬렉터(Collector), collect는 서로 다르다.

6.1 컬렉터란 무엇인가?

Collector 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할지 지정한다.

6.1.1 고급 리듀싱 기능을 수행하는 컬렉터

스트림에 collect를 호출하면 스트림의 요소에(컬렉터로 파라미터화된) 리듀싱 연산이 수행된다. 즉, 내부적으로 리듀싱 연산이 일어난다.
장점 : collect 로 결과를 수집하는과정을 간단하면서도 유연한 방식으로 정의할 수 있다.

예제 6.1

그림 6.1

6.1.2 미리정의된 컬렉터

Collectors에서 제공하는 메서드의 기능은 크게 세 가지로 구분할 수 있다.

  • 스트림 요소를 하나의 값으로 리듀스하고 요약
  • 요소 그룹화
  • 요소 분할

6.2 리듀싱과 요약

counting 예시

int howManyDishes = menu.stream().collect(counting());
6.2.1 스트림값에서 최댓값과 최솟값 검색

Collectors.maxBy 와 Collectors.minBy 로 스트림의 최대값과 최소값을 구할 수 있다.

Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCaloriesDish = menu.stream().collect(maxBy(dishCaloriesComparator));
6.2.2 요약연산

Collectors.summinngInt

int totalCalories = menu.stream().collect(summingInt(Dish::getCalories))

그림 6.2 summinngInt 컬렉터 누적과정

그 외 long, double 과 averagingInt 및 summarizingInt 가 있다.

6.2.3 문자열 연결

joining
추출한 모든 문자열을 하나의 문자열로 연결해서 반환

String menuListStr = menu.stream().map(Dish::getName).collect(joining(", "))

6.2.3 범용 리듀싱 요약 연산

reducing

범용 Collectors.reducing을 사용
특화된 summinngInt 등과 같은 메소드를 사용하는 이유는 편의성 및 가독성 때문

int totalCalories = menu.stream()
  .collect(reducing(0, Dish::getCaloreis, (i, j) -> i + j));

reducing은 세 개의 인수를 받는다. (초기값, 합계 함수, 변환 함수)

  • 첫 번째 인수는 리듀싱 연산의 시작값이거나 스트림에 인수가 없을 때는 반환값이다.(숫자 합계에서는 인수가 없을 때 반환하므로 0이 적합하다.)
  • 두 번째 인수는 함수를 받는다.
  • 세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator이다.

한 개의 인수를 갖는 reducing

가장 칼로리가 높은 요리 찾는 방법

Optional<Dish> mostCaloireDish = menu.stream().collect(
    reducing((d1, d2) -> d1.getCaloreis() > d2.getCalories() ? d1 : d2)
);

세 개의 인수를 갖는 reducing 메서드에서 첫 번째 인수를 받고, 두 번째 인수에서
자기 자신을 그대로 반환하는 항등함수(identity function)를 두 번째 인수로 받는 상황에 해당한다.

한 개의 인수를 갖는 reducing 컬렉터는 시작값이 없으므로 빈 스트림이 넘겨졌으래 시작값이 설정되지 않아 null을 반환할 수 있으므로
Optional 객체로 만들어 사용해야 한다.

컬렉션 프레임워크 유연성 : 같은 연산도 다양한 방식으로 수행할 수 있다.

int totalCaloires = menu.stream().collect(reducing(0, Dish::getCaloires, Integer::sum)); 
menu.stream().map(Dish::getCalories).reduce(Integer::sum).get(); // 인자가 하나여서 Optional 을 반환하지만 get 으로 값 추출

그룹화

자바8의 함수형을 이용하면 가독성 있는 한 줄의 코드로 그룹화를 구현할 수 있다.
Collectors.groupingBy

/** 
  * 그룹화 groupingBy
  * 생선, 고기 그 밖의 것들로 그룹화 
  */
Map<Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));

groupingBy를 분류 함수(classification function) 이라고 한다.
그림 6.4 - 그룹화로 스트림의 항목을 분류하는 과정

람다 표현식으로도 그룹화 가능

/**
  * 칼로리별로 그룹화
  */
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(
  groupingBy(dish -> {
    if(dish.getCalories() <= 400) return CaloricLevel.DIET;
    else if(dish.getCalories() <= 700) return CaloricLevel.NORMAL;
    else return CaloricLevel.FAT;
  }));
// 500칼로리가 넘는 요리만 타입과 종류로 그룹화
Map<Type, List<Dish>> caloricDishesByType = menu.stream().filter(dish -> dish.getCalroies() > 500)
  .collect(groupingBy(Dish::getType));
/**
  * 결과
  * {OTHER=[french fries, pizza], MEAT=[pork, beef]}
  * FISH 라는 키 자체가 사라짐
  * 위 코드의 단점은 위 filter 프레디케이트를 만족하는 값이 없을 경우 키값 자체가 제외되서 맵에 담지 못한다.
  * 해결책 : Collectors 클래스의 정적 팩터리 메서드인 filtering 사용
  */

// 해결
Map<Type, List<Dish>> caloricDishesByType = menu.stream().collect(groupingBy
  (Dish::getType, filtering(dish -> dish.getCalories() > 500, toList())));
// 결과 : {OTHER=[french fries, pizza], MEAT=[pork, beef], FISH=[]}

/** mapping 사용 */
Map<Type, List<String>> dishNamesByType = menu.stream().
  collect(groupingBy(Dish::getType, mapping(Dish::getName, toList())));

/** flatMapping 사용 
  * flatMap은 두 수준의 리스트를 한 수준으로 평면화 할 수 있음 
  * 연산결과를 수집해서 리스트가 아니라 집합으로 그룹화해 중복 태그를 제거한다.
  */
Map<Type, Set<String>> dishNamesByType = 
  menu.stream()
    .collect(groupingBy(Dish::getType, flatMapping(dish -> dishTags.get(dish.getName()).stream(), toSet())));
6.3.2 다수준 그룹화
menu.stream().collect(
        groupingBy(Dish::getType,
            groupingBy((Dish dish) -> {
              if (dish.getCalories() <= 400) {
                return CaloricLevel.DIET;
              }
              else if (dish.getCalories() <= 700) {
                return CaloricLevel.NORMAL;
              }
              else {
                return CaloricLevel.FAT;
              }
            })
        )

groupingBy

groupingBy(x)는 사실 groupingBy(x, toList())의 축약형이다.

  • 요리의 종류를 분류하는 컬렉터로 메뉴에서 가장 높은 칼로리를 가진 요리를 찾는 프로그램
Map<Type, Optional<Dish>> mostCaloricByType = menu.stream().collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCaloires))));
6.3.3 서브그룹으로 데이터 수집
Map<Dish.TYPE, Long> typeCount = menu.stream().collect(groupingBy(Dish::getType, counting()))

컬렉터 결과를 다른 형식에 적용하기

collectingAndThen

  • 팩토리 메서드 collectingAndThen은 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환한다.

  • 분류함수로 여러 서브스트림을 만든 뒤 각각 리듀싱 요약 가능

    Map<Type, Dish> mostCaloricByType = 
    menu.stream()
      .collect(groupingBy(Dish::getType, // 분류함수
                          collectingAndThen(maxBy(comparingInt(Dish::getCaloreis)), // 감싸인 컬렉터 
                          Optional::get)); // 변환함수 -- Optional 에 포함된 값을 추출
    

그림 6.6 여러 컬렉터를 중첩한 효과

  • 각 요리 형식에 존재하는 모든 CaloricLevel 값을 알고 싶은 경우(groupingBy와 mapping 사용)
Map<Type, Set> caloricLevelByType =  
menu.stream().collect(  
groupingBy(Dish::getType, 
            mapping(dish -> { // dish를 CaloricLevel 로 매핑  
                        if(dish.getCalories() <= 400) return CaloricLevel.DIET;  
                        else if(dish.getCalories() <= 700 return CaloricLevel.NORMAL;  
                        else return CaloricLevel.FAT; 
                    },  
                    toSet() )
                     )
           ); // 리스트가 아닌 집합으로 반환

6.4 분할(partitioningBy)

분할은 분할 함수(partitioning function)이라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능이다. 분할 함수는 불리언을 반환하므로 맵의 키 형식은 Boolean이다.

분할의 장점은 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지한다.

  • 모든 요리를 채식과 아닌 요리로 분류

    Map<Boolean, List> partitionedMenu =  
    menu.stream().collect(partitioningBy(Dish::isVegetarian));
    List vegetarianDishes = partitionedMenu.get(true); // 파티셔닝 사용
    List vegetarianDishes2 = menu.stream().filter(Dish::isVegetarian).collct(toList()); // 필터사용
  • 분할함수 안에 그룹함수를 사용

    menu.stream().collect(partitioningBy(Dish::isVegetarian, groupingBy(Dish::getType)))
    
  • 채식과 채식이 아닌 요리에서 가장 칼로리가 높은 음식 찾기

  • 분할함수 안에서서 값 비교

    Map<Boolean, Dish> mostCaloricPartitioneByVegetarian =  
    menu.stream().collect(  
                      partitioningBy(Dish::isVegetarain, 
                                     collectingAndThen(maxBy(comparingInt(Dish::getCalories)), 
                                     Optional::get)));
    
6.4.2 숫자를 소수와 비소수로 분할하기

6.5 Collector 인터페이스

public interface Collector<T, A, R> {  
    Supplier supplier();  
    BiConsumer<A, T> accumulator();  
    BinaryOperator combiner();  
    Function<A, R> finisher();  
    Set<Collector.Characteristics> characteristics();  
}
  • T는 수집될 스트림 항목의 제네릭 형식
  • A는 누적자, 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식
  • R은 수집 연산 결과 객체의 형식(항상 그런 것은 아니지만 대개 컬렌션 형식)

characteristics 은 collect 메소드가 어떤 최적화를 이용해서 리듀싱 연산을 수행할 것인지를 결정하도록 돕는 힌트 특성 집합 제공

supplier 메소드: 새로운 결과 컨테이너 만들기

supplier 메소드는 빈 결과로 이루어진 Supplier를 반환해야 한다.

accumulator 메소드: 결과 컨테이너에 요소 추가하기

accumulator 메소드는 리듀싱 연산을 수행하는 함수를 반환

finisher 메소드: 최종 변환값을 결과 컨테이너로 적용하기

finisher 메소드는 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환해야 한다.

combiner 메소드: 두 결과 컨테이너 병합

combiner 는 스트림의 서로 다른 서브파트를 병렬로 처리할 떄 누적자가 이 결과를 어떻게 처리 할지 정의한다.

Characteristics 메소드

Characteristics 는 스트림을 병렬로 리듀스할지 병렬로 리듀스한다면 어떤 최적화를 선택해야 할지 힌트 제공
UNORDERED : 리듀싱 결과가 누적순서에 영향 받지 않을때
CONCURRENT : 병렬 리듀싱을 수행할 수 있다. 데이터 소스가 정렬되어 있지 않은 상황에서만 병렬 리듀싱 가능(순서 무의미)
IDENTIFY_FINISH : 리듀싱 과정의 최정결과 - 생략가능

menu.stream().collect(Collectors.toList());  
\-> menu.stream().collect(new ToListCollector());

기존 코드의 toList 는 팩토리지만 ToListCollector 는 new 로 인스턴스화한다.

반응형
반응형

https://www.hanbit.co.kr/support/supplement_survey.html?pcode=B4926602499

 

한빛출판네트워크

출판사, IT전문서, 대학교재, 경제경영, 어린이/유아, MAKE, 실용/여행, 전자책, 인터넷 강의

www.hanbit.co.kr

예제 소스 파일

 

 

https://fliphtml5.com/hkuy/hoja

 

모던 자바 인 액션 : 람다, 스트림, 함수형, 리액티브 프로그래밍으로 새로워진 자바 마스터하기

Related x

fliphtml5.com

영어 PDF

https://livebook.manning.com/book/modern-java-in-action/about-this-book/

 

About this book · Modern Java in Action: Lambdas, streams, reactive and functional programming

 

livebook.manning.com

 

반응형
반응형

Modern Java In Action 정리

Modern Java In Action을 읽고 내용을 정리해본다.

5장 스트림 활용

필터링

  • filter() 메서드는 Predicate<T>를 인자로 일치하는 모든 요소를 포함하는 스트림을 반환한다.

    @Test
    public void 스트림_filter(){
      List<Integer> numbers = Arrays.asList(1, 2, 3, 1, 2, 4);
      numbers.stream()
             .filter(i -> i % 2 == 0) // 짝수만 필터링
             .distinct() // 중복요소 제거, hashCode와 equals로 결정된다.
             .forEach(System.out::println); // 출력
    }
  • distinct(), skip(n), limit(n) 와 같이 사용되어 스트림을 축소할 수 있다. 직관적으로 동작이 메서드명에 나타나므로 따로 정리를 하지 않는다.

  • 자바9에서 추가된 takeWhile(), dropWhile() 를 활용하면 기본 filter에서 추가 동작을 지정할 수 있다.
    (이미 정렬된 상태에서 유용하게 적용이 가능하다.)

@Test
public void 스트림_takeWhile_dropWhile(){
    // 메뉴 컬렉션을 칼로리를 기준으로 오른차순 정렬
    menu.sort(Comparator.comparing(Dish::getCalories));
    menu.stream().forEach((dish -> System.out.print(dish.getName() + "(" + dish.getCalories() + ") ")));

    // takeWhile() : 조건에 만족할 때까지 스트림을 반환, false시 반복 중단
    List<Dish> lowCalDish = menu.stream()
            .takeWhile(dish -> dish.getCalories() < 450)
            .collect(Collectors.toList());

    System.out.println("\n450 칼로리 미만 ------");
    lowCalDish.stream().forEach((dish -> System.out.print(dish.getName() + "(" + dish.getCalories() + ") ")));

    // dropWhile() : 조건에 만족하는 요소까지 버림, true부터 스트림을 반환
    List<Dish> highCalDish = menu.stream()
            .dropWhile(dish -> dish.getCalories() < 450)
            .collect(Collectors.toList());

    System.out.println("\n450 칼로리 이상 ------");
    highCalDish.stream().forEach((dish -> System.out.print(dish.getName() + "(" + dish.getCalories() + ") ")));
}
chicken(400) salmon(450) french fries(530) beef(700) pork(800) 
450 칼로리 미만 ------
chicken(400) 
450 칼로리 이상 ------
salmon(450) french fries(530) beef(700) pork(800) 

매핑

  • 스트림API는 스트림의 요소에서 다른 요소로 변환할 수 있는 map()과 flatMap() 메서드를 제공한다.
    <R> Stream<R> map(Function<? super T, ? extends R> var1);
    <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> var1);
  • 매핑 메서드들은 스트림의 한 요소(T 타입)에서 맵핑되는 새로운 타입(R 타입)의 요소를 만들어 반환한다.
  • 아래 예시에서는 map(Function<String, Integer>) 를 이용하여 Integer를 요소로 갖는 스트림을 생성하고 이를 List로 반환했다.
@Test
public void map_테스트(){
    List<String> strs = List.of("하이", "헬로우");

    // 문자열 길이를 요소로 같는 리스트 생성
    List<Integer> lengths = strs.stream()
            .map(String::length)
            .collect(Collectors.toList());
}
  • map()은 스트림의 요소별로 1대1로 반환하기 때문에 이해가 쉽지만 flatMap()은 다른 동작을 한다.

  • map()과 flatMap()에 파라미터로 주는 Function 인터페이스를 보면 반환형이 다른것을 볼 수 있다.

    • map() : Function<? super T, ? extends R>
    • flatMap() : Function<? super T, ? extends Stream<? extends R>>
  • flatMap의 파라미터인 Function 인터페이스의 구현체는 반환하는 객체의 타입이 Stream 타입이고, flatMap은 Stream 그 자체가 아닌 Stream의 컨텐츠로 매핑한다. 즉, map과 다르게 flatMap은 하나의 평면화된 스트림을 반환한다.

  • 차이를 볼 수 있게 예제 코드를 확인한다.
    문자열 리스트에서 중복을 제거한 한글자가 요소인 리스트를 구한다.

    @Test
    public void flatMap_테스트(){
       List<String> strs = List.of("HI", "HELLO");
    
       List<String> distinctStrs = strs.stream()
               .map(s -> s.split("")) // Stream<String[]>
    //          .map(sArr -> Arrays.stream(sArr)) // Stream<Stream<String>>
               .flatMap(sArr -> Arrays.stream(sArr)) // Stream<String>
               .distinct()
               .collect(Collectors.toList());
    
       distinctStrs.forEach(System.out::println); // H I E L O
    }

sArr -> Arrays.stream(sArr) 의 실행결과인 Stream 에 대해

  • map은 Stream<Stream>을 리턴

![ModernJava3_0]({{ "/assets/img/202002/ModernJava3_0.png" | relative_url }})

  • flatMap은 Stream의 컨텐츠인 String으로 하나의 평면화면 Stream을 리턴했다.
    distinct()도 의도한 대로 글자당 중복을 제거하도록 동작한다.

![ModernJava3_1]({{ "/assets/img/202002/ModernJava3_1.png" | relative_url }})

  • 이 밖에도 flatMap은 아래와 같이 변환을 지원하므로 자세한 내용은 Mkyong flatMap Example 의 내용을 참고하자
    Stream<String[]>        -> flatMap ->   Stream<String>
    Stream<Set<String>>        -> flatMap ->   Stream<String>
    Stream<List<String>>    -> flatMap ->   Stream<String>
    Stream<List<Object>>    -> flatMap ->   Stream<Object>

검색과 매칭

  • 스트림API는 특정 속성이 데이터 집합에 있는지 여부를 검색하는 allMatch, anyMatch, noneMatch, findFirst, findAny 등 다양한 유틸리티 메서드를 제공한다.
@Test
public void 검색과_매칭(){
    List<String> strs = List.of("HI", "HELLO");

    boolean isAllStartWithH = strs.stream()
                                  .allMatch(s -> s.startsWith("H"));
    System.out.println("모두 H로 시작합니다. : " + isAllStartWithH);

    boolean isOneEndWithO = strs.stream()
                                .anyMatch(s -> s.endsWith("O"));
    System.out.println("한 요소 이상이 O로 끝납니다. : " + isOneEndWithO);

    boolean isNoneEndWithK = strs.stream()
                                 .noneMatch(s -> s.endsWith("K"));
    System.out.println("모두 K로 끝나지 않습니다. : " + isNoneEndWithK);

    Optional<String> first = strs.stream().findFirst();
    System.out.println("첫번째 요소는(First) : " + first.get());


    Optional<String> any = strs.stream().findAny();
    System.out.println("첫번째 요소는(Any) : " + any.get());
}
  • findFirst와 findAny가 모두 필요한 이유는 병렬성 때문이다. 요소의 반환순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.

리듀싱

  • 최종 결과값이 나올 때까지 스트림의 요소를 반복적으로 처리하는 연산을 리듀싱 연산이라고 한다.

  • 리듀싱 연산은 reduce 메서드로 수행할 수 있으며 사용은 예제코드를 참고한다.

    @Test
    public void reduce(){
      int[] nums = new int[]{1, 2, 3, 4, 5};
    
      // 초기값을 사용하여 reduce 연산
      int sumWithInitVal = Arrays.stream(nums)
                                 .reduce(0, Integer::sum);
      System.out.println("합계 : " + sumWithInitVal); // 15
    
      // 초기값 없이 reduce 연산
      OptionalInt optSum = Arrays.stream(nums)
                                 .reduce(Integer::sum);
      System.out.println("합계 : " + optSum.getAsInt()); // 15
    
      // 초기값이 없기 때문에 reduce 연산시 결과값이 없을 수 있다.
      OptionalInt optSum2 = Arrays.stream(new int[]{})
                                  .reduce(Integer::sum);
      System.out.println("합계 : " + optSum2.orElse(0)); // 0
    }
반응형
반응형

Modern Java In Action 정리

Modern Java In Action을 읽고 내용을 정리해본다.

4장 스트림 소개

스트림(Stream)

데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소

  • 자바8에 추가된 기능으로 스트림을 이용하면 선언형으로 코드를 구현하여 컬렉션 데이터를 처리할 수 있다.
    선언형으로 구현한다는 것은 for 루프나 if 조건문 등의 제어 블록을 사용하지 않고 동작을 지정하는 것이다.
    → 실제 구현은 신경쓰지 않고 사용하는 SQL를 생각하자.
  • 스트림에서 제공하는 filter, sorted, map, collect 같은 메서드들은 특정 스레딩 모델에 제한되지 않고 스레드와 락을 걱정할 필요없이 편리하게 데이터 병렬처리를 가능하게 해준다.

스트림 기본 구현

  • 기본구현에 사용할 Dish 클래스

    class Dish{
      private final String name;
      private final boolean vegetarian;
      private final int calories;
      private final Type type;
    
      public Dish(String name, boolean vegetarian, int calories, Type type) {
          this.name = name;
          this.vegetarian = vegetarian;
          this.calories = calories;
          this.type = type;
      }
    
      public String getName() {
          return name;
      }
    
      public boolean isVegetarian() {
          return vegetarian;
      }
    
      public int getCalories() {
          return calories;
      }
    
      public Type getType() {
          return type;
      }
    
      enum Type {
          MEAT, FISH, OTHER
      }
    }
  • 스트림 구현에 사용할 Dish 컬렉션

    List<Dish> menu = Arrays.asList(
      new Dish("pork", false, 800, Dish.Type.MEAT),
      new Dish("beef", false, 700, Dish.Type.MEAT),
      new Dish("chicken", false, 400, Dish.Type.MEAT),
      new Dish("french fries", true, 530, Dish.Type.OTHER),
      new Dish("salmon", false, 450, Dish.Type.FISH)
    );
  • 스트림 기본구현

    @Test
    public void 스트림_기본구현(){
      List<String> threeHighCaloricDishNames =
          menu.stream() // 컬렉션에서 스트림(Stream<Dish>)을 가져온다.
              .filter(dish -> dish.getCalories() > 300) // 해당 조건의 요소만 추출한다.
              .map(Dish::getName) // 이름(String) 속성을 스트림(Stream<String)으로 가져온다.
              .limit(3) // 3개를 제외하고 truncate한다.
              .collect(Collectors.toList()); // 스트림을 컬렉션(리스트)로 변환한다.
    
      System.out.println(threeHighCaloricDishNames); // [pork, beef, chicken]
    }
  • filter, map 메서드는 인자로 Functional Interface 인스턴스를 받기 때문에 람다표현식이나, 메서드참조로 간결하게 코딩이 가능하다. 또한 메서드들은 실행 결과로 스트림을 리턴하기 때문에 파이프라인 형태가 된다.
    → 빌더 패턴과 유사하다.

  • 서로 연결이 가능한 filter, map, limit는 중간연산이며, collect는 스트림을 닫는 최종연산이다.

스트림과 컬렉션

  • 컬렉션은 DVD에 저장된 영화에 비유할 수 있다. 모든 데이터(영화 내용 전부)가 메모리(DVD)에 저장되어 있다.
    컬렉션에는 계산된 결과물이 저장되어 있으며, 주 관심사는 특정요소에 접근하여 값을 계산하거나 치환한다.
  • 스트림은 인터넷으로 스트리밍하는 영화에 비유할 수 있다.
    영화 전체를 모두 받는 것이 아니라 미리 몇 프레임만 내려받아 재생이 가능할 수 있다. 스트림은 이론적으로 요청할 때만 요소를 계산하는 고정된 자료구조다.
  • 스트림은 한번만 탐색이 가능하다.
    이미 소비된 스트림을 사용하려 하면 Exception이 발생하므로 다시 생성하여 사용해야 한다.
  • 컬렉션은 데이터를 순회하기 위해서는 루프문을 이용해 명시적으로 반복해야 한다. → 외부반복
  • 스트림은 반복을 알아서 처리(중간값 저장, 최적화, 병렬성 구현 등) 한다. → 내부반복
    @Test
    public void 스트림_내부반복(){
      // forEach 메서드에서 반복되며 명시적으로 루프문이 필요없다.
      menu.stream()
          .forEach(dish -> System.out.println(dish.getName()));
    }

스트림은 게으른(lazy) 연산을 지원

  • 스트림 처리시 한 요소는 연결된 모든 파이프라인을 타고나서 다음 요소가 처리된다.

    @Test
    public void 스트림_게으른_연산(){
      List<String> threeHighCaloricDishNames =
          menu.stream()
              .filter((dish) -> {
                  System.out.println("filtering :: " + dish.getName());
                  return dish.getCalories() > 300;
              })
              .map((dish) -> {
                  System.out.println("mapping :: " + dish.getName());
                  return dish.getName();
              })
              .limit(3)
              .collect(Collectors.toList());
    
      System.out.println(threeHighCaloricDishNames); // [pork, beef, chicken]
    }
  • 콘솔출력으로 스트림 처리 순서를 보면 한 요소씩 filter → map → limit → collect 파이프라인을 타는것을 볼 수 있다. 중간 연산들(filter → map → limit)을 합친 다음에 최종 연산(collect)에서 한 번에 처리한다.

    filtering :: pork
    mapping :: pork
    filtering :: beef
    mapping :: beef
    filtering :: chicken
    mapping :: chicken
    [pork, beef, chicken]
반응형

+ Recent posts