[책] Java8 in Action

12 분 소요 |

자바 8 버전은 함수형 언어로 가기 위한 준비 단계의 느낌이다. 결국 모든 언어의 끝은 함수형 언어일까.

자바 9이 발표되기까지 52일 남았으니 아직 자바 8을 보지 못했다면 이번 기회에 한번 공부해보자.

Java8 in Action 정리


1. 자바 8을 눈여겨봐야 하는 이유

Java8의 핵심은 간결한 코드와 멀티코어 프로세서의 간단한 활용이다.

  • 왜 변화하는가?
    • 모든 언어는 장단점을 가지고 있고
    • 진화하지 않은 기존 언어는 사장된다.
    • 새로 프로그래밍을 배우는 사람은 자연스럽게 새로운 언어를 선택하게 되며 기존 언어는 도태된다.
  • 자바가 성공한 이유는
    • 모든 것은 객체다.
    • 한번 작성하면 어디서든 실행할 수 있다. JVM위에서
    • 초반엔 JVM으로 인해 실행시간이 느렸지만 하드웨어가 발전하면서 프로그래머의 시간이 더 중요한 요소로 부각됐다.
  • Java8에서 제공하는 3가지 프로그래밍 개념
    • 스트림 처리
    • 동작 파라미터화로 메서드에 코드 전달하기. lambda
    • 병렬성과 공유 가변 데이터
  • Java 함수
    • 메서드와 람다를 일급 시민으로 만들자.
      • 일급 시민 : 전달할 수 있는 값
      • 이급 시민 : 전달할 수 없는 값. 이전 자바의 메서드, 클래스 등
    • 메서드 레퍼런스 ::
    • 람다. 익명함수
  • 스트림
  • 디폴트 메서드
  • 함수형 프로그램에서 가져온 아이디어
    • 메서드와 람다를 일급값으로 사용하자.
    • 가변 공유 상태가 없는 병렬 실행을 이용해서 효율적이고 안전하게 함수나 메서드를 호출하자.
  • 람다가 몇 줄 이상으로 길어진다면
    • 익명 람다보다는 코드가 수행하는 일을 잘 설명하는 이름을 가진 메서드를 정의하고 메서드 레퍼런스를 활용하는 것이 바람직하다.
    • 코드의 명확성이 더 중요하기 때문이다.

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

  • 동작 파라미터화
    • behavior parameterization
    • 아직 어떻게 실행할 것인지 결정하지 않은 코드 블럭을 의미한다.
    • 전략 디자인 패턴을 구현하는 방법
    • Predicate<T> (boolean을 반환하는 함수)를 사용해서 구현
    • 클래스나 익명 클래스로도 구현가능하지만 장황하다.
    • 람다로 구현하는게 유연하고 간결함.
  • 코드의 장황함은 나쁜 특성이다
    • 장황한 코드는 구현하고 유지보수하는 데 시간이 오래 걸릴 뿐 아니라 읽는 즐거움을 빼앗는 요소로 개발자로부터 외면받는다

3. 람다 표현식

  • 람다
    • 메서드로 전달할 수 있는 익명 함수를 단순화한 것
    • 익명, 함수, 전달가능, 간결함
// (파라미터 리스트) -> 람다 바디
(String s) -> s.length();

(Apple a) -> a.getWeight > 150;

(int x, int y) -> {
    System.out.println("Result:");
    System.out.println(x + y);
};

() -> 42;

() -> new Apple(10);

