glenn-syj / more-effective-java

이펙티브 자바를 읽으며 자바를 더 효율적으로 공부합니다
4 stars 5 forks source link

[MEJ-010] 함수형 인터페이스 다중정의 문제 보충 탐구 #186

Closed FickleBoBo closed 2 weeks ago

FickleBoBo commented 1 month ago

Based on : #183 by @undeadtimo


어려운 내용이었는데 열심히 탐구를 진행해주셔서 흥미롭게 배울 수 있었습니다. 탐구 주제로 삼으셨던 다중정의 내용에 대해 별 생각없이 있었는데 예제 코드를 통해 에러와 해결방안을 보여주셔서 저도 올려주신 코드를 참고해서 직접 탐구를 해보게 되었습니다.

package item44;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        // 1. Thread 생성자 호출
        new Thread(System.out::println).start();

        // 2. ExecutorService의 submit 메서드 호출
        ExecutorService exec = Executors.newSingleThreadExecutor();

        exec.submit(System.out::println);                        // compile error(ambiguous)
        exec.submit((Runnable) System.out::println);
        exec.submit((Callable<?>) System.out::println);  // compile error(bad return type)
    }
}

먼저 위 코드의 경우 System.out::println에 대한 3가지 경우를 생각해보았습니다.

ambiguous에러의 경우 Runnable과 Callable 둘 중 뭔지 모르겠다는 에러가 나왔는데 Callable<V>은 반환 값이 V인 call 메서드를 갖으므로 항상 반환형이 void인 println 메서드가 어떻게 Callable로 해석될 수 있을까 싶었습니다. Runnable로 형변환을 했을 경우 문제가 없었으며, Callable<?>로 형변환 했을 경우 리턴 타입이 일치하지 않는다는 에러가 나왔습니다. 와일드카드 대신 Object를 넣어도 여전히 에러가 발생했으며 이는 println 메서드가 반환값이 없어서 발생하는 에러라고 생각했습니다.

3가지 submit의 결과만 놓고 봤을 때는, compile error(ambiguous) 이것이 발생한 이유를 알기 어려웠습니다.

package item44;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {

    static void myPrint1(){
        System.out.println("1");
    }

    static String myPrint2(){
        System.out.println("2");
        return "2";
    }

    static Object myPrint3(){
        System.out.println("3");
        return 3;
    }

    static Void myPrint4(){
        System.out.println("4");
        return null;
    }

    static <V> V myPrint5(){
        System.out.println("5");
        return null;
    }

    public static void main(String[] args) {
        // 1. Thread 생성자 호출
        new Thread(System.out::println).start();

        // 2. ExecutorService의 submit 메서드 호출
        ExecutorService exec = Executors.newSingleThreadExecutor();

        exec.submit(Main::myPrint1);                       // 1
        exec.submit((Runnable) Main::myPrint1);     // 1
        exec.submit((Callable<?>) Main::myPrint1);  // compile error(bad return type)

        exec.submit(Main::myPrint2);                        // 2
        exec.submit((Runnable) Main::myPrint2);      // 2
        exec.submit((Callable<?>) Main::myPrint2);  // 2

        exec.submit(Main::myPrint3);                        // 3
        exec.submit((Runnable) Main::myPrint3);      // 3
        exec.submit((Callable<?>) Main::myPrint3);  // 3

        exec.submit(Main::myPrint4);                        // 4
        exec.submit((Runnable) Main::myPrint4);      // 4
        exec.submit((Callable<?>) Main::myPrint4);  // 4

        exec.submit(Main::myPrint5);                        // compile error(ambiguous)
        exec.submit((Runnable) Main::myPrint5);      // 5
        exec.submit((Callable<?>) Main::myPrint5);  // 5
    }
}

위 코드는 5가지 메서드에 대해 테스트를 해본 코드입니다.

반환값이 없는 1번 메서드에 대해서는 Callable로 형변환했을 때만 bad return error가 발생했는데 1번 메서드는 반환값이 없어서 그런 것으로 예상합니다. (나머지 둘은 Runnable) 이전의 코드와 동일하게 동작할 줄 알았는데 형변환을 안해도 에러가 발생하지 않은 점이 놀라웠습니다.

