티스토리 뷰

  • 아래 내용은 '모던 자바 인 액션'을 읽고 정리한 글로 책 내용의 순서를 따라간다.
  • 상세한 내용이나 예시는 책과 상이할 수 있다.
  • 예제 코드에서 사용되고 있는 스펙
    1. Java 15 preview (record라는 새로운 클래스 개념을 사용하기 위해서 해당 프리뷰 버전을 사용. 하위 버전의 경우는 일반 클래스를 생성한 후 getter를 만들고 사용하면 됨) - 참고
    2. JUnit 5
    3. Gradle 6.7
  • 소스코드

람다(Lambda)란?

  • 람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다.

  • 람다의 특징

    • 익명 : 메서드의 이름이 없기 때문에 구현해야 할 코드에 대한 걱정거리가 줄어든다.
    • 함수 : 메서드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함하지만 클래스에 종속되지 않으므로 함수라고 부른다.
    • 전달 : 메서드 인수로 전달하거나 변수로 저장할 수 있다.
    • 간결성 : 익명 클래스처럼 클래스 이름, 메서드 이름, 파라미터 타입, 반환 타입 등이 없기 때문에 코드가 간결하다.
  • 람다의 구조

    • 파라미터 리스트
      • 파라미터 타입 생략 가능
      • 파라미터가 하나일때 () 생략 가능
    • 화살표
    • 람다 바디
      • 실행 내용이 단일 실행(한 줄)일때 {} 생략 가능. {}이 생략되면 return 키워드와 ;(세미콜론)도 같이 생략해야 한다.
  • 표현식 스타일(expression style) 람다(기본 문법)

      (parameters) -> expression
  • 블록 스타일(block style) 람다

      (parameters) -> { statements; }

Q. 퀴즈-1) 람다 문법

  • 앞에서 설명한 람다 규칙에 맞지 않는 람다 표현식을 고르시오.
    1. () → {}
    2. () → "Raoul"
    3. () → { return "Mario"; }
    4. (Integer i) → return "Alan" + i;
    5. (String s) → { "Iron Man"; }

A. 정답

  • 정답
    • 4번과 5번이 유효하지 않은 람다 표현식이다.
    • 4번은 (Integer i) → { return "Alan" + i; }처럼 되어야 올바른 람다 표현식이다.
    • 5번은 (String s) → "Iron Man" 또는 (String s) → { return "Iron Man"; }처럼 되어야 올바른 람다 표현식이다.

람다 예제

  • 불리언 표현식

      (List<String> list) -> list.isEmpty()
  • 객체 생성

      () -> new Apple(10)
  • 객체에서 소비

      (Apple a) -> {
              System.out.println(a.getWeight());
      }
  • 객체에서 선택/추출

      (String s) -> s.length()
  • 두 값을 조합

      (int a, int b) -> a * b
  • 두 객체 비교

      (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())

어디에, 어떻게 람다를 사용할까?

  • 함수형 인터페이스(Functional Interface)
  • 함수 디스크립터(Function Descriptor)
  • 위의 내용은 따로 포스팅해서 다룰 예정이다.

람다 활용 : 실행 어라운드 패턴

  • 자원 처리 (예를 들면 데이터베이스의 파일 처리)에 사용하는 순환 패턴(recurrent pattern)은 자원을 열고, 처리한 다음에, 자원을 닫는 순서로 이루어진다.
  • 설정(setup)과 정리(cleanup) 과정은 대부분 비슷하다.
  • 아래 그림과 같은 형식의 코드를 실행 어라운드 패턴(execute around pattern)이라고 부른다.
public String processFile() throw IOException {
        try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
                return br.readLine(); // <- 실제 필요한 작업을 하는 행이다.
        }
}