함수형 인터페이스

  • functional interface
  • 정확히 하나의 추상 메서드만 지정하는 인터페이스
  • 함수형 인터페이스를 가리키는 애노테이션 @FunctionalInterface
  • 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있다

  • 함수형 인터페이스를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있다
    • 왜?
    • 자바에 함수 형식을 추가하는 방법도 고려했지만 언어를 더 복잡하게 만드는것 같아 선택안함
    • 대부분의 자바 프로그래머는 하나의 추상 메서드를 갖는 인터페이스에 이미 익숙하다는 점도 고려
  • 함수 디스크립터
    • function descriptor
    • 함수형 인터페이스의 추상 메서드 시그니처
    • 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다
    • 즉 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 부른다
  • Predicate
    • T 형식의 객체를 받아 boolean 결과가 필요할 때
    • Predicate<T>가 바로 함수형 인터페이스
      @FunctionalInterface
      public interface Predicate<T> {
        boolean test(T t);
      }
      
  • Consumer
    • T 형식의 객체를 받아 어떤 동작을 수행하고 싶을 때
      @FunctionalInterface
      public interface Consumer<T> {
        void accept(T t);
      }
      
  • Function
    • T를 받아 R 객체를 반환할 때
      @FunctionalInterface
      public interface Function<T, R> {
        R apply(T t);
      }
      
  • 자바의 모든 형식은 참조형 아니면 기본형
    • 참조형
      • reference type
      • Byte, Integer, Object, List
    • 기본형
      • primitive type
      • int, double, byte, char
    • 제네릭 파라미터에는 참조형만 사용 가능
      • 제네릭의 내부 구현때문에 어쩔 수 없음
    • 기본형 -> 참조형으로 변환 : 박싱. boxing
    • 참조형 -> 기본형으로 변환 : 언박싱. unboxing
    • 박싱과 언박싱이 자동으로 이루어지는 오토박싱. autoboxing
      • 물론 변환과정에 비용이 소모
      • 박싱한 값은 기본형을 감싸는 래퍼 형태. 힙에 저장.
      • 따라서 박싱한 값은 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색하는 과정이 필요
  • 람다 표현식의 파라미터 형식 추론
    • 상황에 따라 형식을 포함하는게 좋을 때도 있고 생략하는게 좋을 때도 있다
    • 어떤 게 가독성이 더 좋은지를 보고 판단하자

형식검사, 형식추론, 제약

  • 람다가 사용되는 context를 이용해서 람다의 type을 추론할 수 있음
  • 익명 함수처럼 람다도 자유 변수를 활용할 수 있음
    • 이와 같은 동작을 람다 캡쳐링이라고 부른다. capturing lambda
    • 자유 변수
      • free variable
      • 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수
    • 단 final 변수와 똑같이 사용해야 함
      • 왜?
      • 인스턴스 변수는 힙에 저장
      • 반면 지역변수는 스택에 위치
      • 람다에서 지역변수에 바로 접근하는경우
        • 변수를 할당한 스레드가 사라지면 변수도 해제되는데도
        • 람다를 실행하는 스레드에서 변수에 접근하려는 시도가 있을 수 있음.
        • 따라서 제약을 만들어버림

메서드 레퍼런스

  • 특정 메서드만을 호출하는 람다의 축약형
  • 가독성을 높일 수 있다
  • 메서드명 앞에 구분자 ::를 붙이는 방식
(Apple a) -> a.getWeight
Apple::getWeight

() -> Thread.currentThread().dumpStack()
Thread.currentThread()::dumpStack

(str, i) -> str.substring(i)
String::substring

(String s) -> System.out.println(s)
System.out::println

() -> expensiveTransaction.getValue()
expensiveTransaction::getValue

Apple::new;

4. 스트림 소개

  • 스트림이란 데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소
  • 중간연산
  • 최종연산

5. 스트림 활용

필터링과 슬라이싱

  • filter
    • .filter(Dish::isVegetarian)
    • .filter(i -> i % 2 == 0)
  • distinct
    • 고유한 요소 필터링
    • .distinct
  • limit
    • 스트림 축소
    • .limit(3)
  • skip
    • 요소 건너뛰기
    • .skip(2)

매핑

  • map
    • 스트림의 각 요소에 함수 적용하기
    • .map(Dish::getName)
    • .map(String::length)
    • .map(n -> n * n)
  • flapMap
    • 스트림 평면화
    • 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결
    • .flapMap(Arrays::stream)