반환 타입이 String인 2번 메서드, 반환 타입이 Object인 3번 메서드, 반환 타입이 Void인 4번 메서드 모두 잘 출력이 되었는데 Runnable로 형변환 한 경우 반환값이 있는데 어떻게 잘 동작한 것인지는 이해하지 못했습니다. 형변환을 하지 않은 경우도 Callable로 동작한 것으로 예상했습니다.

반환 타입이 V인 5번 메서드에 대해서는 형변환을 해주지 않으면 ambiguous 에러가 발생했는데, 형 변환을 해주면 둘다 잘 동작했습니다. 정확한 이유를 알기는 어려웠는데, ambiguous 에러가 발생한 것과는 달리 타입 매개변수가 구제적으로 무엇인지 컴파일러가 알지 못해서 에러가 발생했다고 생각합니다. Runnable로 형변환 한 경우 잘 동작한 이유는 2, 3, 4와 마찬가지로 이해하지 못했습니다.

탐구 과정에 문제가 있을 수 있으니 비판적으로 봐주시기 바랍니다.

undeadtimo commented 1 month ago

우선 FickleBoBo님의 정리와 질문 감사드립니다.

FickleBoBo님의 질문들을 정리해보면,

  1. println 메서드에서 왜 Callalble 로 해석되는 것인지.

  2. 1번 메서드에서 형변환을 해도 에러가 발생하지 않은 점.

  3. 2번, 3번, 4번 메서드에 대해 Runnable로 형변환 한 경우 반환값이 있는데 어떻게 잘 동작한 것인지

  4. 5번 메서드의 ambuous 에러 이유와 runnable로 형변환한 경우에 잘 동작한 이유에 대해.

이렇게 네 가지가 있다고 생각하여 이에 대해 정리해보겠습니다.

우선 말씀드리자면, 첫 번째 의문인 반환값이 없는 void 타입의 println 메서드가 어째서 Callable로 해석될 수 있어서 ambiguous 에러가 발생하는지는 알 수 없었습니다.

이는 추후 조사를 진행하여 알아보도록 하겠습니다.

두 번째 의문에서 FickleBoBo님은 형변한을 하지 않아도 에러가 발생하지 않은 점에 대해 말하셨습니다.

제 추측으로, 첫 에러는 println이라는 메서드의 특수성 때문에 Callable과 Runnable에 대한 ambiguous 에러가 발생한 것으로 여겨집니다.

따라서, FickleBoBo님이 직접 작성하신 MyPrint1에서는 원래 생각하신 것처럼 반환값이 없는 void 타입의 메서드이기 때문에 컴파일러가 Runnable임을 확정할 수 있어서 에러가 발생하지 않는 것입니다.

세 번째 의문에서는 반환값이 존재하지 않는 Runnable로 형변환했지만 오류가 발생하지 않은 것에 대해 의문을 말씀해주셨습니다.

이에 대한 저의 생각으로는 Runnable로 형변환하여 실행하면 Runnable 처럼 반환값이 무시되고 반환값 외의 기능들만 실행되는 것입니다.

따라서 FickleBoBo님의 예시들을 보면 Runnable로 형변환 해준 것들은 어떤 경우에도 오류가 발생하지 않는 것을 알 수 있습니다.

마지막 네 번째 의문에서 ambiguous 에러가 발생한 이유에 대해 의문을 표하셨는데, 이 부분에 대해서는 저 또한 이유를 헤아리기가 어려웠습니다.

이 부분들은 특히 어려웠기 때문에 추가적으로 조사하며 의문들을 해결해보도록 하겠습니다.

FickleBoBo commented 1 month ago

Runnable과 Callable의 반환 타입이 다른 것으로 보여서 ambiguous 에러가 발생하는 이유를 테스트 해봤는데 해석에 어려움이 있었던 탐구였습니다. 같이 고민해주셔서 감사합니다. 좋은 자료를 찾게 되면 공유하도록 하겠습니다!!