본문 바로가기

프로그래밍/Book

[모던 자바 인 액션] 3장. 람다 표현식

람다를 어디에 사용할수 있을까?

함수형 인터페이스

정확히 하나의 추상메서드를 지정하는 인터페이스

  • 함수형 인터페이스로 뭘 할수 있을까?
    • 람다 표현식으로 함수형 인터페이스의 추상메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스 취급
    • 익명 클래스로도 같은 기능을 구현 가능하다.
public class Main {
    public static void main(String[] args) {
        Runnable r1 = () -> System.out.println("Hello 1");
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello 2");
            }
        };

        process(r1);
        process(r2);
        process(() -> System.out.println("Hello 3"));
    }

    public static void process(Runnable r){
        r.run();
    }
}

함수 디스크립터

함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다.
람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 부른다.
함수형 인터페이스를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있다.

실행 어라운드 패턴

자원처리에 사용하는 순환 패턴은 자원을 열고, 처리한 다음에, 자원을 닫는 순서로 이루어진다.
실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태의 코드를 실행 어라운드 패턴이라고 한다.

텍스트 파일에서 텍스트를 읽고 자원을 반환하는 코드의 경우에는 아래와 같이 코드가 작성될 수 있는데, 동일 프로젝트 내에서 읽어드리는 파일과 읽어드린 데이터를 처리하는 방법만 변경될 뿐 리더를 여닫는 패턴을 비슷할 것이다

public String processFile() throws IOException {
        try ( BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
            return br.readLine();
        }
    }

특별한 void 호환규칙

람다의 바디에 일반 표현식이 있으면 void를 반환하는 함수 디스크립터와 호환된다.(물론 파라미터 리스트도 호환되어야 함)
예를 들어 다음 두 행의 예제에서 List의 add메서드는 Consumter 콘텍스트(T → void)가 기대하는 void 대신 boolean을 반환하지만 유효한 코드다.

//Predicate는 boolean 반환값을 갖음
Predicate<String> predicate = s -> list.add(s);
// Consumer는 void 반환값을 갖지만 컴파일 에러가 발생되지 않음
Consumer<String> consumer = s -> list.add(s);

지역변수 사용

람다 표현식은 익명함수가 하는 것처럼 자유변수(파라미터로 넘겨받은 변수가 아닌 외부에 정의된 변수)도 사용할 수 있는데 이를 람다 캡쳐링(capturing lambda) 이라 한다.
람다는 인스턴스 변수와 정적 변수를 자유롭게 캡쳐할 수 있지만 지역변수는 명시적으로 final로 선언되어 있거나 final을 붙인 것처럼 수정하지 말아야 한다.

// 가능
int port = 8080;
Runnable r = () -> System.out.println(port);
r.run();


//불가능
int port = 8080;
Runnable r = () -> System.out.println(port);
port=9090;
r.run();
public class Capturing {
    private static int port=8080;
    public static void main(String[] args) {

        Runnable r = () -> System.out.println(port);
        port=9090;  // 인스턴스 변수는 재할당하면 안된다는 제약조건에 적용되지 않음.
        r.run();
    }
}

지역변수 제약

위와 같은 제약이 필요한 이유는 지역 변수는 스택에 생성, 인스턴스 변수는 힙 영역에 생성된다.
따라서 지역 변수는 쓰레드끼리 공유가 안 되며 인스턴스 변수는 쓰레드끼리 공유가 가능하다.
변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서 해당 변수를 접근하려 할 수 있어서 자바에서는 원래 변수에 접근이 아니라 자유 지역 변수의 복사본을 제공한다. 따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수는 한 번만 값을 할당해야 한다.

메서드 참조

아래에 책을 연도순으로 오름차순으로 정렬하는 코드가 있다.

public class MethodReference {
    public static void main(String[] args) {
        List<Book> books = asList(new Book("이펙티브 자바", "조슈아 블로크", 2018, "IT"),
                new Book("모던 자바 인 액션", "라울-게이브리얼 우르마", 2019, "IT"),
                new Book("이순신의 바다", "황현필", 2021, "History"),
                new Book("갈매기의 꿈", "리처드 바크", 2020, "Fiction")
        );

        books.sort((book, t1) -> book.getYear().compareTo(t1.getYear()));
    }
}

이 경우 메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다.

books.sort(Comparator.comparing(Book::getYear));

메서드 참조는 람다 표현식이 오직 하나의 메소드를 호출할 때 불필요한 매개변수를 제거하고 사용하도록 해준다.

참고문서

모던 자바 인 액션 - 라울 게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로소포트