Study/Java

자바 기본서를 다시 읽다. 3 - 로컬클래스와 익명객체, 그리고 람다식 (2)

going.yoon 2022. 3. 15. 22:33

이제 람다식을 정리해보자. 앞서 로컬클래스와 이름 없이 사용하는 로컬클래스인 익명 객체에 대해 알아보았다.

그리고 이 람다식은 런타임시 익명객체를 생성해주는 함수 생성 식이다. 이를 사용하면 자바 코드가 매우 간결해주고 컬렉션의 요소를 필터링하거나 매핑해서 원하는 결과를 쉽게 집계할 수 있다. 

 

람다식은 아래와 같이 인터페이스 변수 = 람다식의 형태로 작성된다.

Runnable runnable = () -> {};

 

이렇게 보면 자바의 메소드를 선언하는 것 처럼 보이지만, 자바는 메소드만을 단독으로 생성할 수 없고, 항상 클래스의 구성 멤버로 선언하기 때문에 자바는 이 메소드를 가지는 객체를 생성하고 인터페이스 변수에 대입된다. 위 코드는 Runnable 변수에 대입되므로 람다식은 Runnable 익명 구현 개체를 생성 후 대입해 주는 것이다.

Runnable runnable = new Runnable(){
    
};

이런식으로!

 

 

이렇듯 람다식은 대입될 인터페이스 종류에 따라 작성 방법이 달라지기에 람다식이 대입될 인터페이스를 람다식의 타겟 타입이라고 한다. 

그렇다면 타겟타입에 따라 람다식이 어떻게 사용되는지 알아보자

타겟 타입과 함수적 인터페이스 

I. 함수적 인터페이스(@FunctionalInterface)

람다식을 사용하기 위해서는 타켓타입 인터페이스는 하나의 추상 메소드만 선언이 되어있어야 한다. 그리고 이러한 인터페이스를 함수적 인터페이스라고 한다. 인터페이스 선언 시 @FunctionalInterface 애노테이션을 붙여주면 두개 이상의 추상 메소드가 생성되지 않도록 컴파일러가 자동으로 체킹을 해준다.

 

II. 매개변수와 리턴값이 없는 람다식

// # 1. functionalInterface 선언
@FunctionalInterface
public interface Vehicle {
    public void run();
}

// # 2. Main 에서 해당 인터페이스 구현 방식
public class Main {
    public static void main (String args[]){
        Vehicle vi; // # 2.1  인터페이스 선언
        vi= () -> { 
        	// # 2.2 추상메소드 run에 대한 구현을 해준다.
            System.out.println("the Car is running");
        } ;
        vi.run();// # 2.3 메소드 실행
    }
}

 

III. 매개변수와 리턴값이 있는 경우

public class Main {
    public static void main (String args[]){
        Vehicle vi;
        vi= () -> {
            System.out.println("the Car is running");
        } ;
        vi.run();
    }
}

 

 

람다식의 클래스 멤버와 로컬 변수 사용

람다식의 실행블록에서는 클래스 멤버인 필드와 메소드를 제약 없이 사용할 수 있지만 this를 사용할 땐 조심해야 한다. 람다 내부에서 사용되는 this 는 내부적으로 생성되는 익명객체의 참조가 아니라 람다식을 실행한 객체의 참조이다.

// #1. Main 에서 객체 호출
public class Main {
    public static void main (String args[]){
       UsingThis usingThis = new UsingThis();
       UsingThis.Inner inner = usingThis.new Inner(); 
       // # 1.1 usingThis 인스턴스의 내부 클래스를 호출하는 방식
       inner.method();
    }
}

// 2. UsingThis 객체
public class UsingThis {
    public int field=10;
    class Inner {
        int field = 20;
        void method(){
            MyFunctionalInterface fi = () -> {
                int field = 30;
                System.out.println("field : " + field); // 30
                System.out.println("this.field : " + this.field); //20
                System.out.println("UsingThis.this.field : " + UsingThis.this.field); // 10
            };
            fi.method();
        }
    }
}

 

UsingThis 객체를 살펴보면 field 라는 변수가 아래와 같이 세개 선언되어 있다.

  1.  바깥클래스의 필드 변수 // int 10
  2.  중첩클래스의 필드 변수 // int 20
  3.  람다식 내부의 로컬 변수 // int 30

위에서 말했듯, 람다식에서의 this는 자기 자신이 아니라 자신을 호출한 객체를 참조한다. this.field를 호출 했을 때 중첩클래스의 필드 변수인 20이 찍힌걸 확인할 수 있었다. Usingthis.this.field를 호출하면 바깥 클래스를 참조한다.

 

로컬변수는 익명객체, 로컬클래스와 동일하게 final의 특성을 갖는다. 따라서 매개변수 또는 로컬변수를 람다식에서 읽는 것은 허용이 되지만 람다식 내부 또는 외부에서 변경할 수 없다.