1단계 : 동작 파라미터화를 기억하라

  • 현재 코드는 파일에서 한 번에 한 줄만 읽을 수 있다.
  • 한 번에 두 줄을 읽거나 가장 자주 사용되는 단어를 반환하려면 어떻게 해야 할까?
  • 기존의 설정, 정리 과정은 재사용하고 processFile 메서드만 다른 동작을 수행하도록 명령할 수 있다면 좋을 것이다.
  • 여기서는 processFile의 동작을 파라미터화하면 된다.
  • processFile 메서드가 한 번에 두 행을 읽게 하려면 우선 BufferedReader를 인수로 받아서 String을 반환하는 람다가 필요하다.
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());

2단계 : 함수형 인터페이스를 이용해서 동작 전달

  • 함수형 인터페이스 자리에 람다를 사용할 수 있다.
@FunctionalInterface
public interface BufferedReaderProcessor {
        String process(BufferedReader b) throws IOException;
}
  • 정의한 인터페이스를 processFile 메서드의 인수로 전달할 수 있다.
public String processFile(BufferedReaderProcessor p) throws IOException {
        ...
}

3단계 : 동작 실행

  • 이제 BufferedReaderProcessor에 정의된 process 메서드의 시그니처 (BufferedReader → String)와 일치하는 람다를 전달할 수 있다.
  • 따라서 processFile 바디 내에서 BufferedReaderProcessor 객체의 process를 호출할 수 있다.
public String processFile(BufferedReaderProcessor p) throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
                return p.process(br);
        }
}

4단계 : 람다 전달

// 한 행을 처리하는 코드다.
String oneLine = processFile((BufferedReader br) -> br.readLine());

// 두 행을 처리하는 코드다.
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());

함수형 인터페이스 사용

  • 별도 포스팅

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

형식 검사

  • 람다가 사용되는 컨텍스트(context)를 이용해서 람다의 형식(type)을 추론할 수 있다.
  • 어떤 컨텍스트에서 기대되는 람다 표현식의 형식을 대상 형식(target type)이라고 부른다.
List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);
  • 위 코드의 형식 확인 과정을 보여준다.
    1. filter 메서드의 선언을 확인한다.
    2. filter 메서드는 두 번째 파라미터로 Predicate<Apple> 형식(대상 형식)을 기대한다.
    3. Predicate<Apple>test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스다.
    4. test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터를 묘사한다.
    5. filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 한다.

  • 람다 표현식이 예외를 던질 수 있다면 추상 메서드도 같이 예외를 던질 수 있도록 throws로 선언해야 한다.

같은 람다, 다른 함수형 인터페이스

  • 대상 형식(target typing)이라는 특징 때문에 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있다.
Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
  • 다이아몬드 연산자

    • Java 7에서도 다이아몬드 연산자(<>)로 컨텍스트에 따른 제네릭 형식을 추론할 수 있었다.

    • 주어진 클래스 인스턴스 표현식을 두 개 이상의 다양한 컨텍스트에 사용할 수 있다.

    • 이때 인스턴스 표현식의 형식 인수는 컨텍스트에 의해 추론된다.

      List<String> listOfStrings = new ArrayList<>();
      List<Integer> listOfIntegers = new ArrayList<>();
  • 특별한 void 호환 규칙

    • 람다의 바디에 일반 표현식이 있으면 void를 반환하는 함수 디스크립터와 호환된다.

    • 물론 파라미터 리스트도 호환되어야 한다.

      // Predicate는 불리언 반환값을 갖는다.
      Predicate<String> p = s -> list.add(s);
      // Consumer는 void 반환값을 갖는다.
      Consumer<String> b = s -> list.add(s);
  • 할당문 컨텍스트, 메서드 호출 컨텍스트(파라미터, 반환값), 형변환(cast) 컨텍스트 등으로 람다 표현식의 형식을 추론할 수 있다.

Q. 퀴즈-2) 형식 검사 문제, 다음 코드를 컴파일할 수 없는 이유는?

  • 다음 코드의 문제를 해결하시오.

      Object o = () -> {
              System.out.println("Tricky example");
      };

