반응형

자바 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]
반응형
반응형

Mkyong

자바 예제소스만 버젼별로 많이 올려주시는데, 이거로만 튜토리얼 공부해도 될 정도로 다양한 내용 예제 소스로 영문으로만 정리되어 있습니다.

https://mkyong.com/

 

Mkyong.com - Learn Java and Spring.

Focus on Core Java and Spring Frameworks, with simple examples, code snippets, and tutorials.

mkyong.com

 

javable

자바 관련 컨텐츠로 좋은 코드관련해서 헷갈릴만한 내용들이 많이 있습니다.
우아한형제들 기술블로그랑 관련 있어보이네요.

https://woowacourse.github.io/javable/

 

Home

우아한테크코스 코드리뷰 모음 공간

woowacourse.github.io

 

제이 블로그

자바 관련 컨텐츠가 많습니다. 자바 뿐만 아니라 자바를 쓰면서 사용할 수 있는 devops 관련된 기술 블로그 또한 많습니다.

https://pjh3749.tistory.com/

 

JayTech의 기술 블로그

개발, 알고리즘 등 기술 관련 블로그입니다. 추가로 저의 일상, 미국 생활이야기, 여행 관련 글도 씁니다.

pjh3749.tistory.com

velopert 블로그

https://velog.io/@velopert

 

velopert (Minjun Kim) - velog

2019.log 벌써 2019년도 끝났다. 쓸 이야기가 많고 생각을 정리할 것도 많아서 결국 2020년 1월 1일에 릴리즈하고 말았다! 역시, 회고록은 12월 초부터 작성하기 시작해야 한다. 내년엔 꼭 회고록을 부�

velog.io

 

Outsider's Dev Story

이 분도 여러가지 주제로 꾸준히 블로그 하시는 분이네요.
IT 뉴스도 유익한 정보가 많습니다.

https://blog.outsider.ne.kr/

 

기술 뉴스 #149 : 20-05-01 :: Outsider's Dev Story

# 웹개발 관련 * **[await의 함정, 숨은 병목을 찾자](https://jaeheon.kr/161)** : `async`, `await`를 사용하면 비동기 코드를 동기처럼 읽기 쉽게 작성할 수 있지만 동시에 실행해도 되는 코드도 `await`로 인...

blog.outsider.ne.kr

 

유명하신분들 블로그 천천히 업로드 예정입니다.

반응형

'서비스평가 및 사용' 카테고리의 다른 글

[heroku] 시작하기 java 배포  (0) 2021.04.29
Buy me a coffee 블로그로 후원받기  (0) 2020.05.26
Github gist 사용해보기  (0) 2020.05.22
반응형

Select SQL 실행순서(오라클 SQL 기준)

 

1) FROM table(s) [alias]

- FROM 절에 사용된 테이블을 인식하여 데이터 딕셔너리에서 관련된 정보들을 파악

 

2) [WHERE condition(s)]

- WHERE절에서 조건에 맞는 데이터를 추출

 

3) [GROUP BY column(s)]

- GROUP BY절이 추가되면 GROUP BY절에 사용된 항목별로 데이터의 정렬이 일어남

 

4) [HAVING condition(s)]

- HAVING 절은 GROUP BY절로 정렬이 된 데이터를 대상으로 조건을 정의.

 

5) SELECT {*, column(s) [alias],...}

- 대부분의 RDBMS가 ROW(로우)기준 저장구조입니다. SELECT이전까지 원하지 않는 칼럼까지도 데이터베이스의 메모리에 저장.

 

6 [ORDER BY column(s) [alias] [DESC],.....];

- ORDER BY절이 가장 나중에 실행.

- SELECT절에서 선택되지 않은 칼럼이나 연산도 데이터베이스의 메모리에 저장되어있으므로 ORDER BY절에서 사용

 

 

정리하자면 다음과 같다.

 

1.FROM 절에서 테이블의 목록을 가져옴
2.WHERE 절에서 검색 조건에 불일치 하는 행 제외
3.GROUP BY 절에서 명시된 행의 값을 그룹화
4.HAVING 절이 GROUP BY 절의 결과 행 중 검색 조건에 불일치 하는 행 제외
5.SELECT 절에서 명시된 열을 정리 
6.ORDER BY 절에서 열을 기준으로 출력할 대상을 정렬 후 출력 

 

반응형
반응형

포인트컷 표현식

execution() : 가장 대표적이고 강력한 지시자로, 접근제어자, 리턴 타입, 타입 패턴, 메서드, 파라미터 타입, 예외 타입 등을 조합해서 메서드까지 선택가능한 가장 정교한 포인트컷을 만들수 있다.
execution([수식어] 리턴타입 [클래스이름].이름(파라미터)

execution(public Integer com.edu.aop.*.*(*))
 - com.edu.aop 패키지에 속해있고, 파라미터가 1개인 모든 메서드

execution(* com.edu..*.get*(..))
 - com.edu 패키지 및 하위 패키지에 속해있고, 이름이 get으로 시작하는 파라미터가 0개 이상인 모든 메서드 

execution(* com.edu.aop..*Service.*(..))
 - com.edu.aop 패키지 및 하위 패키지에 속해있고, 이름이 Service르 끝나는 인터페이스의 파라미터가 0개 이상인 모든 메서드

execution(* com.edu.aop.BoardService.*(..))
 - com.edu.aop.BoardService 인터페이스에 속한 파마리터가 0개 이상인 모든 메서드

execution(* some*(*, *))
 - 메서드 이름이 some으로 시작하고 파라미터가 2개인 모든 메서드

within() : 타입 패턴만을 이용하여 조인포인트를 정의한다.

within(com.edu.aop.SomeService)
 - com.edu.aop.SomeService 인터페이스의 모든 메서드

within(com.edu.aop.*)
 - com.edu.aop 패키지의 모든 메서드

within(com.edu.aop..*)
 - com.edu.aop 패키지 및 하위 패키지의 모든 메서드

this : 빈 오브텍트의 타입의 조인포인트를 정의한다.
target : 대상객체의 타입 비교를 이용한 조인포인트를 정의한다.
args : 메서드의 파라미터 타입만을 이용하여 조인포인트를 정의한다.
@target : 특정 어노테이션이 정의된 객체를 찾는 조인포인트를 정의한다.
@args : 특정 어노테이션을 파라미터로 받는 오브젝트를 찾는 조인포인트를 정의한다.
@within : @target과 유사하게 특정 어노테이션이 정의된 객체를 찾는데, 선택될 조인포인트 메서드는 타겟 클래스에서 선언이 되어있어야 한다.
@annotation : 조인 포인트 메서드에 특정 어노테이션을 찾는 조인포인트를 정의한다.

포인트컷의 조합식에는 or, and, not 3가지를 사용할 수 있으며 각각 ||, &&, !으로 표현할 수 있음.

execution과 @annotation을 주로 사용

반응형

+ Recent posts

Buy Me A Coffee
방문해주셔서 감사합니다. 후원이 큰힘이 됩니다.