Java

[Java] 람다식에 대해서

SN.Flower 2023. 10. 14.


람다식이란?

 

람다식은 함수형 프로그래밍 기법을 지원하는 자바의 문법 요소다.

람다식을 설명하기 전에 함수와 메서드에 대해 생각해보자.

 

함수(function)는 기능 또는 동작을 정의한 일련의 명령 모음이고, 메서드(method)는 클래스 또는 인터페이스 내에 정의된 함수를 말한다.

자바에서 메서드를 사용하려면 항상 클래스 객체를 먼저 생성한 후, 생성한 객체로 메서드를 호출해야 한다.

만약 모든 클래스에서 공통적으로 사용하는 기능이 있다면 모든 클래스마다 메서드를 정의해야 한다.

 

그런데 이 공통 기능을 일반적인 함수처럼 독립적으로 만든 후 모든 클래스에서 공통적으로 사용할 수 있다면 모든 클래스에서 메서드를 정의할 필요가 없어서 효율적일 것이다.

하지만 자바는 객체 지향 언어이므로 모든 함수는 클래스 내부에 메서드로 존재해야 한다.

이를 해결하기 위해 나온 방법이 '람다식'이다.

 

자바는 새로운 함수 문법을 정의하는 대신, 이미 있는 인터페이스의 문법을 활용해 람다식을 표현한다.

단 하나의 추상 메서드만을 포함하는 인터페이스를 함수형 인터페이스라 한다. 그리고 이 함수형 인터페이스의 호출 및 기능을 구현하는 방법을 새롭게 정의한 문법이 바로 람다식이다.

 

정리하자면 람다식은 기존의 객체 지향 프로그램 체계 안에서 함수형 프로그래밍을 가능하게 하는 기법이라 생각할 수 있다.

 

interface A {
    void abc();
}
public class LambdaExample {
    public static void main(String[] args) {

        // 익명 이너 클래스 사용
        A a1 = new A() {
            @Override
            public void abc() {
                System.out.println("메서드 내용1");
            }
        };
        a1.abc();

        // 람다식 사용
        A a2  = () -> {
            System.out.println("메서드 내용2");
        };
        a2.abc();
    }
}

코드 실행 결과

 

람다식은 익명 이너 클래스 정의 내부에 있는 메서드명을 포함해 이전 부분을 삭제한 형태다.

즉, 람다식은 익명 이너 클래스의 축약된 형태라고 볼 수 있다.

 

위 코드를 통해 알 수 있는 점은 단 하나의 추상 메서드만을 가진 함수형 인터페이스만 람다식으로 표현할 수 있다는 것이다.

왜냐하면 람다식은 내부 메서드명을 생략하므로 구현해야 할 추상 메서드가 2개 이상이라면 어떤 메서드를 구현한 것인지 구분할 수 없기 때문이다.

 

람다식의 문법

 

    // 1번째 경우
    void method1() {
        System.out.println(1);
    }

    () -> {
        System.out.println(1);
    }

    // 2번째 경우
    void method2(int a) {
        System.out.println(a);
    }

    (int a) -> {
        System.out.println(a);
    }

    // 3번째 경우
    int method3() {
        return 3;
    }

    () -> {
        return 3;
    }

    // 4번째 경우
    double method4(int a, double b) {
        return a + b;
    }

    (int a, double b) -> {
        return a + b;
    }

 

람다식은 기본적으로 (입력매개변수) -> { 메서드 내용 } 의 형태로 구성하면 된다.

소괄호는 입력매개변수, 중괄호는 메서드의 내용을 나타내기 때문에 람다식은 입력매개변수에 따른 메서드의 기능만을 정의한다고 생각하면 된다.

람다식은 익명 이너 클래스 내부 구현 메서드의 약식 표현뿐 아니라 메서드 참조와 생성자 참조에도 사용된다.

 

메서드 참조

 

메서드 참조는 추상 메서드를 직접 구현하는 대신, 이미 구현이 완료된 메서드를 참조하는 것이다.

메서드를 참조하는 방식은 인스턴스 메서드를 참조할 때와 정적 메서드를 참조할 때로 나뉜다.

 

우선 정의돼 있는 인스턴스 메서드를 참조할 때부터 알아보자.

 

interface A {
    void abc();
}

class B {
    void bcd() {
        System.out.println("메서드");
    }
}

A a = () -> {
    B b = new B();
    b.bcd();
};

 

인터페이스 A와 클래스 B가 위와 같이 정의돼있고 인터페이스 A의 추상 메서드 abc()는 람다식을 통해 구현한다고 가정해보자.

