2021. 3. 5.

15주차 과제: 람다식

https://github.com/whiteship/live-study/issues/15

목표

자바의 람다식에 대해 학습하세요.

학습할 것 (필수)

  • 람다식 사용법
  • 함수형 인터페이스
  • Variable Capture
  • 메소드, 생성자 레퍼런스


람다식 사용법

람다 표현식은 자바 8에서 추가됐습니다.

람다식은 기본적으로 함수형 인터페이스(functional interface: 단일 추상 메소드를 가진 인터페이스. 예-java.lang.Runnable)의 인스턴스 표현입니다. 추상 메소드가 하나만 포함되어 있어서 구현 시 메소드의 이름을 생략 가능합니다.

파라미터(매개변수)를 받아 값을 반환하는 짧은 코드 블록입니다.

// 하나의 파라미터
parameter -> expression

// 둘 이상의 파라미터
(parameter1, parameter2) -> expression

// 코드 블록 사용 시
(parameter1, parameter2) -> { code block }

표현식에는 제한이 있습니다. 즉시 값을 반환해야 하며 변수, 값 할당 if나 for 같은 구문을 포함할 수 없습니다. 더 복잡한 작업을 수행하려면 중괄호({ })를 사용해야 합니다. 람다식이 값을 반환해야 하는 경우에는 코드 블록에 return문이 있어야 합니다.

GUI 애플리케이션의 Lambda 표현식

키보드 동작, 마우스 동작, 스크롤 동작과 같은 graphical user interface(GUI) 응용 프로그램에서 이벤트를 처리하려면 일반적으로 특정 인터페이스 구현과 관련된 이벤트 처리기를 만듭니다. 종종 이벤트 핸들러 인터페이스는 함수형 인터페이스(functional interface)입니다.

btn.setOnAction(new EventHandler<ActionEvent>() {

    @Override
    public void handle(ActionEvent event) {
        System.out.println("Hello World!");
    }
});

'btn.setOnAction' 메소드 호출은 btn 객체가 나타내는 단추를 선택할 때 발생하는 작업을 지정합니다. 이 메소드에는 EventHandler<ActionEvent> 타입의 객체가 필요합니다. EventHandler<ActionEvent> 인터페이스에는 void handle(T event) 메소드가 하나만 포함되어 있습니다. 이 인터페이스는 함수형 인터페이스(functional interface)이므로 다음과 같이 람다식을 사용하여 대체할 수 있습니다.

btn.setOnAction(
    event -> System.out.println("Hello World!")
);

변수 할당

Callable c = () -> process();

메소드 파라미터

new Thread(() -> process()).start();

출처:
https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
https://www.w3schools.com/java/java_lambda.asp


함수형 인터페이스

함수형 인터페이스를 나타내는 '@FunctionalInterface' 문서 설명입니다.

Java 언어 사양에 정의된 함수형 인터페이스임을 나타내는데 사용하는 정보 주석 타입입니다. 개념적으로 함수형 인터페이스에는 정확히 하나의 추상 메소드가 있습니다. 기본 메소드(default method)는 구현이 있으므로 추상이 아닙니다. 인터페이스가 java.lang.Object의 공용 메소드 중 하나를 재정의하는 추상 메소드를 선언하면 java.lang.Object 또는 다른 곳에서 구현되므로 추상 메소드 수에 포함되지 않습니다.

함수형 인터페이스의 인스턴스는 람다식, 메소드 참조(reference), 생성자 참조를 사용해 만들 수 있습니다.

이 애노테이션 타입이 적혀있는 경우 컴파일러는 다음 조건이 아닌 경우 오류 메시지를 생성합니다.

  • 인터페이스 타입이며 애노테이션 타입, enum 타입, 클래스가 아닙니다.
  • 애노테이션 타입은 함수형 인터페이스의 요구 사항을 충족합니다.

하지만 컴파일러는 @FunctionalInterface 선언 여부와 관계없이 함수형 인터페이스의 정의를 충족하는 모든 인터페이스를 함수형 인터페이스로 취급합니다.

