null은 오버 로딩된 메서드 중 어떤 메서드를 호출할까?

실행 환경: Java 17

우선 해당 주제를 본격적으로 다루기 전에 아래의 코드를 보자. 아래 코드는 null을 참조하는 변수를 사용했을 때와, 리터럴 null을 사용했을 때 String.valueOf() 메서드의 동작을 보여준다.

class NullTest {

    public static void main(String[] args) {
        String s = null;
        String nullValue = String.valueOf(s);
        System.out.println(nullValue); // null, 정상 출력 ---- 1

        nullValue = String.valueOf(null); // NullPointerException ---- 2
        System.out.println(nullValue);
    }
}

1번 코드는 정상적으로 null을 출력하지만, 2번 코드는 NullPointerException이 발생하는 것을 확인할 수 있다. 디버깅 모드를 통해 호출 된 메서드를 추적했을 때, 두 라인은 서로 다른 메서드를 호출하고 있음을 알 수 있었다.

1. String.valueOf(s)

// java.lang.String
public final class String {

    public static String valueOf(Object obj) {
        return (obj == null) ? "null" : obj.toString();
    }
}

String.valueOf(s)String.valueOf(Object obj)를 호출하는데, 이 메서드는 null 체크를 하는 것을 확인할 수 있다. 때문에 넘겨 받은 obj 값이 null이기 때문에 자연스럽게 "null"을 반환하게 된다.

2. String.valueOf(null)

하지만 직접 null을 넘겨 받은 경우에는 String.valueOf(char data[])를 호출하게 된다.

// java.lang.String
public final class String {

    public String(char value[]) {
        this(value, 0, value.length, null); // 3. value.length에서 NullPointerException 발생
        // Exception in thread "main" java.lang.NullPointerException: Cannot read the array length because "value" is null
    }

    // 1. 메서드 호출 받음
    public static String valueOf(char data[]) {
        return new String(data); // 2. 위의 String(char value[])를 호출
    }
}

주석의 순번대로 코드가 실행되는데, 결국 3번에서 null인 값에서 length를 읽으려고 하기 때문에 NullPointerException이 발생하게 된다. 그렇다면 왜 nullchar data[] 타입이 아닌데 해당 메서드가 호출되는 것일까?

호출 메서드는 어떻게 결정되는가?

위 상황과 비슷하게 char data[] 타입과 Object o 타입을 파라미터로 갖는 메서드를 호출했을 때 어떤 메서드가 호출되는지 확인해보자.

class MethodCallTest {

    public static void main(String[] args) {
        testMethod(null); // char[] Param Method Called
    }

    public static void testMethod(char data[]) {
        System.out.println("char[] Param Method Called");
    }

    public static void testMethod(Object o) {
        System.out.println("Object Param Method Called");
    }
}

처음의 예제 코드와 같이 char[] Param Method Called가 출력된다. 그럼 nullchar[]과 특수한 관계가 있는 것일까? 그것도 아니다.

class MethodCallTest {

    public static void main(String[] args) {
        testMethod(null); // Object Param Method Called
    }

//    public static void testMethod(char data[]) {
//        System.out.println("char[] Param Method Called");
//    }

    public static void testMethod(Object o) {
        System.out.println("Object Param Method Called");
    }
}

char data[] 타입의 메서드를 주석 처리하고 실행해보면 Object Param Method Called가 출력된다. 즉, nullchar[]과 특수한 관계가 있는 것이 아니라, Object 보다는 char[] 타입이 더 높은 우선순위를 가진다고 추측해 볼 수 있다. 다른 타입들도 더 살펴보자.

class MethodCallTest {

    public static void main(String[] args) {
        testMethod(null); // no suitable method found for testMethod(<nulltype>)
    }

    public static void testMethod(int i) {
        System.out.println("int Param Method Called");
    }

    public static void testMethod(long i) {
        System.out.println("Integer Param Method Called");
    }

    // ...
    // char, byte, short, int, long, float, double, boolean
}

