우선 해당 주제를 본격적으로 다루기 전에 아래의 코드를 보자.
아래 코드는 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이 발생하게 된다.
그렇다면 왜 null이 char 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가 출력된다.
그럼 null은 char[]과 특수한 관계가 있는 것일까? 그것도 아니다.
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가 출력된다.
즉, null은 char[]과 특수한 관계가 있는 것이 아니라, 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을 호출 할 수 있는 메서드를 탐색하게되고,
마지막 두 타입에 대해 모호하다는 에러 메시지가 발생한 것으로 추측해 볼 수 있다.
결론
null은 원시 타입을 제외한 모든 타입의 인자로 호출 당할 수 있다.
null은 Object 타입으로도 호출 당할 수 있다.
Object가 아닌 참조 타입 메서드가 존재하면 더 높은 우선순위를 가진다.
만약 Object 타입을 제외한 참조 타입 메서드가 두 개 이상 존재하면 refrence to method is ambiguous 에러가 발생하게 된다.
에러 메시지는 null을 호출 할 수 있는 메서드를 탐색하다가 마지막 두 가지 타입에 대해 모호하다는 에러 메시지가 발생한 것으로 추측해 볼 수 있다.
다소 애매한 결과라고 볼 수 있지만, 사실 null 타입을 그대로 넣는 일은 거의 없기 때문에 이러한 상황은 잘 발생하지 않을 것이라고 생각한다.
다시 한 번 null을 사용할 때는 주의 해야 한다는 것을 체감할 수 있었고, null 자체를 파라미터로 넘기는 것을 지양하는 것이 좋을 것 같다.