검색과 매칭

  • anyMatch
    • 적어도 한 요소와 일치하는지 검사
    • 최종연산
    • .anyMatch(Dish::isVegetarian)
  • allMatch
    • 모든 요소와 일치하는지 검사
    • 최종연산
    • .allMatch(d -> d.getCalories() < 1000)
  • noneMatch
    • 일치하는 요소가 없는지 검사
    • 최종연산
    • .nonMatch(d -> d.getCalories() < 1000)
  • 쇼트서킷
    • 자바의 && , || 같은 연산
    • 하나라도 거짓이라는 결과가 나오면 나머지 표현식은 평가하지 않고 결과를 반환하는 상황을 부르는 말
    • anyMatch, allMatch, noneMatch, limit 등은 쇼트서킷
  • findAny
    • 임의의 요소를 반환. 랜덤
    • 최종연산
    • .findAny()
    • 아무 요소도 반환하지 않을 수 있음
    • 그래서 null 대신 Optional<T>를 반환
  • Optional
    • null은 쉽게 에러를 만들 수 있으므로 대신 사용
    • isPresent() : 값이 있는가 여부
    • ifPresent(Consumer<T> block) : 값이 있으면 주어진 블록 실행
    • T get() : 값이 있으면 반환, 없으면 NoSuchElementException
    • T orElse(T other) : 값이 있으면 반환, 없으면 기본값 반환
        menu.stream()
        .filter(Dish::isVegetarian)
        .findAny
        .ifPresent(d -> System.out.println(d.getName()));
      
  • findFirst
    • 첫번째 요소 찾기
    • Optional<T>를 반환
    • .findFirst()

리듀싱

  • 모든 스트림 요소를 처리해서 값으로 도출
    • 함수형 프로그래밍 언어로는 종이를 작은 조각이 될 때까지 반복해서 접는 것과 비슷하다는 의미로 폴드 fold라고 부름
  • 합 구하기
    int sum = numbers.stream()
              .reduce(0, (a, b) -> a + b);
    
    • 초기값이 없으면
      Optional<Integer> sum = numbers.stream()
                            .reduce((a, b) -> a + b);
      
  • 최대값 구하기
    Optional<Integer> max = numbers.stream()
                              .reduce(Integer::max);
    

숫자 스트림

  • 기본형 특화 스트림
    • 박싱 비용을 피하기 위해 제공
    • IntStream, DoubleStream, LongStream
    • .mapToInt(Dish::getCalories)
    • max, min, average 등 다양한 유틸리티 메서드 지원
    • 객체 스트림으로 복원하려면
        IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
        Stream<Integer> stream = intStream.boxed();
      
    • 스트림에 요소가 없을 때 기본 값
    • OptionalInt, OptionalDouble, OptionalLong
        OptionalInt maxCalroies = menu.stream().mapToInt(Dish::getCalories).max();
        int max = maxCalroies.orElse(1);
      
  • 숫자범위
    • .range(1, 100) : 1과 100은 미포함
    • .rangeClosed(1, 100) : 모두 포함
  • 스트림만들기
    • 값으로 만들기
        Stream.of("java8", "lambdas", "in", "action")
        Stream.empty()
      
    • 배열로 만들기
        int[] numbers = {2, 3, 5, 7, 11, 13}
        int sum = Arrays.stream(numbers).sum();
      
    • 파일로 만들기
        Stream<String> line = Files.lines(Paths.get("data.txt"), Charset.defaultCharset());
        long uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
                            .distinct()
                            .count();
      
    • 함수로 만들기
      • 무한 스트림. infinite stream. 즉 크기가 고정되지 않은 스트림
      • 언바운드 스트림. unbound stream 이라고도 표현
      • iterate와 generate로 만든 스트림은 요청할 때마다 주어진 함수를 이용해서 값을 만든다
      • 따라서 무제한으로 값을 계산할 수 있음. 그래서 당연히 limit이 필요
      • iterate
        • 기존 결과에 의존해서 순차적으로 연산을 수행
            Stream.iterate(0, n -> n + 2)
            .limit(10)
            .forEach(System.out::println)
          
      • generate
        • iterate와 달리 생산된 각 값을 연속적으로 계산하지 않음
            Stream.generate(Math::random)
            .limit(5)
            .forEach(System.out::println)
          
    • 스트림을 병렬로 처리하면서 올바른 결과를 얻으려면 불변상태기법을 고수해야 한다.
  • 스트림연산의 상태
    • 내부 상태 없음
      • stateless operation
      • map, filter
      • 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보내는 연산
    • 내부 상태 있음
      • stateful operation
      • reduce, sum, max
        • 결과를 누적할 내부 상태가 필요
      • sorted, distinct 도 마찬가지
        • 요소를 정렬하거나 중복을 제거하려면 과거의 이력을 알고 있어야 함
        • 따라서 모든 요소가 버퍼에 추가되어 있어야 한다
        • 그래서 이러한 연산은 내부 상태를 갖는 연산