// 예시
interface FileFilter { boolean accept(File x); }
interface ActionListener { void actionPerformed(…); }
interface Callable<T> { T call(); }

@FunctionalInterface
public interface Runnable {
    // 추상 메소드가 1개
    public abstract void run();
}

@FunctionalInterface
public interface Predicate<T> {
    default Predicate<T> and(Predicate<? super T> p) {…};
    default Predicate<T> negate() {…};
    default Predicate<T> or(Predicate<? super T> p) {…};
    static <T> Predicate<T> isEqual(Object target) {…};
    // 추상 메소드가 1개이기 때문에 함수형 인터페이스
    boolean test(T t);
}

@FunctionalInterface
public interface Comparator {
    // Static, default 메소드 생략
    
    // 오직 1개인 추상 메소드
    int compare(T o1, T o2);
    // equals(객체) 메소드는 Object class에서 온 메소드로 추상 메소드 수에 포함하지 않습니다.
    boolean equals(Object obj);
}

출처:
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/FunctionalInterface.html
https://www.oracle.com/webfolder/technetwork/tutorials/moocjdk8/documents/week1/lesson-1-3.pdf


Variable Capture

로컬, 익명 클래스처럼 람다식도 변수를 캡처할 수 있습니다. 둘러싼 범위(enclosing scope) 내에서 로컬 변수에 대해 동일한 액세스 권한을 가집니다. 그러나 로컬 및 익명 클래스와 달리 람다식에는 shadowing 문제가 없습니다. 람다 표현식은 문법적으로 범위가 지정됩니다. 상위타입(supertype)에서 이름을 상속하지 않고 새로운 범위 지정을 하지 않습니다. 람다식의 선언은 둘러싼 환경에 따라 해석됩니다. 다음 예제 LambdaScopeTest는 이를 보여줍니다.

package com.test.mytest;

import java.util.function.Consumer;

public class LambdaScopeTest {

    public int x = 0;

    class FirstLevel {
        public int x = 1;

        void methodInFirstLevel (int x) {
            Consumer<Integer> myConsumer = (y) ->
            {
                System.out.println("x = " + x);
                System.out.println("y = " + y);
                System.out.println("this.x = " + this.x);
                System.out.println("LambdaScopeTest.this.x = " + LambdaScopeTest.this.x);
            };
            myConsumer.accept(x);
        }
    }

    public static void main(String[] args) {
        LambdaScopeTest st = new LambdaScopeTest();
        LambdaScopeTest.FirstLevel f1 = st.new FirstLevel();
        f1.methodInFirstLevel(23);
    }
}
x = 23
y = 23
this.x = 1
LambdaScopeTest.this.x = 0

Process finished with exit code 0

Interface Consumer<T>: 입력 인수(argument)를 받아서 결과값을 반환하지 않는 작업을 합니다. 대부분 다른 함수형 인터페이스와 달리 Consumer는 부작용을 예상하고 작동합니다.

만약 람다식 myConsumer 매개변수(parameter) y를 x로 교체하면 컴파일 오류를 생성합니다.

람다식이 새로운 범위를 지정하지 않았기 때문에 컴파일러에서 "변수 x가 methodInFirstLevel(int)에 이미 정의되어 있습니다."라는 오류를 생성합니다. 결과적으로 둘러싼 범위(enclosing scope)의 필드, 메소드, 로컬 변수에 직접 액세스 할 수 있습니다. 예를 들어 람다식은 methodInFirstLevel 메소드의 매개변수(parameter)에 직접 접근합니다. 근접 클래스(enclosing class) 변수에 접근하려면 this 키워드를 사용합니다. 예시에서 this.x는 맴버 변수 FirstLevel.x를 참조합니다.

하지만 로컬 및 익명 클래스처럼 람다식은 근접 블록 내의 final 또는 사실상 final인 로컬 변수와 매개변수(parameter)에만 접근할 수 있습니다. 예를 들어 methodInFirst 바로 뒤에 'x = 99;' 할당문을 추가한다고 가정해 봅시다.

