간단한 코드이다.
아래의 코드를 수행하면 어떤 글자들이 콘솔에 찍힐까?
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가지가 있다.
- PrintStream.println(),
- PrintStream.println(boolean),
- PrintStream.println(char),
- PrintStream.println(char[])
- PrintStream.println(double)
- PrintStream.println(float)
- PrintStream.println(int)
- PrintStream.println(long)
- PrintStream.println(java.lang.Object)
- PrintStream.println(java.lang.String)
이중 어떤 것에 걸려서 출력이 되었을까?
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의 내용과 동일하게 구현이 되어 있음을 알 수 있다.)
'Programing > JVM(Java, Kotlin)' 카테고리의 다른 글
[Java] "서로 다른 두 객체는 결코 같은 해시코드를 가질 수 없다."? (0) | 2020.01.28 |
---|---|
[java] Java 8의 시간은 10000년을 파싱 못한다? (0) | 2020.01.23 |
[Java] String: literal vs new (0) | 2020.01.09 |
[Java] hashCode() internal : String, Object (0) | 2020.01.09 |
[Java] 한 영역(scope)에서 변수를 두 번 선언할 수 없는 이유? (1) | 2019.11.23 |