abc() 메서드는 B 클래스 객체를 생성해서 bcd() 메서드를 호출하는 기능을 갖고 있다.

 

그런데 위와 같은 경우 bcd()는 이미 완성된 인스턴스 메서드이므로 abc()가 bcd()와 동일하다는 사실만 알려주면 될 것이다.

이 때 람다식의 인스턴스 메서드 참조를 할 수 있으며, 아래와 같이 간단하게 표현할 수 있다.

 

B b = new B();
A a = b::bcd;

 

B 객체를 생성한 후 A a = b::bcd와 같이 작성하면, 이는 'A 인터페이스 내부의 abc() 메서드는 참조 변수 b 객체 내부의 인스턴스 메서드 bcd()와 동일하다'는 의미가 된다.

이렇게 추상 메서드 abc()가 인스턴스 메서드 bcd()를 참조하기 위해서는 리턴 타입과 매개변수의 타입이 반드시 동일해야 한다.

 

 

다음으로 정의돼 있는 정적 메서드를 참조할 때를 알아보자.

 

위에서 B 클래스의 bcd() 메서드가 인스턴스 메서드가 아니라 static 제어자가 붙은 정적 메서드라고 가정해보자.

정적 메서드는 객체 생성 없이 클래스명으로 바로 사용할 수 있기 때문에 객체의 생성없이 클래스명을 바로 사용해서 호출하면된다.

 

A a = B::bcd;

 

따라서 이 경우에는 위와 같이 클래스 객체를 생성할 필요없이 바로 클래스명::정적 메서드명으로 작성하면 된다.

 

생성자 참조

 

생성자 참조에서 참조하는 생성자는 크게 배열 객체 생성자와 클래스 객체 생성자로 나뉜다.

 

우선 배열 객체 생성자를 참조하는 것부터 알아보자.

 

interface A {
    int[] abc(int len);
}

A a = (len) -> {
    return new int[len];
};

 

함수형 인터페이스에 포함된 추상 메서드가 배열의 크기를 입력매개변수로 하며, 특정 배열 타입을 리턴한다면 구현 메서드의 내부에는 반드시 new 자료형[]이 포함될 것이다.

 

이 때 인터페이스에 포함된 추상 메서드의 구현 메서드가 new 자료형[]과 같이 배열 객체의 생성 기능만을 수행할 때는 람다식의 배열 생성자 참조 방법을 사용할 수 있다.

 

A a = int[]::new;

 

이 경우에는 '배열 타입::new'로 작성하면 된다.

위 코드의 의미는 'abc()을 호출하면 len 크기를 갖는 int[]를 생성하는 new int[len]을 실행한 후 배열 객체를 리턴하라'는 의미이다.

예를 들어 a.abc(3)을 호출하면 new int[3]이 실행돼고 배열 객체가 리턴될 것이다.

 

 

다음으로 클래스 생성자 참조에 대해서 알아보자.

 

배열과 완벽히 동일한 개념으로, 추상 메서드를 구현할 때 new 생성자()와 같이 객체만을 생성해 리턴하면 클래스 생성자 참조를 사용할 수 있다.

 

interface A {
    B abc();
}

class B {
    B() {}
    B(int a) {}
}

A a = () -> {
    return new B();
};

 

인터페이스 A와 B가 위와 같이 정의돼있다고 가정해보자.

위의 경우 람다식을 통해 abc() 메서드 내부에서는 new B()와 같이 클래스 B의 객체만을 생성해서 리턴했다.

이때는 아래와 같이 람다식의 클래스 생성자 참조 방법인 클래스명::new로 표현할 수 있다.

 

A a = B::new;

 

그런데 클래스 생성자 참조에서는 고려해야 할 사항이 있다. 위의 경우와 같이 클래스의 생성자가 여러 개일 수 있다는 것이다.

클래스가 여러 개의 생성자를 갖고 있을 때 위의 경우에는 클래스의 기본 생성자만 호출되고, 매개변수가 있는 생성자는 호출되지 않는다.

그렇다면 매개변수가 있는 생성자는 어떻게 참조해야할까? 방법은 간단하다.

 

interface A {
    B abc(int a);
}

 

추상 메서드의 매개변수에 호출할 클래스 생성자의 매개변수와 동일하게 바꾸면 된다.

이러면 클래스명::new로만 표현해도 해당 매개변수를 가지고 있는 생성자를 호출해서 객체를 생성하게 된다.

 


 

Reference

  • 자바 완전 정복 | 김동형 지음

댓글