본문 바로가기

Programing/JVM(Java, Kotlin)

[Java] Wrapper 클래스 vs Overload 메서드 중 우선순위는?

카페에 어떤 분이 아래와 같은 질문을 올렸다.

질문

public class OverrideVsAutoUnboxing {
    public static void main(String[] args) {
        System.out.println(new Integer(59));
    }
}

위와 같은 코드가 59로 출력되는 원리가 궁금합니다.

'new Integer(숫자)'를 system.out.println하면 숫자가 출력되는 원리에 대해 설명주세요.

코드리뷰

일단 위와 같은 코드는 여러 안티패턴(피해야 되어야 할 코드)이 들어가 있다.

불변 클래스의 생성자 생성

아래의 코드에 대한 내용이다.

new Integer(59)

자바에서 문자열 타입으로 문자열을 생성할 때 new String()으로 문자열을 만들지 말라고 하는 이유가 있는데

Integer는 "int (primitive type)에 대한 참조 타입(reference type)"이기도 하지만 "불변 클래스(Immutable Class)"이기도 하다.

불변 클래스라고 함은 클래스에 값을 설정을 할 수는 있지만 값을 바꿀 수 없다는 의미이다.

 

 

기본 데이터 타입의 사용

리뷰

일단 질문을 리뷰해보자.

"59로 출력"

59로 출력이라고 했는데 이것이 숫자 59인지, 문자열 "59"인지 명시하지는 않았지만 println에서 출력되는 출력의 내용은 무조건 문자열이다. println 라는 메서드로 이야기를 했지만 실제로 System.out 은 아래와 같이 미리 정의된 PrintStream 클래스이다.