6. 스트림으로 데이터 수집

  • collect
    • 고급 리듀싱 기능을 수행하는 컬렉터
    • 스트림에 collect를 호출하면 컬렉터로 파라미터화된 리듀싱 연산을 수행
    • collect 연산은 Collectors.reducing 연산으로 모두 구현 가능
      • 하지만 편의성과 가독성을 위해 정의해놓은 리듀싱 연산 사용하자
      • 게다가 병렬성을 확보하려면 collect 메서드가 바람직
  • toList
  • toSet
  • toCollection
    • stream().collect(toCollection(), ArrayList::new)

리듀싱과 요약

  • counting
      long howManyDishes = menu.stream().collect(Collectors.counting());
      long howManyDishes = menu.stream().count();
    
  • summingInt
    • 객체를 int, double, long으로 매핑한 컬렉터를 반환
        int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
      
    • summingDouble
    • summingLong
  • averagingInt
    • 숫자 집합의 평균
        double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories));
      
    • averagingLong
    • averagingDouble
  • summarizingInt
    • 요소의 수, 합계, 평균, 최댓값, 최솟값 등의 요약 정보
        IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
      
    • summarizingLong
    • summarizingDouble

문자열

  • joining
    • 스트림의 각 객체에 toString 호출해서 하나의 문자열로 연결
    • 내부적으로 StringBuilder 사용
      String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
    
      // Dish.toString 이 getName 이라면
      String shortMenu = menu.stream().collect(joining(", "));
    

그룹화

  • groupingBy
    • 함수를 기준으로 스트림을 그룹화
      Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));
    
      // 기준이 없는 경우
      public enum CaloricLevel { DIET, NORMAL, FAT};
    
      Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(
                  groupingBy(dish -> {
                      if (dish.getCalroies() <= 400) {
                          return DIET;
                      } else if (dish.getCalroies <= 700) {
                          return NORMAL;
                      } else {
                          return FAT;
                      }
                  })
              );
    
    • groupingBy를 중첩해서 사용하면 n차원의 맵을 만들 수 있음
  • 서브그룹으로 데이터 수집
      Map<Dish.Type, Long> typesCount = menu.stream()
                      .collect(groupingBy(Dish::getType, counting()));
    
  • collectingAndThen
    • 적용할 컬렉터와 변환 함수를 인수로 받아 한번 래핑한 다른 컬렉터를 반환

분할

  • Boolean을 반환하는 프리디케이트를 분류 함수로 사용하는 특수한 그룹화
Map<Boolean, List<Dish>> partitionedMenu = menu.stream()
                    .collect(partitioningBy(Dish::isVegetarian));
List<Dish> vegetarianDishes = partitionedMenu.get(true);

// filter를 사용해도 동일
List<Dish> vegetarianDishes = menu.stream().filter(Dish::isVegetarian).collect(toList());

커스텀 Collector

  • Collector interface를 구현하면 만들 수 있다

7. 병렬 데이터 처리와 성능

