김찬진의 개발 블로그

22/12/26 [lamda?] 본문

1일1배움/Java

22/12/26 [lamda?]

kim chan jin 2022. 12. 26. 01:23

익명객체를 람다식으로 대체할 수 있다.
심지어 참조변수 없이 람다식만 매개변수로 주고 받을 수도 있다.

익명객체익명클래스로 생성한 객체를 의미한다.

(참고. 익명클래스는 클래스의 선언과 객체의 선언을 동시에 하는 방식이다.)

익명객체람다식으로 대체할 수 있다.

이것이 가능한 이유는

1. 람다식이 익명객체 이기 때문이고

2. 함수형 인터페이스의 추상메서드를 구현한 익명객체의 메서드람다식의 매개변수의 타입, 개수, 반환값이 일치하기 때문이다. (익명객체의 메서드와 람다식이 시그니처가 같기 때문)

https://cjkimhello97.tistory.com/59

 

23/02/09 [내부클래스?]

내부클래스의 종류는 변수의 선언위치에 따른 종류와 같다. https://cjkimhello97.tistory.com/60 23/02/09 [인스턴스변수? 클래스변수? 지역변수?] 인스턴스변수, 클래스변수, 지역변수? 인스턴스변수 인스

cjkimhello97.tistory.com

익명객체를 람다식으로 대체할 수 있다. 람다식 자체가 익명객체 개념이기 때문에, 익명객체의 메서드의 시그니처와 람다식이 시그니처가 같기 때문에 가능하다.

 

코드로 확인

@FunctionalInterface // 컴파일러가 함수형 인터페이스와 람다식이 1대1 정의되었는지 확인주므로 어노테이션을 쓰자
interface MyFunction {
    public abstract int max(int a, int b);
}

public abstract class Test {
    MyFunction f1 = new MyFunction(){ // 익명객체
        public int max(int a, int b){return a > b ? a : b;}
    };

    MyFunction f2 = (int a, int b) -> a > b ? a : b; // 람다식
    int big = f2.max(5,3);
}

 

다른 예시 코드

@FunctionalInterface
interface MyFunction{
    void run(); // public abstract void run();
}

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

    static MyFunction getMyFunction1(){
        // 익명클래스로 run() 구현하고 익명객체를 참조변수 f1에 대입
        MyFunction f1 = new MyFunction() {
            public void run(){ // 반드시 public
                System.out.println("f1.run()");
            }
        };
        return f1; // 익명객체의 주소값이 들어있는 참조변수 f1 반환
    }

    static MyFunction getMyFunction2(){
        // 익명객체를 람다식으로 대체하고 람다식을 참조변수 f2에 대입
        MyFunction f2 = () -> System.out.println("f2.run()");
        return f2; // 익명객체의 주소값이 들어있는 참조변수 f2 반환
    }

    // main
    public static void main(String[] args) {
        // 익명클래스로 run() 구현하고 익명객체를 참조변수 f3에 대입
        MyFunction f3 = new MyFunction(){
            public void run(){ // 반드시 public
                System.out.println("f3.run()");
            }
        };
        // 익명객체를 람다식으로 대체하고 람다식을 참조변수 f4에 대입
        MyFunction f4 = () -> System.out.println("f4.run()");

        MyFunction f5 = getMyFunction1();
        MyFunction f6 = getMyFunction2();


        f3.run(); // f3.run()
        f4.run(); // f4.run()
        f5.run(); // f1.run()
        f6.run(); // f2.run()
        System.out.println("");

        execute(f3); // f3.run()
        execute(f4); // f4.run()
        execute(f5); // f1.run()
        execute(f6); // f2.run()
        System.out.println("");

        // 익명클래스로 run() 구현하고 익명객체를 참조변수에 대입하지 않고 그대로 사용
        execute(new MyFunction() {
            public void run() {
                System.out.println("f7.run()");
            }
        });
        // 익명객체를 람다식으로 대체하고 람다식을 참조변수에 대입하지 않고 그대로 사용
        execute(()-> System.out.println("f8.run()"));
    }
}

 

 

인터페이스를 상속(implements)하는 것이 아니라
인터페이스를 참조타입으로 사용하여 익명객체를 만드는 것이다.

위 코드에 대한 설명이다.

 

MyFunction 인터페이스를 상속(implements)하는 것이 아니라

 

MyFunction 인터페이스를 참조타입으로 사용하여 익명객체(MyFunction f = new MyFunction(){...})를 만드는 것이다.

(물론 ... 안에는 MyFunction 인터페이스의 추상메서드(run())를 오버라이딩해야만 한다.)

 

이 때 익명객체를 람다식으로 대체한다면

MyFunction f = () -> {...} 가 될 것이다.

 

지금은 참조변수 f가 있으니 MyFunction 참조타입인 것을 알 수 있지만

어떤 경우에는 참조변수 없이 람다식만 매개변수로 주고받을 수도 있다.

 

예를 들어 이렇게 말이다.

execute(() -> System.out.println("f8.run()"));

그럼 execute 메서드의 매개변수가 람다식인 것은 알겠는데

참조변수의 타입(MyFunction)도 사라졌고

람다식으로 익명객체를 대체했으니 애초에 생성자(new Function(){...})를 알 수도 없기 때문에

참조타입이 무엇인지 모를 수 있다.

하지만 참조변수 없이 람다식만 매개변수로 주고 받을 수 있는 이유는

 

인터페이스의 추상메서드와 람다식의 시그니처가 같기 때문이다.

