본문 바로가기
Java

[자바 라이브 스터디] 15. 람다식

by 매트(Mat) 2021. 10. 26.

15주차 과제: 람다식

목표

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

학습할 것

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




람다식 사용법

람다식(Lambda Expression)이란 함수를 하나의 식으로 표현할 것을 말합니다. 함수를 람다식으로 표현하면 메소드명이 필요없게 됩니다. 그래서 람다식을 익명 함수(Anonoymous Function)의 한 종류라고 볼 수 있습니다.

@FunctionalInterface
public interface MyInterface {
    int sum (int x, int y);
}

먼저 인터페이스를 만들고 두 파라미터를 받는 sum이라는 추상 메소드가 있습니다.

public class Main {
    public static void main(String[] args) {
        MyInterface myInterface = new MyInterface() {
            @Override
            public int sum(int x, int y) {
                return x + y;
            }
        };
    }
}

main() 메소드에서 인터페이스의 인스턴스를 생성하여 sum() 메소드를 오버라이딩을 하면 위와 같은 코드가 됩니다. sum()이라는 메소드는 두 파라미터인 x, y를 받아서 더한 값을 리턴합니다. 하지만 위 코드를 더 간결하게 만들 수 있습니다. 바로 람다식으로 말이죠.

public class Main {
    public static void main(String[] args) {
        MyInterface myInterface = (x, y) -> {
            return x + y;
        };
    }
}

첫 번째 코드보다 더 간결해졌습니다.

  • (x, y) : 괄호안에 두 파라미터를 입력합니다. 이 때 타입을 생략가능합니다.
  • { return x + y } : {} 중괄호 안에 로직을 작성합니다.

위 코드보다 더 간결하게 만들 수 있습니다.

public class Main {
    public static void main(String[] args) {
        MyInterface myInterface = (x, y) -> x + y;
    }
}

만약에 {} 중괄호 안에 로직이 단 한 줄이라면 {} 중괄호와 return은 생략가능합니다.
최종적으로 한 줄로 표현식이 완성됩니다. 이게 람다식 입니다.
아마 자바스크립트에 익숙하신 분이라면 함수 표현식을 많이 사용하셔서 람다식도 익숙하실 것 같습니다.

 

이렇게 람다식을 사용하면 보시는 것처럼 코드가 간결해집니다.
다만 너무 람다식을 남발하면 코드가 오히려 지저분해지고 해석하기 어려울 수 있습니다. 그래서 디버깅도 하기 힘들어집니다. 따라서 적절히 사용하는 것이 좋겠죠.




함수형 인터페이스

위 코드에서 MyInterface 인터페이스가 바로 함수형 인터페이스입니다.
함수형 인터페이스(Functional Interface)란 인터페이스 내에 선언하여 단 하나의 추상 메소드만을 갖도록 제한하는 역할을 합니다. 이러한 함수형 인터페이스를 사용하는 이유는 바로 자바에서 람다식이 이 함수형 인터페이스를 반환하기 때문입니다.

@FunctionalInterface
public interface MyInterface {
    int sum (int x, int y);
    int sum2 (int x, int y);
}

위 코드처럼 추상 메소드가 두 개라면 함수형 인터페이스가 아닙니다.
보통 함수형 인터페이스라고 지정할 수 있는 방법이 @FunctionalInterface 애노테이션을 사용하는 것입니다. @FunctionalInterface 애노테이션을 붙이고 위 코드처럼 추상 메소드가 두 개라면 컴파일 에러를 발생시킵니다.

 

여기서 중요한 점은 람다식은 함수형 인터페이스로만 반환한다는 점만 기억하시면 될 것 같습니다.




Variable Capture

public class Main {
    private int a = 144;

    public void printExample() {
        int b = 222;
        final MyInterface myInterface = () -> System.out.println(a);
    }
}

위 코드에서 람다식에서 람다 시그니처의 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수를 자유 변수(Free Variable)이라고 합니다.
그리고 이 자유 변수를 참조하는 행위를 람다 캡처링(Lambda Capturing)이라고 합니다.

 

지역 변수를 람다 캡처링할 때 두 가지 제약조건이 있습니다.

  • 지역변수는 final로 선언되어 있어야 합니다.
  • final로 선언되어 있지 않더라도 지역변수는 final처럼 동작해야 합니다.

그래서 위 코드는 제약조건에 만족합니다. 이유는 final로 선언하지 않더라도 값을 변경하지 않고 final처럼 동작하기 때문이죠.
그렇다면 아래 코드는 어떨까요?

public class Main {
    private int a = 144;

    public void printExample() {
        int b = 222;
        a = 1000;
        final MyInterface myInterface = () -> System.out.println(a);
    }
}

변수 a는 final로 선언되어 있지도 않고 final처럼 동작하지도 않습니다. (값을 변경해버렸죠.)
이 때는 제약조건에 만족하지 않습니다.

 

그렇다면 람다식에는 왜 이런 제약조건이 생기게 되었을까요?
그 이유는 https://perfectacle.github.io/2019/06/30/java-8-lambda-capturing/ 이 블로그를 꼭 보세요. 설명이 아주 자세하게 나와있습니다.




메소드, 생성자 레퍼런스

람다식 메소드 참조란 메소드를 참조해서 파라미터의 정보 및 리턴 타입을 알아내어 람다식에서 불필요한 파라미터는 제거하는 것을 말합니다.
람다식을 아래 코드처럼 표현할 수 있었습니다.

public class Main {
    public static void main(String[] args) {
        MyInterface myInterface = (x, y) -> x + y;
    }
}

위 코드를 더 생략해서 이제는 파라미터까지 제거할 수 있습니다.

public class Main {
    public static void main(String[] args) {
        MyInterface myInterface = Integer::sum;
    }
}

함수형 인터페이스에서 선언한 추상 메소드의 타입 :: 메소드명 이렇게 적어주면 이게 람다식 메소드 참조입니다.

 

람다식 생성자 참조도 메소드 참조와 같은 개념입니다.
먼저 멤버 클래스를 하나 만들어줍니다.

public class Member {

    private String name;
    private int age;

    public Member() {
    }

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
@FunctionalInterface
public interface MyInterface {
    Member example(String name, int age);
}
public class Main {
    public static void main(String[] args) {
        MyInterface myInterface = new MyInterface() {
            @Override
            public Member example(String name, int age) {
                return new Member(name, age);
            }
        };
    }
}

이제 위 코드를 람다식으로 바꾸면 아래 코드처럼 되겠죠?

public class Main {
    public static void main(String[] args) {
        MyInterface myInterface = (name, age) -> new Member(name, age);
    }
}

그리고 생성자 참조를 하게 되면 아래 코드처럼 변경할 수 있습니다.

public class Main {
    public static void main(String[] args) {
        MyInterface myInterface = Member::new;
    }
}

아주 간결해졌죠?
대신에 이 코드만 보고 이게 어떤 코드인지는 알기가 정말 힘들 것 같네요 ㅎㅎ;




References

댓글