병렬 스트림

  • .parallel() 병렬로 처리
  • .sequential() 순차적으로 처리
  • 소프트웨어 공학에서 추측은 위험한 방법이다! 특히 성능을 최적화할 때는 3가지 황금 규칙을 기억해야 한다
    • 첫째도 측정, 둘째도 측정, 셋째도 측정!
  • 병렬화는 공짜가 아니다
    • 스트림을 재귀적으로 분할해야 하고
    • 각 서브스트림을 서로 다른 스레드의 리듀싱 연산으로 할당
    • 이들 결과를 하나의 값으로 합쳐야 한다
  • 병렬 스트림의 올바르게 사용하려면
    • 공유 가변 상태를 피해야 한다
  • 병렬 스트림을 효과적으로 사용하려면
    • 잘 모르겠으면 일단 측정고고
    • 박싱 언박싱 주의. 성능을 크게 저하시킬 수 있음
    • limit 이나 findFirst 처럼 요소의 순서에 의존하는 연산은 병렬 스트림에서 성능이 떨어짐
    • 전체 파이프라인 연산 비용을 고려
    • 소량의 데이터에서는 별로
    • 자료구조가 적절한지 확인. 분할하기 쉬운가
      • Best : ArratList, IntStream.range
      • Good : HashSet, TreeSet
      • Bad : LinkedList, Stream.iterate
    • 중간연산으로 인해 분해 과정의 성능이 달라질 수 있음
    • 최종 연산의 병합 과정 비용 확인

포크/조인 프레임워크

  • 병렬 스트림은 내부적으로 Java7에 추가된 포크/조인 프레임워크로 처리된다

8. 리팩토링, 테스팅, 디버깅

  • 익명 클래스를 람다로 변환하기
  • 람다 표현식을 메서드 레퍼런스로 표현하기
  • 명령형 데이터 처리를 스트림으로 리팩토링하기
  • 람다로 디자인 패턴 구현
    • 전략 패턴
    • 템플릿 메서드 패턴
    • 옵저버 패턴
    • 의무 체인 패턴
    • 팩토리 패턴
  • 람다 디버깅은 어려울 수 있음.

9. 디폴트 메서드

  • 기본 구현을 포함하는 인터페이스
public interface Sized {
    int size();

    default boolean isEmpty() {
        return size() == 0;
    }
}
  • 기존에 존재하는 구현체들을 변경하지 않고 인터페이스에 새로운 기능을 넣기 위해 탄생
  • 추상 클래스와 비슷한데? 다른 점은?
    • 공통 상태. 즉 변수를 가질 수 없다는 것
    • 다중 상속 가능
  • 같은 시그니처를 가진 디폴트 메서드가 여러개라면 어떻게 처리?
    • 3가지 규칙을 따라야 한다
      1. 클래스가 항상 우선. 디폴트 메서드보다 클래스/서브클래스에서 정의한 메서드가 우선.
      2. 그 다음은 서브 인터페이스가 우선. 인터페이스가 상속 관계라면 자식 인터페이스 먼저
      3. 그래도 결정 안되면 컴파일 에러. 클래스가 명시적으로 디폴트 메서드를 오버라이드해야 함
  • C++의 다이아몬드 문제를 Java는 쉽게 해결할 수 있다

10. Optional

  • null 레퍼런스 및 값이 없는 상황을 만든 호어는 몇년 후 이렇게 말했다고
    • null 및 예외를 만든 결정은 억만 달러짜리 실수
  • null 로 발생하는 문제
    • 에러의 근원
    • null 체크 코드로 어지럽다
    • 아무 의미가 없다
    • 자바 철학에 위배. 포인터를 다 숨겼는데 유일하게 남은 포인터가 null
    • 형식 시스템에 구멍을 만든다
  • Optional<T>
  • 선택형 값을 캡슐화하는 클래스
  • 값이 없을 수도 있다는 의미를 더 명확하게 표현
  • 즉 Optional을 이용하면 값이 없는 상황이 우리 데이터에 문제가 있는 것인지 아니면 버그인지 명확하게 구분 가능
Optional<Car> optCar = Optional.empty();
Optional<Car> optCar = Optional.of(car);

// car가 null 이면 빈 Optional 객체 반환
Optional<Car> optCar = Optional.ofNullable(car);
  • 주의! Optional 클래스를 모델의 필드 형식으로 사용하지 말것. 직렬화 안됨.
    • 도메인 모델에서 값의 유무를 설명하는 용도로만 사용할 것.
  • get
  • orElse
  • orElseGet
  • orElseThrow
  • ifPresent
  • filter