인터페이스의 추상메서드람다식의 매개변수의 타입, 개수, 반환값이 같기 때문이라는 것이다.

 

바르게 쓰고 있는지 내 눈에는 보이진 않지만 아주 바르게 쓰고 있다는 것이다.

이걸 감시하는 어노테이션이 @FunctionalInterface 이다.

 

@FunctionalInterface를 MyFunction 인터페이스에 붙여주면

컴파일러

MyFunction 인터페이스를 참조타입으로 사용하는

익명객체가

MyFunction 인터페이스의 추상메서드를

1대1로 오버라이딩했는지, 

오버라이딩할 때 매개변수의 타입, 개수, 반환값이 동일한지

확인해준다.

 

그니깐 @FunctionalInterface 를 꼭 써줘야 한다.

 

여기서 착각하지 말아야 할 것이

@FunctionalInterface 어노테이션을 MyFunction 인터페이스에 붙여줬다고 해서

익명객체의 반환타입이 MyFunction 인터페이스 참조타입이 되는 것처럼 인식할 수 있는데 그건 아니다.

익명객체는 타입이 없다!

정확히 말하면 익명객체는 컴파일러가 임의의 이름으로 정하기 때문에 알 수 없다.

interface MyInterface {
    public void method();
}

public class LamdaTest {
    MyInterface f1 = ()->{};
    MyInterface f2 = (MyInterface)(()->{});
}

람다식은 MyInterface 인터페이스를 직접 구현하지 않았지만, 이 인터페이스를 구현한 클래스의 객체와 완전히 동일하기 때문에 위와 같은 형변환을 허용한다.

즉, 람다식 "()->{}" 과 Myinterface 인터페이스의 추상메서드의 시그니처가 완전히 동일하기 때문에 

또한 람다식이 익명객체이므로

MyInterface f1 = ()->{};

가 가능한 것이고

 

익명객체의 타입이 무엇인지 모르지만 

형변환을 원한다면 MyInterface 참조타입으로만 형변환할 수 있다.

다른 타입으로는 형변환이 불가능하다.

심지어 Object 참조타입으로도 형변환이 불가능하다.

굳이 원한다면 MyInterface 참조타입으로 먼저 형변환한 이후에 Object 참조타입으로 형변환해야 한다.

@FunctionalInterface
interface MyFunction{
    void myMethod(); // public abstract void run();
}

public class Test {
    public static void main(String[] args) {
        MyFunction f = ()->{};
        Object obj = (MyFunction)(()->{}); // (Object)((MyFunction)(()->{}))
        String str = ((Object)(MyFunction)(()->{})).toString();

        System.out.println(f);
        System.out.println(obj);
        System.out.println(str);

        // System.out.println(()->{}); // error, println()의 인자로 들어갈 수 없는 익명객체의 미지수 타입
        System.out.println((MyFunction)(()->{})); // 익명객체의 타입을 MyFunction으로 변환하면 가능
        // System.out.println((MyFunction)(()->{}).toString()); // error, MyFunction 인터페이스는 toString 오버라이딩 불가하므로 사용불가
        System.out.println(((Object)(MyFunction)(()->{})).toString()); // 익명객체의 타입을 MyFunctoin, Object로 변환하면 가능
    }
}

()->{} 는 익명객체를 만들었다는 의미이고

참조변수 없이 쓰는거라 익명객체의 타입을 더 모르니깐 (MyFunction) 이라고 참조타입으로 형변환을 해줬다.

MyFunction 인터페이스는 Object의 자손이니깐 toString 오버라이딩해서 써도 되겠지 생각하지만 

MyFunction 인터페이스는 함수형인터페이스이기 때문에 오직 하나의 메서드(myMethod())만 선언해야 한다. 

그래서 MyFucntion 인터페이스는 toString() 메서드를 오버라이딩할 수 없다.

(애초에 인터페이스라 선언뿐만 아니라 구현도 못함ㅋㅋ)

그래서 (MyFunction) 이라고 참조타입으로 형변환을 하더라도 toString() 을 못쓰는 것이다.

정리하자면 이 모든 일은 익명객체를 만들었기 때문이다.

다시 말해 저 부분에서는 람다식 ()->{} 을 쓸 수 없다는 것이다.

 

 

 

 

 


 

 

 

 

 

람다식 내에서 참조하는 지역변수는 final을 안붙여도 상수로 간주된다.

@FunctionalInterface
interface MyFunction{
    void myMethod(); // public abstract void myMethod();
}

class Outer{
    int val = 10; // Outer.this.val

    class Inner{
        int val = 20; // this.val

        void method(int i){
            int val = 30;
            // i = 10; // 상수를 변경해서는 안된다!

            MyFunction f = () -> {
                System.out.println("            i : " + i); // i를 상수(final)로 간주하므로
                System.out.println("          val : " + val);
                System.out.println("     this.val : " + ++this.val);
                System.out.println("Test.this.val : " + ++Outer.this.val);
            };

            f.myMethod();
        } // Inner 내부클래스 끝
    } // Outer 외부클래스 끝
}


public class Test{
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.method(100);
    }
}

'1일1배움 > Java' 카테고리의 다른 글

23/01/16 [6가지 Sort]  (0) 2023.01.16
22/01/03 [toString?]  (0) 2023.01.03
23/01/16 [array?]  (0) 2022.12.25
22/12/23 [객체?]  (0) 2022.12.23
22/12/23 [char? String? Character?] Character  (0) 2022.12.23
Comments