public final class System {
	// ...
    public static final PrintStream out = null;
    // ..

PrintStream 클래스

PrintStream JavaDoc 문서를 보면 print 나  println 같은 출력 메서드들이 여러가지로 오버로딩되어 있음을 알 수 있다.

Java 7기준으로 println은 10개의 동일한 메서드가 있다. Java 11에도 10개였다.

메서드에 "어떤 값을 넘기느냐"에 따라서 어떤 메서드가 선택될지는 달라진다.

int 타입의 매칭

예를 들면 아래와 같이 타입이 int인 변수를 println에 인자로 사용을 한다면,

int i = 1;
System.out.println(i);

PrintStream의 println(int x)에 매칭이 되어 실행이 된다. 이 메서드는 내부에서 int를 String으로 바꾸는코드가 들어있다.

public class PrintStream
    // ..
    public void println(int x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
    public void print(int i) {
        write(String.valueOf(i)); // int가 String으로 변환이 된다.
    }
    private void newLine() {
    // ...

객체를 인자로 넘긴다면?

만약 본인이 만든 클래스의 인스턴스를 생성해서 println에 인자로 넘기면 "OverrideVsAutoUnboxing@4d591d15" 같은 이상한 결과를 얻게 된다.

public class OverrideVsAutoUnboxing {
    public static void main(String[] args) {
        OverrideVsAutoUnboxing overrideVsAutoUnboxing = new OverrideVsAutoUnboxing();
        System.out.println(overrideVsAutoUnboxing);	// OverrideVsAutoUnboxing@4d591d15
    }
}

이 이유는 println(Object x)로 매칭이되어 수행되었기 때문이다.

package java.io;
public class PrintStream
    // ...
    public void println(Object x) {
        String s = String.valueOf(x);
        synchronized (this) {
            print(s);
            newLine();
        }
    }
// ...

package java.lang;
public final class String
    // ...
    public static String valueOf(Object obj) {
        return (obj == null) ? "null" : obj.toString();
    }

위에 코드에서 볼 수 있듯이 println(Object x) 코드가 수행되면 내부적으로 String.valueOf 라는 정적 매서드가 호출이 되는데,

내부에서는 Object가 null이면 "null"이라는 문자열을 돌려주고, 널이 아니면 Object의 toString()이라는 메서드의 값을 돌려주게 된다.

public class Object {
    // ...
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

Object의 toString() 메서드는 위와 같이 정의되어 있다.

"OverrideVsAutoUnboxing@4d591d15" 가 출력이 되었는데

- OverrideVsAutoUnboxing 부분은 클래스 이름(getClass().getName()) 이고

- 4d591d15 부분은 hashCode() 값이 16진수(hex)로 출력이 되었다고 볼 수 있다.

Wrapper 클래스 vs Overload 메서드

내가 질문자의 질문과 별개로 궁금했던 것은 메서드 오버로딩이 수행될 때 Wrapper 클래스(Boxed 클래스)가 넘어가면 어떤 메서드가 매칭이 될 것인가이다.

1번 print(Object obj)

2번 print(int i)

 

2번을 떠올렸던 이유에서는 자바 5부터는 boxing 변환unboxing 변환을 해주는 코드를 컴파일러가 자동으로 만들어주기 때문이다.

더보기

컴파일러에 의한 자동 Boxing 변환

Integer integer = 1;

위와 같이 자바 코드를 짰다면 컴파일러는 아래와 같은 바이트 코드를 만든다.

ICONST_1
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
ASTORE 1

바이트코드를 자바코드로 옮긴다면 아래와 같다.

Integer integer = Integer.valueOf(1);

컴파일러에 의한 자동 Unboxing 변환

int i = integer;	// integer는 Integer 타입이다.

반대로 int로 Integer 참조를 넘기면 컴파일러는 아래와 같이 unboxing을 해주는 코드를 넣어준다.

LINENUMBER 23 L1
ALOAD 1
INVOKEVIRTUAL java/lang/Integer.intValue ()I
ISTORE 2

결국 아래와 동일하다.

int i = integer.intValue();

1번이 수행된다면 클래스 계층 구조를 따라 Integer의 최종 부모인 Object에 매칭이 되어 수행이 되는 것일 것이다.

Integer는 Object의 서브타입이다.

2번이 수행된다면 컴파일러가 아래와 같은 코드로 만들어주었다고 볼 수 있다.

public class OverrideVsAutoUnboxing {
    public static void main(String[] args) {
        System.out.println(new Integer(59).intValue());
    }
}

결론은?

이것을 확인해보기 위한 방법은 간단하다.

만들어지는 바이트코드를 확인하는 방법도 있지만 간단히 자바코드를 짜보는 것이다.

public class OverrideVsAutoUnboxing {
//    private static void test(Integer i) {
//        System.out.println("Boxed " + i);
//    }

    private static void test(int i) {
        System.out.println("Primitive " + i);
    }

    private static void test(Object o) {
        System.out.println("Object " + o);
    }

    public static void main(String[] args) {
        test(new Integer(59));
    }
}

위와 같이 수행하면 결과는 "Primitive 59" 일까? "Object 59" 일가?

 

왜 이렇게 되는지는 자바 언어 규격(The Java® Language Specification) 문서에 잘 나와있다.

15.12.2. Compile-Time Step 2: Determine Method Signature (메서드 시그너처 결정)에 내용이 잘 나와있다.

메서드 시그너처 결정시 서브타이핑이 가능한지 적용하는 방법이 3가지 방법 중 우선순위가 제일 높다.

하지만 $§15.12.2.2 적용시 박싱이나 언박싱 변환의 수행이 이루어지는 것은 제외한다(without permitting boxing or unboxing conversion)고 정의되어 있다.

만약 오토박싱, 언박싱을 메서드 오버로딩에 적용을 하게 된다면 Java 5.0 이전에 만든 코드가 제대로 동작하지 않게 될 것이기 때문이다.

결국 이렇게 스펙을 결정한 이유는 Java SE 5.0 이전과의 하위 호환을 맞추기 위함이라고 생각할 수 있다.