본문 바로가기

JAVA 스터디

[JAVA] 람다식(Lambda)

람다식

람다식이란 하나의 메소드를 간략하게 표현하는 것을 말합니다.

 

// 일반 메소드
int max(int x, int y) {
    return x > y ? x : y;
}

// 람다식
(x, y) -> x > y ? x : y;

 

위의 예제처럼 메소드를 람다식으로 표현하면,

클래스를 작성하고 객체를 생성하지 않아도 메소드를 이용할 수 있습니다.

 

자바에서는 클래스의 선언과 동시에 객체를 생성하게 되므로,

단 하나의 객체만을 생성할 수 있는 클래스를 익명 클래스라고 합니다.

 

따라서 자바에서 람다식은 익명 클래스와 같다고 할 수 있습니다.

 

// 람다 표현식
(x, y) -> x < y ? x : y;

// 익명 클래스
new Object() {
    int max(int x, int y) {
        return x > y ? x : y;
    }
}

 

람다식은 값으로 사용할 수도 있으며, 파라미터로 전달 및 변수에 대입하기와 같은 연산들을 수행할 수 있습니다.

 

장점

 

  1. 코드의 간결성 : 불필요한 반복문의 삭제가 가능하며 식을 단순하게 표현할 수 있습니다.
  2. 지연 연산 수행 : 지연 연산을 수행함으로써 불필요한 연산을 최소화할 수 있습니다.
  3. 병렬 처리 가능 : 멀티쓰레드를 활용하여 병렬 처리를 할 수 있습니다.

단점

 

  1. 람다식의 호출이 까다롭습니다.
  2. 람다 stream 사용 시 단순 for문 혹은 while문을 사용하면 성능이 떨어집니다.
  3. 과도한 사용은 오히려 가독성을 떨어뜨립니다.

 

람다식 사용법

람다식을 사용하기 위해서는 화살표(->) 키워드를 이용해야 합니다.

메소드에서 이름과 반환 타입을 제거하고, 매개변수 선언부와 메소드의 몸통 사이에 화살표를 추가합니다.

 

문법

// 메소드
반환타입 메소드이름(매개변수, ...) {
    코드
}

// 람다식
(매개변수 목록) -> { 코드 }

 

예제

// 메소드
int max(int x, int y) {
    return x > y ? x : y;
}

// 람다식
(int x, int y) -> { return x > y ? x : y; }

 

람다식을 작성할 때는 아래의 특성에 의해서 여러 요소들을 생략할 수 있습니다.

 

1. 함수의 몸체가 하나의 명령문만으로 이루어진 경우에는 중괄호({})를 생략 할 수 있습니다.

(이때 세미콜론(;)은 붙이지 않음)

(int x, int y) -> x > y ? x : y

 

2. 매개변수의 타입을 추론할 수 있는 경우에는 타입을 생략할 수 있습니다.

(x, y) -> x > y ? x : y

 

3. 매개변수가 하나인 경우에는 괄호(())를 생략할 수 있습니다.

(x) -> x * x
// 괄호 생략
x -> x * x

 

4. 함수의 몸체가 하나의 return 문으로만 이루어진 경우에는 중괄호({})를 생략할 수 없습니다.

(x, y) -> { return x > y ? x : y; }

 

 

5. return 문 대신 표현식을 사용할 수 있으며, 이때 반환값은 표현식의 결과값이 됩니다. (이때 세미콜론은 붙이지 않음)

(x, y) -> x < y ? x : y

 

다음은 일반적인 방식의 쓰레드 생성법과 람다식을 이용한 쓰레드 생성법을 비교하는 예제입니다.

 

// 기존 자바 문법
new Thread(new Runable() {
    @Overrid
    public void run() {
        System.out.println("Welcome JAVA Study");
    }
}).start();

// 람다식
new Thread() -> {
    System.out.println("Welcome JAVA Study");
}).start();

 

기존의 문법과 람다식을 비교했을 때, 코드가 간결해져서 가독성이 훨씬 좋아진 걸 알 수 있습니다.

 

함수형 인터페이스

보통 람다식은 추상 메소드를 구현하는 형태로 표현됩니다.

또, 따로 상태를 가질 필요가 없으므로 인터페이스가 적합합니다.

 

람다식은 실제로 메소드가 아닌 익명 클래스의 객체와 동등합니다.

익명 객체의 메소드와 람다식의 매개변수, 반환 값이 일치하면 익명 객체를 람다식으로 대체할 수 있습니다.

 

해당 인터페이스가 함수형 인터페이스임을 컴파일러에게 알려주기 위해

@FunctionalInterface 애너테이션을 사용해 줍니다.

 