A. 정답

  • 정답

    • 람다 표현식의 컨텍스트는 Object(대상 형식)다.

    • 하지만 Object는 함수형 인터페이스가 아니다.

    • 따라서 void 형식의 함수 디스크립터를 갖는 Runnable로 대상 형식을 바꿔서 문제를 해결할 수 있다.

        Runnable r = () -> {
                System.out.println("Tricky example");
        };
    • 람다 표현식을 명시적으로 대상 형식을 제공하는 Runnable로 캐스팅해서 문제를 해결할 수도 있다.

        Object o = (Runnable) () -> {
                System.out.println("Tricky example");
        };
    • 같은 함수형 디스크립터를 가진 두 함수형 인터페이스를 갖는 메서드를 오버로딩할 때 이와 같은 기법을 활용할 수 있다.

    • 어떤 메서드의 시그니처가 사용되어야 하는지를 명시적으로 구분하도록 람다를 캐스트할 수 있다

    • 예를 들어 execute(() -> {})라는 람다 표현식이 있다면 RunnableAction의 함수 디스크립터가 같으므로 누구를 가리키는지가 명확하지 않다.

        public void execute(Runnable runnable) {
                runnable.run();
        }
        public void execute(Action<T> action) {
                action.act();
        }
        @FunctionalInterface
        interface Action {
                void act();
        }
    • 하지만 execute((Action) () -> {});처럼 캐스트를 하면 누구를 호출할 것인지가 명확해진다.

형식 추론

  • 자바 컴파일러는 람다 표현식이 사용된 컨텍스트(대상 형식)를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다.
  • 즉, 대상 형식을 이용해서 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론할 수 있다.
  • 결과적으로 컴파일러는 람다 표현식의 파라미터 형식에 접근할 수 있으므로 람다 문법에서 이를 생략할 수 있다.
  • 즉, 자바 컴파일러는 다음처럼 람다 파라미터 형식을 추론할 수 있다.

  • 여러 파라미터를 포함하는 람다 표현식에서는 코드 가독성 향상이 더 두드러진다.

  • 상황에 따라 명시적으로 형식을 포함하는 것이 좋을 때도 있고 형식을 배제하는 것이 가독성을 향상시킬 때도 있다.
  • 어떤 방법이 좋은지 정해진 규칙은 없다.
  • 개발자 스스로 어떤 코드가 가독성을 향상시킬 수 있는지 결정해야 한다.
    • 필자의 경우 잘 모르는 메서드를 쓰거나 할때 간혹 명시하기도 하지만 대부분 생략한다.

