본문 바로가기

Programing/JVM(Java, Kotlin)

[Java] toString에서 나타나는 [, L 등의 문자의 정체는?

간단한 코드이다.

아래의 코드를 수행하면 어떤 글자들이 콘솔에 찍힐까?

package com.tistory.namocom.question

public class ToStringTest {
    public static void main(String[] args) {
        Object object = new Object();
        Object[] objects = new Object[1];
        System.out.println(object);
        System.out.println(objects);
    }
}

 

아래의 결과를 보기 전에 미리 생각해보자.

더보기
java.lang.Object@6576fe71
[Ljava.lang.Object;@76fb509a

생각했던 것과 일치했을까?

System.out.println

왜 이런 글자들이 찍혔는지 설명하기 전에 println 부터 확인해보자.

출력 기능을 하는 println의 메서드는 여러가지 형태로 오버로딩이 되어 있다.

Java 8 기준 JavaDoc 문서에 보면 See Also에 여러 오버로딩된 형태를 보여주는데 10가지가 있다.

  1. PrintStream.println(),
  2. PrintStream.println(boolean),
  3. PrintStream.println(char),
  4. PrintStream.println(char[])
  5. PrintStream.println(double)
  6. PrintStream.println(float)
  7. PrintStream.println(int)
  8. PrintStream.println(long)
  9. PrintStream.println(java.lang.Object)
  10. PrintStream.println(java.lang.String)

이중 어떤 것에 걸려서 출력이 되었을까?

더보기
정답은 9번 println(Object x) 이다.

즉, 객체 및 배열는 참조타입이기 때문에 모든 객체의 부모 클래스인 Object 로 나타낼 수 있기 때문이다.

PrintStream 의 println(Object x) 의 구현

System.out.println 의 구현을 보고 싶으면 System.out 의 타입인 PrintStream 을 찾아야 한다.

보통 아래와 같이 구현이 되어 있다. (상세 구현은 다를 수 있다.)

public class PrintStream extends FilterOutputStream
    implements Appendable, Closeable {
    // ...
    public void println(Object x) {
        String s = String.valueOf(x);
        synchronized (this) {
            print(s);
            newLine();
        }
    }
    // ...
}

결국은 객체를 문자열로 바꾸고 해당 문자열을 출력을 하는 식으로 구현이 되어 있다.

String.valueOf 의 구현

이제 문자열 클래스로 가보자. 

보통 아래와 같이 구현이 되어 있다. (역시나 상세 구현은 다를 수 있다.)

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    // ..
    public static String valueOf(Object obj) {
        return (obj == null) ? "null" : obj.toString();
    }
    // ..
}

결국 다시 Object 클래스의 toString() 메서드를 호출하는 것을 알 수 있다.

이펙티브 자바의 3장에 아이템 12. toString을 항상 재정의하라 라는 아이템이 있는데 왜 재정의를 해야하는지 엿볼 수 있다.

(여기저기에서 사용된다. 적절하게 객체를 표시하는 것이 바람직하다.)

Object.toString() 의 구현

public class Object {
    // ..
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
    
    public final native Class<?> getClass();
    // ..
}

네이티브 메소드인 getClass() 호출로 Class를 획득해서 getName() 한 문자열에 @ 문자 후에 해시코드를 출력함을 알 수 있다.

간혹 해시코드 값을 주소로 아는 분들도 최근 많이 보았다. (도시 전설인가??)

최근에 관련 글을 쓴 것이 있으니 이것을 참고하자.

Class.getName() 의 구현

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {
    //..
    public String getName() {
        String name = this.name;
        if (name == null)
            this.name = name = getName0();
        return name;
    }

    // cache the name to reduce the number of calls into the VM
    private transient String name;
    private native String getName0();
    //..
}

결국 위의 의문의 끝에 도달했다.

java.lang.Object@6576fe71
[Ljava.lang.Object;@76fb509a

에서 뒤의 @해시값을 제외한 앞의 부분이 바로 getName()이 찍는 것이다.

 

다만 내부에서 getName0()라는 네이티브 메서드를 호출하기 때문에 자바 코드상으로는 여기까지이다.

하지만 JavaDoc에 어떤 내용이 찍히는지에 대해 잘 적혀있다.

https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#getName--

클래스 객체는 배열이 아닌 경우에 자바 언어 규약에 명세된 형태로 클래스의 바이너리 이름을 반환한다.

java.lang.Object

...

객체들의 클래스로 표현되는 객체 클래스의 경우 내부적인 형태로 처리된다.

배열의 깊이(차원)에 따라 [ 캐릭터가 나오고 그 이후에는 타입에 따른 인코딩 값을 찍는다.

따라서 배열의 경우 [ 이라는 문자가 출력되고, 배열은 참조타입이기 때문에 L+ 클래스이름 + ; (새미콜론) 이 분는 형태가 되는 것이다.

[Ljava.lang.Object;

좀 더 깊이 알고 싶은 분들이 있을까 싶어 떡밥을 끄적여본다.

위에서 언급된 getName0 네이티브 메서드 부터 출발한다.

Class.java 의 JNI 의 구현은 Class.c 가 담당한다.

Class.c 매핑 메서드들

static JNINativeMethod methods[] = {
    {"getName0",         "()" STR,          (void *)&JVM_GetClassName},
    {"getSuperclass",    "()" CLS,          NULL},
    {"getInterfaces0",   "()[" CLS,         (void *)&JVM_GetClassInterfaces},
    {"getClassLoader0",  "()" JCL,          (void *)&JVM_GetClassLoader},
    {"isInterface",      "()Z",             (void *)&JVM_IsInterface},
    {"getSigners",       "()[" OBJ,         (void *)&JVM_GetClassSigners},
    {"setSigners",       "([" OBJ ")V",     (void *)&JVM_SetClassSigners},
    {"isArray",          "()Z",             (void *)&JVM_IsArrayClass},
    {"isPrimitive",      "()Z",             (void *)&JVM_IsPrimitiveClass},
    {"getComponentType", "()" CLS,          (void *)&JVM_GetComponentType},
    {"getModifiers",     "()I",             (void *)&JVM_GetClassModifiers},
    {"getDeclaredFields0","(Z)[" FLD,       (void *)&JVM_GetClassDeclaredFields},
    {"getDeclaredMethods0","(Z)[" MHD,      (void *)&JVM_GetClassDeclaredMethods},
    {"getDeclaredConstructors0","(Z)[" CTR, (void *)&JVM_GetClassDeclaredConstructors},
    {"getProtectionDomain0", "()" PD,       (void *)&JVM_GetProtectionDomain},
    {"getDeclaredClasses0",  "()[" CLS,      (void *)&JVM_GetDeclaredClasses},
    {"getDeclaringClass0",   "()" CLS,      (void *)&JVM_GetDeclaringClass},
    {"getGenericSignature0", "()" STR,      (void *)&JVM_GetClassSignature},
    {"getRawAnnotations",      "()" BA,        (void *)&JVM_GetClassAnnotations},
    {"getConstantPool",     "()" CPL,       (void *)&JVM_GetClassConstantPool},
    {"desiredAssertionStatus0","("CLS")Z",(void *)&JVM_DesiredAssertionStatus},
    {"getEnclosingMethod0", "()[" OBJ,      (void *)&JVM_GetEnclosingMethodInfo},
    {"getRawTypeAnnotations", "()" BA,      (void *)&JVM_GetClassTypeAnnotations},
};

getName0는 JVM_GetClassName 에 매핑이 되는 것을 알 수 있다.

JVM_GetClassName 메서드는 jvm.cpp 에서 정의를 한다.

jvm.cpp

JVM_ENTRY(jstring, JVM_GetClassName(JNIEnv *env, jclass cls))
  assert (cls != NULL, "illegal class");
  JVMWrapper("JVM_GetClassName");
  JvmtiVMObjectAllocEventCollector oam;
  ResourceMark rm(THREAD);
  const char* name;
  if (java_lang_Class::is_primitive(JNIHandles::resolve(cls))) {
    name = type2name(java_lang_Class::primitive_type(JNIHandles::resolve(cls)));
  } else {
    // Consider caching interned string in Klass
    Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve(cls));
    assert(k->is_klass(), "just checking");
    name = k->external_name();
  }
  oop result = StringTable::intern((char*) name, CHECK_NULL);
  return (jstring) JNIHandles::make_local(env, result);
JVM_END

이게 무슨 C++ 코드인가 할 수 있지만 매크로로 되어 있어서 그렇다.

대충 형태만 보면 primitive 타입이면(is_primitivejava_lang_Class::is_primitive) type2name 메서드를 호출하여 타입 이름을 가져오고,

그외 (참조 타입)이면 Klass의 external_name() 메서드를 호출해서 이름을 구해옴을 알 수 있다.

(위의 JavaDoc의 내용과 동일하게 구현이 되어 있음을 알 수 있다.)