함수형 인터페이스를 정의할 것이라면 @Override 애너테이션처럼 꼭 작성해주도록 합니다.

 

@FunctionalInterface
interface MyFunction {
    public abstract int max(int x, int y);
}

 

MyFunction 인터페이스를 정의하고 이를 구현한 익명 클래스의 객체는 아래와 같이 생성합니다. (기존 방식)

 

MyFunction f = new MyFunction() {    // MyFunction 인터페이스를 구현한 익명 클래스의 객체 생성
    @Override
    public int max(int x, int y) {
        return x > y ? x : y;
    }
};

int bigger = f.max(5, 3)             // 익명 객체의 메소드 호출

 

앞서 설명한대로 람다식은 익명 객체와 동등하여 대체가 가능하다 했으므로, 아래와 같이 작성할 수 있습니다.

 

MyFunction f = (x, y) -> x > y ? x : y;    // 익명 객체를 람다식으로 대체

int bigger = f.max(5, 3);                  // 익명 객체의 메소드 호출

 

이처럼 람다식으로 인터페이스의 추상 메서드를 구현할 수 있고, 람다식을 참조 변수로 다룰 수도 있습니다.

 

함수형 인터페이스를 이용할 때는 2가지 제약사항이 있습니다.

 

  1. 함수형 인터페이스에는 람다식과 1:1로 연결될 수 있도록 하나의 추상 메소드만 정의해야 합니다.
  2. 단, static 메소드와 default 메소드의 개수에는 제약이 없습니다.

 

※ 함수형 인터페이스 타입의 매개변수와 리턴타입

메소드의 매개변수를 함수형 인터페이스 타입으로 선언함으로써 다음의 효과를 얻을 수 있습니다.

 

  1. 람다식을 참조하는 참조 변수를 매개변수로 지정한다.
  2. 람다식을 직접 매개변수로 지정할 수 있다.
@FunctionalInterface
interface MyFunction {
    void myMethod();
}

public class Example {
    static void callMethod(MyFunction f) {    // 함수형 인터페이스 타입의 매개변수
        f.myMethod();
    }
    
    public static void main(String[] args) {
        MyFunction f = ( ) -> System.out.println("myMethod()");
        
        // 1. 참조변수 지정 방식
        callMethod(f);
        
        // 2. 람다식 지정 방식
        callMethod(( ) -> System.out.println("myMethod()"));
    }
}

 

메소드의 리턴 타입을 함수형 인터페이스 타입으로 지정함으로써 아래의 효과를 얻을 수 있습니다.

 

  1. 함수형 인터페이스의 추상 메소드와 동등한 람다식을 가리키는 참조 변수를 반환한다.
  2. 람다식을 직접 반환한다.
// 람다식을 참조하는 변수 반환
MyFunctino myMethod() {
    MyFunction f = ( ) -> { };
    return f;
}

// 람다식을 반환
MyFunction myMethod() {
    return ( ) -> {};
}

 

이렇게 람다식을 참조 변수로 다룰 수 있고 메소드를 통해 람다식을 주고받을 수도 있습니다.

 

 예제

@FunctionalInterface
interface MyFunction {
    void run();
}

public class LambdaEx {
    static void execute(MyFunction f) {
        f.run();
    }

    static MyFunction getMyFunction() {
        MyFunction f = () -> System.out.println("f3.run()");
        return f;
    }

    public static void main(String[] args) {
        // 1. 익명 클래스로 MyFunction.run() 구현(기존 방식)
        MyFunction f1 = new MyFunction() {
            @Override
            public void run() {
                System.out.println("f2.run()");
            }
        };

        // 2. 람다식으로 MyFunction.run() 구현
        MyFunction f2 = () -> System.out.println("f1.run()");

        // 3. 람다식을 반환하는 메소드 호출
        MyFunction f3 = getMyFunction();

        f1.run();
        f2.run();
        f3.run();

        execute(f2);    // 1. 람다식을 참조하는 참조변수를 매개변수로 지정
        execute(() -> System.out.println("run()")); // 2. 람다식을 직접 매개변수로 지정
    }
}

 

Variable Capture

자바의 람다식은 특정 상황에서 람다식 외부에 선언된 변수에 접근할 수 있습니다.

람다식이 접근할 수 있는 변수의 유형은 다음과 같습니다.

 

  • 지역 변수
  • 인스턴스 변수
  • 클래스 변수

 

지역 변수 Capture

자바의 람다식은 람다식 본문 외부에 선언된 지역 변수의 값에 접근할 수 있습니다.

단, 이 변수는 사실상 변경되지 않는 것이 확인된 변수만 접근이 가능합니다.

 

개발자는 컴파일러가 값이 변경되지 않는다는 점을 명확하게 알려주기 위해 final 키워드를 입력해주는 것도 좋습니다.

 