이 할당문 때문에 FirstLevel.x 변수는 사실상 final이 아닙니다. 결과적으로 "람다식에서 참조된 로컬 변수가 final 혹은 사실상 final이어야 합니다."와 유사한 오류 메세지를 생성합니다.

출처: https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html#accessing-local-variables


메소드, 생성자 레퍼런스

메소드 레퍼런스

람다식을 사용해 익명 메소드를 만듭니다. 하지만 람다식은 종종 기존 메소드를 호출하는 것 외에는 아무것도 하지 않습니다. 이런 경우 기존의 메소드 이름을 참조하는 것이 더 명확합니다. 메소드 참조(reference)를 사용해 이미 이름이 있는 메소드를 간결하고 읽기 쉬운 람다식으로 사용합니다.

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;

    public int getAge() {
        // ...
    }
    
    public Calendar getBirthday() {
        return birthday;
    }    

    public static int compareByAge(Person a, Person b) {
        return a.birthday.compareTo(b.birthday);
    }}

    // ...

애플리케이션의 멤버가 배열에 포함되어 있고 배열을 연령별로 정렬한다고 가정합니다.

Person[] rosterAsArray = roster.toArray(new Person[roster.size()]);

class PersonAgeComparator implements Comparator<Person> {
    public int compare(Person a, Person b) {
        return a.getBirthday().compareTo(b.getBirthday());
    }
}
        
Arrays.sort(rosterAsArray, new PersonAgeComparator());

인터페이스 Comparator는 함수형 인터페이스입니다. 따라서 Comparator를 구현(implement)하는 클래스의 새 인스턴스를 정의하고 만드는 대신 람다식을 사용할 수 있습니다.

Arrays.sort(rosterAsArray,
    (Person a, Person b) -> {
        return a.getBirthday().compareTo(b.getBirthday());
    }
);

하지만 두 Person 인스턴스의 생년월일을 비교하는 메소드는 이미 Person.compareByAge로 존재합니다. 람다식의 본문에서 이 메소드를 호출할 수 있습니다.

Arrays.sort(rosterAsArray,
    (a, b) -> Person.compareByAge(a, b)
);

람다식은 기존의 메소드를 호출하므로 람다식 대신에 메소드 레퍼런스를 사용할 수 있습니다.

Arrays.sort(rosterAsArray, Person::compareByAge);

메소드 레퍼런스 'Person::compareByAge'는 람다식 '(a, b) -> Person.compareByAge(a, b)'와 의미상 동일합니다. 다음과 같은 특징이 있습니다.

  • 형식 인자(formal parameter) 리스트 (Person, Person)는 Comparator<Person>.compare에서 복사됩니다.
  • 본문은 Person.compareByAge 메소드를 호출합니다.


생성자 레퍼런스

new 이름을 사용해 static 메소드와 동일한 방법으로 생성자를 참조할 수 있습니다. 다음 메소드는 컬렉션에서 다른 컬렉션으로 요소(elements)를 복사합니다.

public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>>
    DEST transferElements(
        SOURCE sourceCollection,
        Supplier<DEST> collectionFactory) {
        
        DEST result = collectionFactory.get();
        for (T t : sourceCollection) {
            result.add(t);
        }
        return result;
}

함수형 인터페이스 Supplier에는 인수(argument)를 받지 않고 객체를 반환하는 메소드가 있습니다. 따라서 다음과 같이 람다식을 사용해 'transferElements' 메소드를 호출할 수 있습니다.

Set<Person> rosterSetLambda =
    transferElements(roster, () -> { return new HashSet<>(); });

다음과 같이 람다식 대신 생성자 레퍼런스를 사용할 수 있습니다.

Set<Person> rosterSet = transferElements(roster, HashSet::new);

자바 컴파일러는 당신이 Person 타입 요소를 포함하는 HashSet collection을 만들고 싶어한다고 추론합니다. 대안으로 다음과 같이 작성할 수 있습니다.

Set<Person> rosterSet = transferElements(roster, HashSet<Person>::new);

출처: https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html