지역 변수 사용

  • 람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수(free variable, 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용할 수 있다.
  • 이와 같은 동작을 람다 캡처링(capturing lambda)이라고 부른다.
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
  • 람다는 인스턴스 변수와 정적 변수를 자유롭게 캡처(자신의 바디에서 참조할 수 있도록)할 수 있다.

  • 하지만 그러려면 지역 변수는 명시적으로 final로 선언되어 있어야 하거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야 한다.

  • 즉, 람다 표현식은 한 번만 할당할 수 있는 지역 변수를 캡처할 수 있다.

    • 인스턴스 변수 캡처는 final 지역 변수 this를 캡처하는 것과 마찬가지다.

지역 변수의 제약

  • 인스턴스 변수는 힙에 저장되는 반면 지역 변수는 스택에 위치한다.
  • 람다에서 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있다.

  • 따라서 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공한다.
  • 따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생긴 것이다.
  • 또한 지역 변수의 제약 때문에 외부 변수를 변화시키는 일반적인 명령형 프로그래밍 패턴(병렬화를 방해하는 요소)에 제동을 걸 수 있다.
  • 클로저(closure)
    • 원칙적으로 클로저란 함수의 비지역 변수를 자유롭게 참조할 수 있는 함수의 인스턴스를 가리킨다.
    • 예를 들어 클로저를 다른 함수의 인수로 전달할 수 있고 클로저는 클로저 외부에 정의된 변수의 값에 접근하고, 값을 바꿀 수 있다.
    • Java 8의 람다와 익명 클래스는 클로저와 비슷한 동작을 수행한다.
    • 다만 람다와 익명 클래스는 람다가 정의된 메서드의 지역 변수의 값을 바꿀 수 없다.
    • 람다가 정의된 메서드의 지역 변숫값은 final 변수여야 한다.
      • 인스턴스 변수는 스레드가 공유하는 힙에 존재하므로 특별한 제약이 없다.

메서드 참조(Method References)

  • Java 8의 새로운 기능이다.
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
  • 위와 같이 람다로 표현된 코드가 있을 때 java.util.Comparator.comparing 메서드를 활용해서 다음과 같이 표현할 수 있다.
inventory.sort(comparing(Apple::getWeight));
  • 메서드 참조는 특정 메서드만 호출하는 람다의 축약형이라고 생각할 수 있다.
  • 메서드 참조를 이용하면 기존 메서드 구현으로 람다 표현식을 만들 수 있다.
  • 이때 명시적으로 메서드명을 참조함으로써 가독성을 높일 수 있다.

메서드 참조 만드는 방법

  1. 정적 메서드 참조 : ex) s -> Integer.parseInt(s)Integer::parseInt
  2. 다양한 형식의 인스턴스 메서드 참조 : ex) s -> s.length()String::length
  3. 기존 객체의 인스턴스 메서드 참조 : ex) 람다 외부 변수 apple에 대해 () -> apple.getWeight()apple::getWeight

  • 생성자, 배열 생성자, super 호출 등에 사용할 수 있는 특별한 형식의 메서드 참조도 있다.
  • 컴파일러는 람다 표현식의 형식을 검사하던 방식과 비슷한 과정으로 메서드 참조가 주어진 함수형 인터페이스와 호환하는지 확인한다.
  • 즉, 메서드 참조는 컨텍스트의 형식과 일치해야 한다.

Q. 퀴즈-3) 메서드 참조

  • 다음의 람다 표현식과 일치하는 메서드 참조를 구현하시오.

      1. ToIntFunction<String> stringToInt = (String s) -> Integer.parseInt(s);
      2. BiPredicate<List<String, String>> contains = (list, element) -> list.contains(element);
      3. Predicate<String> startsWithNumber = (String string) -> this.startsWithNumber(string);

A. 정답

  • 정답

      1. ToIntFunction<String> stringToInt = Integer::parseInt
      2. BiPredicate<List<String, String>> contains = List::contains
      3. Predicate<String> startsWithNumber = this::startsWithNumber

생성자 참조

  • ClassName::new처럼 클래스명과 new 키워드를 이용해서 깆곤 생성자의 참조를 만들 수 있다.
  • 인수가 없는 생성자, 즉 Supplier() -> Apple과 같은 시그니처를 갖는 생성자가 있다고 가정하자.
Supplier<Apple> c1 = Apple::new; // Apple()인 디폴트 생성자 참조
Apple a1 = c1.get(); // Supplier의 get 메서드를 호출해서 새로운 Apple 객체를 만들 수 있다.

// 위 코드는 다음과 같다.
Supplier<Apple> c1 = () -> new Apple();
  • Apple(Integer weight)라는 시그니처를 갖는 생성자는 Function 인터페이스의 시그니처와 같다.
Function<Integer, Apple> c2 = Apple::new; // Apple(Integer weight)의 생성자 참조
Apple a2 = c2.apply(110); // Function의 apply 메서드에 무게를 인수로 호출해서 새로운 Apple 객체를 만들 수 있다.

// 위 코드는 다음과 같다.
Function<Apple> c2 = (weight) -> new Apple(weight);
  • 이제 다양한 색과 무게를 갖는 사과를 다음과 같은 방식으로 만들어 보려고 한다.
BiFunction<String, Integer, Apple> c3 = Apple::new; // Apple(String color, Integer weight)의 생성자 참조
Apple a3 = c3.apply(GREEN, 110); // BiFunction의 apply 메서드에 색과 무게를 인수로 제공해서 새로운 Apple 객체를 만들 수 있다.