당연하게도 원시 타입은 null을 가질 수 없기 때문에 일치하는 메서드가 없다는 에러가 발생한다. 그럼 null이 호출할 수 있는 메서드는 참조(주소) 타입 인자의 메서드만 호출할 수 있는 것으로 추측할 수 있다.

class Test {

    int x;
    int y;
}

class MethodCallTest {

    public static void main(String[] args) {
        testMethod(null); // reference to testMethod is ambiguous
    }

    public static void testMethod(Object o) {
        System.out.println("Object Param Method Called");
    }

    public static void testMethod(char data[]) {
        System.out.println("char[] Param Method Called");
    }

    public static void testMethod(Test t) {
        System.out.println("Test Param Method Called");
    }

    public static void testMethod(String s) {
        System.out.println("String Param Method Called");
    }

    public static void testMethod(int... i) {
        System.out.println("int... Param Method Called");
    }

    public static void testMethod(Integer i) {
        System.out.println("Integer Param Method Called");
    }
}

위의 메서드들은 단일로 존재했을 때 전부 호출 될 수 있는 메서드들인데, 동시에 존재하는 경우 호출할 수 있는 메서드가 많아 모호하다는 에러 메시지가 발생한다.

reference to testMethod is ambiguous
  both method testMethod(int...) in MethodCallTest and method testMethod(java.lang.Integer) in MethodCallTest match

그 중 int ...i 타입과 Integer i 두 개의 메서드에 대해 언급하면서 에러 메시지가 발생했는데, 두 타입이 더 null과 관련이 있는 것일까? 아니다, 그 이유는 다시 아래의 코드와 에러 메시지를 보면 알 수 있다.

class Test {

    int x;
    int y;
}

class MethodCallTest {

    public static void main(String[] args) {
        testMethod(null); // reference to testMethod is ambiguous
    }

    public static void testMethod(Object o) {
        System.out.println("Object Param Method Called");
    }

    public static void testMethod(int... i) {
        System.out.println("int... Param Method Called");
    }

    public static void testMethod(Integer i) {
        System.out.println("Integer Param Method Called");
    }

    public static void testMethod(char data[]) {
        System.out.println("char[] Param Method Called");
    }

    public static void testMethod(Test t) {
        System.out.println("Test Param Method Called");
    }

    public static void testMethod(String s) {
        System.out.println("String Param Method Called");
    }
}
reference to testMethod is ambiguous
  both method testMethod(Test) in MethodCallTest and method testMethod(java.lang.String) in MethodCallTest match

에러 메시지를 다시 살펴 보면 Test 타입과 String 타입이 언급되는 것을 볼 수 있다. 결국 특정 타입이 아닌, 완전히 일치하는 타입이 없어 null을 호출 할 수 있는 메서드를 탐색하게되고, 마지막 두 타입에 대해 모호하다는 에러 메시지가 발생한 것으로 추측해 볼 수 있다.

결론

  1. null은 원시 타입을 제외한 모든 타입의 인자로 호출 당할 수 있다.

  2. nullObject 타입으로도 호출 당할 수 있다.

  3. Object가 아닌 참조 타입 메서드가 존재하면 더 높은 우선순위를 가진다.

  4. 만약 Object 타입을 제외한 참조 타입 메서드가 두 개 이상 존재하면 refrence to method is ambiguous 에러가 발생하게 된다.

  5. 에러 메시지는 null을 호출 할 수 있는 메서드를 탐색하다가 마지막 두 가지 타입에 대해 모호하다는 에러 메시지가 발생한 것으로 추측해 볼 수 있다.

다소 애매한 결과라고 볼 수 있지만, 사실 null 타입을 그대로 넣는 일은 거의 없기 때문에 이러한 상황은 잘 발생하지 않을 것이라고 생각한다. 다시 한 번 null을 사용할 때는 주의 해야 한다는 것을 체감할 수 있었고, null 자체를 파라미터로 넘기는 것을 지양하는 것이 좋을 것 같다.

Last updated