11. CompletableFuture

// 인스턴스를 생성하는 방식
public Future<Double> getPriceAsync(String product) {
    CompletableFuture<Double> futurePrice = new CompletableFuture<>();
    new Thread() -> {
        try {
            double price = calculatePrice(product);
            futurePrice.complete(price);
        } catch (Exception e) {
            futurePrice.completeExceptionally(ex);
        }
    }.start();

    return futurePrice;
}

// supplyAsync 로 만드는 방식
public Future<Double> getPriceAsync(String product) {
    return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}

  • 컬렉션 계산을 병렬화하는 2가지 방법
  1. 스트림으로 병렬화
    • I/O를 포함하지 않은 계산 중심의 동작에는 효율적이다.
     public List<String> findPrices(String product) {
         return shops.parallelStream()
                 .map(shop -> shop.getName() + " price is " + shop.getPrice(product))
                 .collect(toList());
     }
    
  2. CompletableFuture로 병렬화
    • I/O를 포함한다면 이게 더 적절하다. 스트림은 lazy 연산때문에 I/O를 실제로 언제 처리할 지 예측하기 힘들다.
     // 스트림으로 요청 병렬화하기
     public List<String> findPrices(String product) {
         List<CompletableFuture<String>> priceFutures =
             shops.streams()
             .map(shop -> CompletableFuture.supplyAsync(
                 () -> shop.getName() + " price is " + shop.getPrice(product)
             ))
             .collect(toList());
    
         return priceFutures.stream()
                     .map(CompletableFuture::join)
                     .collect(toList());
     }
    
  • Executor 의 스레드 풀 크기 조절
    • 스레드 풀이 너무 크면 CPU와 메모리 자원을 서로 경쟁하느라 시간을 낭비할 수 있다.
    • 스레드 풀이 너무 작으면 CPU의 일부 코어는 논다.
    • 게츠의 공식
      • N_threads = N_CPU * U_CPU * (1 + W/C)
      • 스레드수 = CPU 코어 개수 * 0~1 사이의 CPU 활용비율 * (1 + 대기시간 / 계산시간)
  • 작업 조합하기
    • thenXXX
    • thenApply
    • thenCompose
    • thenCombine
  • 모든 작업 기다리기 : CompletableFuture.allOf(futures).join()
  • 처음으로 완료한 작업만 기다리기 : CompletableFuture.anyOf(futures)

12. 새로운 날짜와 시간 API

  • LocalDate
  • LocalTime
  • LocalDateTime
  • Instant
  • Duration
  • Period
  • TemporalAdjusters
  • ZoneId

13. 함수형 관점으로 생각하기

  • 상태를 바꾸지 않고 결과만을 반환하는 메서드. 순수메서드. 부작용없는 메서드
  • pure function
  • side-effect free function

  • 함수형 프로그래밍
    • 함수를 이용하는 프로그래밍
    • 여기서 함수는 수학적인 함수와 동일
    • 0개 이상의 인수를 가지며 1개 이상의 결과를 반환
    • 부작용이 없어야 한다
  • 참조 투명성
    • referential transparency
    • 같은 인수로 함수를 호출했을 때 항상 같은 결과를 반환
    • 참조적으로 투명한 함수
  • 프로그래밍 형식을 스펙트럼으로 표현하자면
    • 한쪽 끝에는 익스트림 객체지향 방식이 위치
      • 모든 것은 객체
      • 프로그램이 객체의 필드를 갱신
    • 다른쪽 끝에는 함수형 프로그래밍 형식이 위치
      • 참조적 투명성을 중시
      • 변화를 허용하지 않음
  • 팩토리얼 재귀의 꼬리 호출 최적화

