4. getClass()
해당 객체가 실제로 속한 클래스(runtime class)를 리턴한다.
JVM 뜯어보기
Object.getClass() 메서드는 해당 객체가 실제로 속한 클래스(runtime class)를 리턴한다. 좀 더 자세하게 말하면 해당 객체의 Class 객체가 리턴된다.
// src/java.base/java/lang/Object.java
public class Object {
@IntrinsicCandidate
public final native Class<?> getClass();
}
역시 Object의 getClass() 메서드도 네이티브 메서드로 구현되어 있다. JVM에서 해당 구현 코드를 찾아보자.
// src/java.base/share/native/libjava/Object.c
JNIEXPORT jclass JNICALL
Java_java_lang_Object_getClass(JNIEnv *env, jobject this)
{
if (this == NULL) {
JNU_ThrowNullPointerException(env, NULL);
return 0;
} else {
return (*env)->GetObjectClass(env, this);
}
}
해당 부분은 Object.c에 Java_java_lang_Object_getClass로 구현되어 있다. 객체를 this로 받은 뒤 만약 null이라면 NullPointerException 예외를 던지고, 0를 돌려준다. null이 아니라면 다시 GetObjectClass함수의 결과를 돌려준다. 그럼 객체의 Class 객체는 GetObjectClass함수에 의해 리턴 받을 수 있을 것이라고 예상할 수 있다. GetObjectClass함수를 보자.
// src/hotspot/share/prims/jni.cpp
JNI_ENTRY(jclass, jni_GetObjectClass(JNIEnv *env, jobject obj))
HOTSPOT_JNI_GETOBJECTCLASS_ENTRY(env, obj);
Klass* k = JNIHandles::resolve_non_null(obj)->klass();
jclass ret =
(jclass) JNIHandles::make_local(THREAD, k->java_mirror());
HOTSPOT_JNI_GETOBJECTCLASS_RETURN(ret);
return ret;
JNI_END
GetObjectClass함수는 다음과 같이 Hotspot JVM을 통해 구현되어 있다. obj의 Klass객체를 얻은 뒤 java_mirror()함수를 통해 실제 객체 데이터인 oop객체를 리턴받고, make_local() 메서드로 jclass 객체로 만들어주고 리턴해준다. 이 때, Klass는 자바의 클래스를 C언어의 class와 구별하기 위해 만든 C언어 클래스다. 즉, 자바 Class를 C언어에서 Klass로 정의해 준 것이다. 어쨌든 jclass를 얻기 위해서는 다시 java_mirror() 함수를 거쳐야 한다. java_mirror() 함수를 보자.
// src/hotspot/share/oops/klass.inline.hpp
inline oop Klass::java_mirror() const {
return _java_mirror.resolve();
}
java_mirror() 함수는 다음과 같이 Java의 'Class'객체를 참조하는 _java_mirror 필드에서 resolve() 메서드를 호출해 실제 Java의 'Class' 객체를 리턴한다. 그럼 다시 resolve() 메서드를 보자.
// src/hotspot/share/oops/oopHandle.inline.hpp
inline oop OopHandle::resolve() const {
return (_obj == nullptr) ? (oop)nullptr : NativeAccess<>::oop_load(_obj);
}
OopHandle의 resolve() 메서드는 해당 객체의 실제 메모리 주소를 나타내는 _obj 멤버 변수가 null포인터가 아니면 다시 NativeAccess의 oop_load()메서드를 거쳐 객체를 안전하게 읽어온다. oop_load() 메서드 안에서는 OopLoadProxy 클래스에 해당 객체의 메모리 주소를 담아 메모리에 액세스하고 객체를 안전하게 가져오는 코드가 담겨 있지만 이 부분은 너무 복잡해서 그냥 그렇구나하고 넘기는 게 정신 건강에 좋을 것 같다. 결론만 말하면 Object.getClass() 메서드는 객체의 메모리 주소를 참조해서 'Class'객체를 리턴받는다.
'Class'객체는 여러 개의 인스턴스가 있어도 클래스 당 한 개만 존재한다. 따라서 어떤 객체에 getClass()메서드를 사용하면 해당 객체의 클래스에 대한 정보를 얻을 수 있는 것이다. 이제 실제로 getClass() 메서드를 사용해보자.
사용해보기
public class Main {
public static void main(String[] args) {
Person person_1 = new Person(988515);
System.out.println(person_1.getClass() instanceof Class);
System.out.println("=======================");
System.out.println(person_1.getClass());
System.out.println(person_1.getClass().getName());
System.out.println("=======================");
System.out.println(person_1);
}
}
true
=======================
class Person
Person
=======================
Person@f1563
Person이라는 클래스를 만들고 person_1이라는 인스턴스를 만들어 주었다. 첫번째 출력에서 볼 수 있듯이 person_1에서 getClass() 메서드를 호출한 결과는 'Class' 클래스의 인스턴스이므로 true다. 다음으로 getClass()의 결과를 출력하면 class Person이 출력되고, 여기에 'Class' 클래스의 메서드인 getName()을 호출하고 출력하면 클래스 이름인 Person이 출력된다. 우리가 만든 클래스를 그대로 출력하면 보면 맨 아래처럼 Person@f1563의 형태로 출력되던데 왜 Class객체는 저렇게 깔끔하게 출력될까.
이유는 println()메서드는 내부적으로 toString()메서드를 호출한 결과를 반환하며, 'Class' 클래스는 toString()메서드를 오버라이딩해서 class를 앞에 붙이도록 수정했기 때문이다. 다음의 두 코드는 Class 클래스의 toString(), PrintStream의 println() 메서드, 그리고 println()메서드에서 다시 호출하는 String클래스의 valueof()메서드를 보여준다.
// src/java.base/java/lang/Class.java
public final class Class<T> implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement,
TypeDescriptor.OfField<Class<?>>,
Constable {
public String toString() {
return (isInterface() ? "interface " : (isPrimitive() ? "" : "class "))
+ getName();
}
}
보다시피 Class 클래스의 toString() 메서드는 단순히 클래스의 이름만을 출력하는 것이 아니라 해당 클래스가 인터페이스라면 interface, 클래스라면 class, 원시타입이라면 공백을 앞에 붙여 출력하는 것을 알 수 있다. 그럼 이제 println() 메서드를 확인해보자.
// src/java.base/java/io/PrintStream.java
public class PrintStream extends FilterOutputStream
implements Appendable, Closeable
{
public void println(Object x) {
String s = String.valueOf(x);
if (getClass() == PrintStream.class) {
// need to apply String.valueOf again since first invocation
// might return null
writeln(String.valueOf(s));
} else {
synchronized (this) {
print(s);
newLine();
}
}
}
}
System의 out 멤버변수는 OS에 맞게 적절한 PrintStream을 가진다. 즉, OS에 따라 어디로 출력해줄 지 자바가 알아서 결정해준다. 따라서 System.out은 PrintStream 인스턴스를 가지며, println() 메서드를 호출하면 아규먼트의 내용을 출력해 준다. 여기 println() 메서드를 보면 내부적으로 다시 String 클래스의 valueOf() 메서드를 호출해 x라는 객체를 String 타입으로 변환시켜 주는 것을 알 수 있다. 그럼 valueOf()메서드는 어떻게 구현되어 있을까.
// src/java.base/java/lang/String.java
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
}
valueOf() 메서드에 오면 드디어 객체에 toString()을 호출한 결과를 리턴해주는 것을 알 수 있다. 따라서 'Class'객체를 println() 메서드로 출력하면 Class.toString()의 결과를 얻게 되는 것이다.
'프로그래밍 > Java' 카테고리의 다른 글
[Java] JDK 부수기 - (2) java.lang.System - 1. err, in, out (0) | 2023.11.23 |
---|---|
[Java] JDK 부수기 - (1) java.lang.Object - 5. toString (0) | 2023.11.21 |
[Java] JDK 부수기 - (1) java.lang.Object - 3. hashCode (0) | 2023.11.21 |
[Java] JDK 부수기 - (1) java.lang.Object - 2. equals (0) | 2023.11.20 |
[Java] JDK 부수기 - (1) java.lang.Object - 1. clone (0) | 2023.11.20 |