// 위 코드는 다음과 같다.
BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight);
  • 인스턴스화하지 않고도 생성자에 접근할 수 있는 기능을 다양한 상황에 응용할 수 있다.
static Map<String, Function<Integer, Fruit>> map = new HashMap<>();
static {
        map.put("apple", Apple::new);
        map.put("orange", Orange::new);
        // 등등
}

public static Fruit giveMeFruit(String fruit, Integer weight) {
        return map.get(fruit.toLowerCase()) // map에서 Function<Integer, Fruit>를 얻었다.
                        .apply(weight); // Function의 apply 메서드에 정수 무게 파라미터를 제공해서 Fruit를 만들 수 있다.
}

Q. 퀴즈-4) 생성자 참조

  • 지금까지 인수가 없거나, 하나 또는 둘인 생성자를 생성자 참조로 바꾸는 방법을 살펴봤다.
  • Color(int, int, int)처럼 인수가 세 개인 생성자의 생성자 참조를 사용하려면 어떻게 해야 할까?

A. 정답

  • 정답

    • 생성자 참조 문법은 ClassName::new이므로 Color 생성자의 참조는 Color::new가 된다.

    • 하지만 이를 사용하려면 생정자 참조와 일치하는 시그니처를 갖는 함수형 인터페이스가 필요하다.

    • 현재 이런 시그니처를 갖는 함수형 인터페이스는 제공되지 않으므로 우리가 직접 다음과 같은 함수형 인터페이스를 만들어야 한다.

      public interface TriFunction<T, U, V, R> {
            R apply(T t, U u, V v);
      }
    • 이제 다음처럼 새로운 생성자 참조를 사용할 수 있다.

      TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new;

람다 표현식을 조합할 수 있는 유용한 메서드

  • Java 8 API의 몇몇 함수형 인터페이스는 다양한 유틸리티 메서드를 포함한다.
  • 예를 들어, Comparator, Function, Predicate 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 메서드를 제공한다.
  • 여러 개의 람다 표현식을 조합해서 복잡한 람다 표현식을 만들 수 있다.
  • 디폴트 메서드(default method)가 이것을 가능하게 해준다.
  • 디폴트 메서드에 대해서는 다른 포스팅에서 다루도록 하겠다.

Comparator 조합

Comparator<Apple> c = Comparator.comparing(Apple::getWeight);

역정렬

  • 사과의 무게를 역정렬하고 싶다고 다른 Comparator 인스턴스를 만들 필요가 없다.
  • 인터페이스 자체에서 주어진 비교자의 순서를 뒤바꾸는 reverse라는 디폴트 메서드를 제공하기 때문이다.
inventory.sort(comparing(Apple::getWeight).reversed());

Comparator 연결

  • 무게가 같은 두 사과가 존재한다면 다른 정렬 조건이 필요할 수도 있다.
  • thenComparing 메서드로 두 번째 비교자를 만들 수 있다.
  • thenComparing은 (comparing 메서드처럼) 함수를 인수로 받아 첫 번째 비교자를 이용해서 두 객체가 같다고 판단되면 두 번째 비교자에 객체를 전달한다.
inventory.sort(comparing(Apple::getWeight)
                .reversed()
                .thenComparing(Apple::getCountry)); // 두 사과의 무게가 같으면 국가별로 정렬

Predicate 조합

  • Predicate 인터페이스는 복잡한 프레디케이트를 만들 수 있도록 negate, and, or 세 가지 메서드를 제공한다.
Predicate<Apple> notRedApple = redApple.negate(); // 기존 프레디케이트 객체 redApple의 결과를 반전시킨 객체를 만든다.
Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150); // 두 프레디케이트를 연결해서 새로운 프레디케이트 객체를 만든다.
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(apple -> apple.getWeight() > 150)
                                .or(apple -> GREEN.equals(a.getColor())); // 프레디케이트 메서드를 연결해서 더 복잡한 프레디케이트 객체를 만든다.