@FunctionalInterface
interface MyFactory {
    public String create(char[] chars);
}

public class LambdaEx {
    public static void main(String[] args) {
        final String myString = "Test"
        
        // 람다식을 참조하는 변수
        MyFactory myFactory = (chars) -> {
            return myString + " : " + new String(chars);
        };
        
        char[] str = {'J', 'A', 'V', 'A'};
        System.out.println(myFactory2.create(str));
    }
}

// 실행결과
Test : JAVA

 

인스턴스 변수 Capture

람다식은 인스턴스 변수에도 접근할 수 있습니다.

 

public class EventConsumerImpl {
    private String name = "MyConsumer";

    public void attach(MyEventProducer eventProducer) {
        eventProducer.listen(e -> {
            System.out.println(this.name);
        });
    }
}

 

이곳의 인스턴스 변수(this.name)가 바뀌면, 람다식이 참조하는 변수도 함께 바뀝니다.

이것이 인스턴스 캡처입니다.

 

이는 익명 클래스와 다른 점인데 익명 클래스는 자체적인 인스턴스 변수를 가질 수 있기 때문에 다릅니다.

 

클래스 변수 Capture

람다식은 클래스 변수에도 접근할 수 있습니다.

이는 접근할 수 있는 모든 영역에서 접근 가능하므로 일반적인 기능이라 할 수 있겠습니다.

 

메소드, 생성자 레퍼런스

메소드, 생성자 레퍼런스를 통해서 람다식을 더 간결하게 표현할 수 있습니다.

 

메소드 참조(references)

람다식이 하나의 메서드만 호출하는 경우, 메서드 레퍼런스를 이용할 수 있습니다.

 

문법

// 일반 람다식
(인스턴스 -> 인스턴스.메소드명)

// 메소드 참조 표현식
(인스턴스의 클래스명::메소드명)
(참조변수이름::메소드명)

 

다음 예제는 두 개의 값을 전달받아 제곱 연산을 수행하는 Math 클래스의 메소드인 pow() 메소드를 호출하고 있습니다.

 

첫 번째는 일반 람다식 방법을 이용한 것이고,

두 번째는 메소드를 참조를 이용한 코드입니다.

 

예제

// 람다식
(base, exponent) -> Math.pow(base, exponent);

// 메소드 참조
Math::pow;

 

또한, 특정 인스턴스의 메소드를 참조할 때에도 참조변수의 이름을 통해 메소드 참조를 사용할 수 있습니다.

 

이는 이미 생성된 객체의 메소드를 람다식에서 사용하는 경우로,

외부에 존재하는 인스턴스의 참조 변수를 통해서 메소드 참조를 사용할 수 있습니다.

 

MyClass obj = new MyClass;

// 람다식
Function<String, Boolean> func = (a) -> obj.equals(a);

// 메소드 참조
Function<String, Boolean> func = obj::equals(a);

 

즉 메서드 참조가 가능한 경우는 아래와 같습니다.

  • static 메소드 참조 : 클래스명::메소드명
  • 인스턴스 메소드 참조: 클래스명::메소드명
  • 특정 객체의 인스턴스 메소드 참조: 참조변수명::메소드명

 

생성자 참조(references)

 

생성자를 호출하는 람다식도 앞서 살펴본 메소드 참조를 이용할 수 있습니다.

즉, 단순히 객체를 생성하고 반환하는 람다식은 생성자 참조로 변환할 수 있습니다.

 

다음 예제는 단순히 객체를 생성하고 반환하는 람다식입니다.

 

(a) -> { return new Object(a); }

 

위의 예제는 단순히 Object 클래스의 인스턴스를 생성하고 반환하기만 하므로

생성자 참조를 사용하여 다음과 같이 간단히 표현할 수 있습니다.

 

// 생성자 참조
Object::new;

 

이때 해당 생성자가 존재하지 않으면 컴파일 오류가 발생합니다.

 

또한, 배열을 생성할 때에도 다음과 같이 생성자 참조를 사용할 수 있습니다.

 

// 람다식
Function<Integer, double[]> func1 = a -> new double[a];

// 생성자 참조
Function<Integer, double[]> func2 = double[]::new;

 

'JAVA 스터디' 카테고리의 다른 글

[JAVA] I/O (Input/Output)  (0) 2021.08.12
[JAVA] 어노테이션(Annotation)  (0) 2021.07.21
[JAVA] Enum(열거형)  (0) 2021.07.09
[JAVA] 멀티쓰레드 프로그래밍(Multi Thread Programming)  (0) 2021.06.30
[JAVA] 예외처리(Exception)  (0) 2021.06.26