14. 함수형 프로그래밍 기법

  • 일급 함수
    • first class function
    • 인수로 전달하거나
    • 결과로 반환받거나
    • 자료구조에 저장할 수 있는
    • 일반 값처럼 취급할 수 있는 함수
      Function<String, Integer> strToInt = Integer::parseInt;
    
  • 고차원 함수
    • high order function
    • 한 개 이상의 함수를 인수로 받아서 다른 함수를 반환하는 함수
  • 커링
    • currying
    • 함수를 모듈화하고 코드를 재사용할 수 있도록 지원하는 기법
    • f(x, y) 를 (g(x))(y) 로 대체하는 기법
    • 즉 함수 f 와 함수 g가 최종적으로 반환하는 값은 같음
    • 이 과정이 끝까지 완료되지 않은 상태를 가리켜 함수가 부분적으로 적용되었다라고 말한다
  • 영속 자료 구조
    • 함수하나가 자료구조를 갱신하는 것은 부수효과임. side effect
    • 같은 메서드를 2번 호출하면 결과가 달라진다. 즉 인수를 결과로 단순하게 매핑할 수 없음
    • 즉 참조 투명성에 위배된다. 즉 인수가 같다면 결과도 같아야 한다
    • 결과 자료구조를 바꾸지 말라
    • 함수를 호출해도 중간에 자료구조를 갱신하지 않고 새로운 자료구조를 반환함
      • 이 과정에서 가능한한 기존의 자료구조를 재사용
      • 그러기 위해 기존 자료구조를 인수로 받음
  • 스트림과 게으른 평가
    • 처리할 필요가 있을 때만 스트림을 실제로 평가
    • 스트림의 최종 연산을 적용해서 실제 계산을 해야하는 상황에서만 실제 연산이 이루어짐
    • 스칼라 코드로는
      def numbers(n: Int): Stream[Int] = n #:: numbers(n+1)
    
  • 패턴매칭
  • 메모라이제이션

15. OOP와 FP의 조화. 자바8과 스칼라 비교

  • 스칼라 문법 설명
  • object : 클래스를 정의하고 동시에 인스턴스화. 싱글톤
object Beer {
    def main(args: Array[String]) {
        2 to 6 foreach {
            n => println("beer")
        }
    }
}
  • val 읽기 전용. 즉 변수에 값 할당 못함. 자바의 final
  • var 읽고 쓸 수 있는 변수

컬랙션

val authorsToAge = Map("Raoul" -> 23, "Mario" -> 40, "Alan" -> 53)
val authors = List("Raoul", "Mario", "Alan")
val numbers = Set(1, 1, 2, 3, 5, 8)

// 튜플
val raoul = ("Raoul", "19")  // (String, Int) 형식의 튜플
val alan = ("Alan", "35")
  • 기본적으로 컬렉션은 불변이라는 점을 기억하자
  • 일단 만들면 변경할 수 없다
val numbers = Set(2, 5, 3)
val newNumbers = numbers + 8   // (2, 5, 3, 8)
  • 필터
  • Option
    • getOrElse

함수

  • 일급 함수
def isJavaMentioned(tweet: String) : Boolean = tweet.contains("Java")
def isShortTweet(tweet: String) : Boolean = tweet.length() < 20

val tweets = List("...", "...", "...")
tweets.filter(isJavaMentioned).foreach(println)
tweets.filter(isShortTweet).foreach(println)
  • 익명 함수
val isLongTweet : String => Boolean = (tweet : String) => tweet.length() > 60
  • 클로저
  • 커링

클래스와 트레이트

  • getter와 setter는 암시적으로 생성됨
  • 트레이트
    • 자바의 interface랑 비슷
    • 인스턴스화 과정에서도 조합할 수 있음. 조합 결과는 컴파일 타임에 결정
trait Sized {
    var size : Int = 0
    def isEmpty() = size == 0
}

class Empty extends Sized
println(new Empty().isEmpty())

class Box
var b1 = new Box() with Sized
println(b1.isEmpty)

var b2 = new Box()
b2.isEmpty()    // 컴파일 에러

16. 결론 그리고 자바의 미래

  • 커다란 2 가지 추세
    • 멀티코어 프로세서의 파워를 충분히 활용해야 함. 즉 코드를 병렬로 실행해야 더 빠름
    • 간결하게 데이터 컬렉션을 다루는 추세. 그럴려면 불변이 최고.

업데이트 :