Function 조합

  • Function 인터페이스에서 제공하는 람다 표현식도 조합할 수 있다.
  • Function 인터페이스는 Function 인스턴스를 반환하는 andThen, compose 두 가지 디폴트 메서드를 제공한다.
  • andThen 메서드는 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환한다.
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g); // 수학으로는 write g(f(x)) 또는 (g ∘ f)(x)라고 표현
int result = h.apply(1); // 4를 반환
  • compose 메서드는 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인수로 제공한다.
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g); // 수학으로는 f(g(x)) 또는 (f ∘ g)(x)라고 표현
int result = h.apply(1); // 3을 반환

  • 다음처럼 문자열로 구성된 편지 내용을 변환하는 다양한 유틸리티 메서드가 있다고 가정하자.
public class Letter {
        public static String addHeader(String text) {
                return "From Raoul, Mario and Alan: " + text;
        }

        public static String addFooter(String text) {
                return text + " Kind regards";
        }

        public static String checkSpelling(String text) {
                return text.replaceAll("labda", "lambda");
        }
}
  • 여러 유틸리티 메서드를 조합해서 다양한 변환 파이프라인을 만들 수 있다.
Function<String, String> addHeader = Letter::addHeader;
Function<String, String> transformationPipeline = addHeader.andThen(Letter::checkSpelling)
                        .andThen(Letter::addFooter);

비슷한 수학적 개념

적분

$f(x) = x + 10$

$\int_3^7f(x)dx,또는,\int_3^7(x+10)dx$

  • 공학에서는 함수가 차지하는 영역을 묻는 질문이 자주 등장한다.
  • 이 예제에서 함수 f는 직선이므로 사다리꼴 기법으로 정답을 쉽게 찾을 수 있다.
    • 1 / 2 X ((3 + 10) + (7 + 10)) X (7 - 3) = 60
  • 이제 이 공식을 어떻게 자바로 표현할 수 있을까?
  • 먼저 적분 기호나 dy/dx 등 이상한 기호를 어떻게 처리할 것인지가 문제다.
  • 우선은 f와 한계값(여기서는 3.0과 7.0)을 인수로 받는 integrate라는 함수를 만들어야 한다.
    • integrate(f, 3, 7)
  • 그러나 다음처럼 간단히 구현할 수는 없다.
    • integrate(x + 10, 3, 7)
  • 우선 이 식에서는 x의 범위가 불분명하며, f를 전달하는 것이 아니라 x + 10이라는 값을 전달하게 되기 때문에 잘못된 식이다.
  • 수학에서 dx의 정체는 'x를 인수로 받아 x + 10의 결과를 만드는 함수'로 정의할 수 있다.

자바 8 람다로 연결

  • 람다를 이용해서 integrate((double x) -> x + 10, 3, 7) 또는 integrate((double x) -> f(x), 3, 7) 같이 나타낼 수 있다.

  • C가 정적 메서드 f를 포함하는 클래스라 가정하면 메서드 참조를 사용해서 코드를 더 간단하게 만들 수 있다.

    • integrate(C::f, 3, 7)
  • 여기서 integrate 메서드를 구현할 때 DoubleFunction 인터페이스를 사용해서 구현할 수 있다.

      public double integrate(DoubleFunction<Double> f, double a, double b) {
              return (f.apply(a) + f.apply(b)) * (b - a) / 2.0;
      }
  • 또한 DoubleUnaryOperator를 이용해도 결과를 박싱할 필요가 없다.

      public double integrate(DoubleUnaryOperator f, double a, double b) {
              return (f.applyAsDouble(a) + f.applyAsDouble(b)) * (b - a) / 2.0;
      }
  • 참고로 수학처럼 f(a)라고 표현할 수 없고 f.apply(a)라고 구현했는데, 이는 자바가 진정으로 함수를 허용하지 않고 모든 것을 객체로 여기는 것을 포기할 수 없기 때문이다.

댓글