devseop08 님의 블로그

[API] Object 클래스와 Class 클래스 본문

Language/Java

[API] Object 클래스와 Class 클래스

devseop08 2025. 8. 5. 19:57

Object

  • 루트 클래스: 자바의 모든 클래스는 암묵적으로 Object 클래스를 상속한다.
  • class A {}class A extends Object {}와 동일하다.
  • 주요 메서드와 기능: 기본적으로 객체의 동일성, 동기화/통신(wait/notify), 복제, 런타임 타입 정보 등을 제공하는 메서드를 정의한다.
메서드 설명 비고 / 주의
equals(Object o) 객체의 논리적 동등성을 비교. 기본 구현은 참조(identity) 비교 (==). 재정의 시 hashCode()도 반드시 함께 재정의해야 한다.
hashCode() 객체를 해시 테이블에 넣을 때 사용하는 정수 값. equals와의 계약이 있음. equals가 같다면 hashCode도 같아야 한다.
toString() 객체를 사람이 읽을 수 있는 문자열로 표현. 기본은 ClassName@hexHashCode 형태. 커스텀 구현으로 디버깅/로깅에 유용하게 만든다.
getClass() 런타임에 실제 객체의 클래스 객체(Class<?>)를 반환. 제네릭 타입 소거 때문에 instanceof 검사와 조합해서 많이 사용.
notify(), notifyAll() 같은 객체의 모니터를 기다리는 스레드들을 깨운다. 반드시 해당 객체의 synchronized 블록 안에서 호출해야 한다.
wait() (및 오버로드) 현재 스레드를 객체의 모니터 대기열로 넣고 해제. 스퍼리어스 웨이크업 대비로 조건을 while 루프에서 다시 검사해야 한다.
clone() 객체의 얕은 복사(shallow copy)를 만든다. 기본은 protected이며 Cloneable을 구현하지 않으면 CloneNotSupportedException이 난다. 대부분의 경우 대안 패턴을 권장.
finalize() 가비지 컬렉션 직전에 호출될 수 있는 정리 메서드. Deprecated 됐고, 예측 불가능하며 사용하지 말고 Cleaner나 명시적 자원 해제를 써야 한다.

C++ 인스턴스의 고유성

  • C++과 같은 네이티브 언어로 작성된 프로그램은 호스트의 실제 메모리 상에 직접적으로 로드되어 실행된다.
  • 따라서 C++ 프로그램 실행 중 생성되는 인스턴스의 저장 위치는 호스트의 실제 메모리 상의 주소로 나타낼 수 있는데
  • 이 때 C++ 인스턴스는 소멸할 때까지 저장 위치가 변하지 않으므로 C++ 인스턴스는 실제 메모리 상에 저장된 주소로 고유하게 식별될 수 있다.

자바 인스턴스의 고유성

  • C++과 같은 네이티브 언어와 달리 자바로 작성된 프로그램은 JVM이라는 하나의 프로세스 상에서 실행되기 때문에
  • 호스트의 실제 메모리 상에 직접적으로 로드되지 않고 JVM 프로세스를 통해 간접적으로 로드되어 실행된다.
  • 따라서, 자바 프로그램 실행 중 생성되는 인스턴스의 저장 위치는 호스트의 실제 메모리 상의 주소가 아닌 JVM 프로세스 상에서 인식되는 위치로 나타내게 된다.
  • 그런데, JVM 프로세스 상에서의 자바 인스턴스 저장 위치는 고정적이지 않기 때문에 JVM 프로세스 상에서 인식되는 위치로는 자바 인스턴스를 고유하게 식별할 수 없다.
  • 자바 인스턴스는 JVM 프로세스의 가비지 컬렉터에 의해 JVM 프로세스 상에서의 저장 위치가 유동적으로 변한다.
  • 그렇다면 무엇을 기준으로 JVM 프로세스 상의 자바 인스턴스를 고유하게 식별할 수 있을까?

hash 알고리즘

  • 자바 인스턴스를 고유하게 식별하기 위해선 그것이 JVM 프로세스 상의 어느 위치에 있든 간에 동일한 값으로 표현되게 하는 것이 관건이다.
  • hash 알고리즘은 이 요건을 충족시켜줄 수 있는 연산을 수행한다.
    • hash 알고리즘이 수행하는 연산은 f(x) -> y만 가능한 단반향 함수와 같다.
    • f(x) -> y만 가능한 단반향 함수라는 것은, 해당 함수의 입력값으로 단일한 출력값을 추론하는 것은 가능하지만
    • 해당 함수의 출력값으로부터 단일한 입력값을 추론하는 역방향 연산은 불가한 함수라는 뜻이다.
    • 예를 들면 나머지 연산자가 전형적인 단방향 연산을 수행한다.
    • x % 2 = 1이라는 수식을 하나의 함수라고 할 때, 이 함수의 출력은 1로 항상 고정적이지만 입력값 x는 고정적이지 않다. 출력값이 1이 되게 할 수 있는 x값은 그 개수가 무한한다.
  • 결국 임의의 자바 인스턴스의 위치 변경과 상관없이 해당 자바 인스턴스를 고유하게 식별하기 위해선
  • 하나의 자바 인스턴스에 대해서, hash 알고리즘을 적용하여 그것이 어느 위치에 있든 간에 고정적으로 동일한 값을 반환하는 연산을 수행하는 메서드가 필요하다.

hashCode 메서드

  • 위와 같이, hash 알고리즘을 적용하여 자바 인스턴스를 고유하게 식별할 수 있게 하는 메서드가 모든 자바 클래스의 루트 클래스인 Object 클래스에 구현되어 있는데, 그것이 바로 hashCode 메서드이다.
// java.lang.Object

@IntrinsicCandidate  
public native int hashCode();
  • Object 클래스의 hashCode 메서드의 default 구현은 native로 선언돼 있는데, 이것은 해당 메서드에서 실제 실행되는 코드가 JVM의 C++로 구현된 코드라는 의미이다.
  • 그 구현이 어찌됐든 간에 결국 Object 클래스의 hashCode 메서드는 하나의 자바 인스턴스가 JVM 프로세스 상에서의 위치 변경과 상관없이 동일한 값을 반환하도록 하여 고유하게 식별될 수 있게 한다.
  • 그런데 하나의 자바 인스턴스에 대해서 hashCode 메서드를 수행하여 얻은 int 타입의 반환값이 하나의 자바 인스턴스의 유동적인 위치들에 대해서는 반드시 고유성을 갖는다고 할 수 있지만
  • 과연 이 반환값이 JVM 프로세스 상의 모든 인스턴스들에 대해서도 반드시 고유성을 갖는다고 할 수 있을까?

hash 충돌 문제

  • Object 클래스의 hashCode 메서드가 반환하는 값은 아쉽게도 JVM 프로세스 상의 모든 자바 인스턴스들에 대해서는 고유성을 갖지 못한다.
  • 어떤 하나의 자바 인스턴스로부터 hashCode 메서드를 호출하여 반환받은 반환값과 해당 인스턴스와는 전혀 다른 위치에 저장돼 있는 자바 인스턴스에서 hashCode 메서드를 호출하여 반환받은 반환값이 동일할 수 있다는 것이다.
  • 이러한 현상을 hash 충돌 문제라고 한다.
  • hash 충돌 문제는 심지어 두 인스턴스가 전혀 다른 타입이더라도 발생 가능하다.

equals 메서드의 필요성

  • hash 충돌 문제로 인하여, 두 자바 인스턴스가 같은 지를 판별한다고 할 때
  • 두 인스턴스로부터 hashCode 메서드를 호출하여 얻은 반환값을 비교하는 것만으로는 부족하다.
  • 두 자바 인스턴스로부터 hashCode 메서드를 호출하여 얻은 반환값을 비교함으로써 두 자바 인스턴스가 같을 가능성을 판별할 수는 있지만
  • 실제로 두 자바 인스턴스가 같다고 판별하는 연산은 추가로 필요한데, 이를 수행하는 메서드가 Object 클래스의 eqauls 메서드다.
  • equals 메서드는 그것을 호출하는 객체와 인자로 전달되는 객체가 같은 지 여부를 판단하여 boolean 타입으로 반환하게 된다.
  • hashCode 메서드와 equals 메서드의 계약 관계
  • 위와 같이 두 자바 인스턴스가 같은 지를 판별함에 있어서 hashCode 메서드로는 부족하여 equals 메서드가 추가됨으로 인해 두 메서드 간에는 설계적으로 계약 관계가 성립하게 된다.
  • 두 자바 인스턴스 간의 equals 메서드의 반환값이 true라면, 두 자바 인스턴스의 hashCode 메서드 반환값은 반드시 동일하다.
  • 따라서 두 자바 인스턴스의 hashCode 메서드 반환값이 다르다면, 두 자바 인스턴스 간의 equals 메서드의 반환값은 반드시 false이다.
  • 또한 두 자바 인스턴스 간의 equals 메서드의 반환값이 false인 경우에는 두 자바 인스턴스의 hashCode 메서드 반환값은 서로 같을 수도 있고 다를 수도 있으며
  • 두 자바 인스턴스의 hashCode 메서드 반환값이 같은 경우, 두 자바 인스턴스 간의 equals 메서드의 반환값은 true일 수도 있고 false일 수도 있다.
  • 두 자바 인스턴스의 hashCode 메서드 반환값이 같은지 여부는 두 자바 인스턴스가 같을 가능성을 의미하는 것이지 두 자바 인스턴스가 실제로 같다는 의미가 아니다.
  • 두 자바 인스턴스 간의 equals 메서드의 반환값이 두 자바 인스턴스가 실제 같은지 여부를 의미한다.

hashCode 메서드와 equals 메서드의 계약 관계를 이용하는 컬렉션 클래스

  • 인스턴스를 요소로 저장하는 컬렉션 타입의 인스턴스는, 새로운 인스턴스를 추가 저장하거나 자신에게 저장된 여러 개의 인스턴스 중 원하는 인스턴스를 탐색, 수정, 삭제하는 데 있어서hashCode 메서드와 equals 메서드의 계약 관계를 반영한 구현을 해놓은 메서드를 호출하게 된다.
  • hashCode 메서드와 equals 메서드의 계약 관계를 반영한 구현을 해놓은 컬렉션 클래스: HashMap, HashSet, Hashtable 등등
  • HashMap 클래스의 get 메서드: 두 인스턴스의 hashCode 메서드 반환값을 비교하여 두 인스턴스가 같을 가능성을 판별 후에 equals 메서드를 호출하여 실제로 두 인스턴스가 같은지를 판별한다.
// HashMap.java

public V get(Object key) {  
    Node<K,V> e;  
    return (e = getNode(key)) == null ? null : e.value;  
}  

final Node<K,V> getNode(Object key) {  
    Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;  
    if ((tab = table) != null && (n = tab.length) > 0 &&  
        (first = tab[(n - 1) & (hash = hash(key))]) != null) {  
        if (first.hash == hash && // always check first node  
            ((k = first.key) == key || (key != null && key.equals(k))))  
            return first;  
        if ((e = first.next) != null) {  
            if (first instanceof TreeNode)  
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);  
            do {  
                if (e.hash == hash &&  
                    ((k = e.key) == key || (key != null && key.equals(k))))  
                    return e;  
            } while ((e = e.next) != null);  
        }  
    }  
    return null;  
}

static final int hash(Object key) {  
    int h;  
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  
}

객체의 동일성과 동등성

  • Object 클래스 equals default 구현은 두 인스턴스의 참조를 비교한다. => 두 참조가 실제 완전히 동일하게 하나의 위치에 존재하는 하나의 인스턴스를 가리키고 있는지를 판별한다.
// java.lang.Object

public boolean equals(Object obj) {  
    return (this == obj);  
}
  • 두 참조가 실제 완전히 동일하게 하나의 위치에 존재하는 하나의 객체를 바라보고 있는 성질을 객체의 동일성이라고 한다. => Object 클래스 equals 메서드의 default 구현은 객체의 동일성을 판단한다.
  • 두 참조가 각각 서로 다른 위치에 따로 존재하는 객체를 바라보고 있을 때, 두 객체의 내용이 같은 성질을 객체의 동등성이라고 한다.
  • 보통 두 자바 인스턴스가 같다는 것은 객체의 동등성을 말한다.
  • 따라서 보통 어떤 클래스를 만든다고 할 때, 객체의 동일성을 판단하는 equals 메서드의 default 구현을 객체의 동등성을 판단하는 구현으로 오버라이딩한다.
public class Member {
    public int id;

    public Member(int id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object obj) {
        if(obj instanceof Member) {
            Member member = (Member) obj;
            if(id == member.id){
                return true;
            }
        }
        return false;
    }
}

equals 메서드 오버라이딩과 hashCode 메서드 오버라이딩

  • equals 메서드를 오버라이딩 하는 경우엔 hashCode 메서드도 이에 맞춰 오버라이딩이 필요하다.
  • 그 이유는 hashCode 메서드와 equals 메서드 간의 계약 관계를 유지하는 설계를 하기 위함이다.
  • equals 메서드의 default 구현을 객체의 동등성을 판단하도록 오버라이딩하면, 두 인스턴스 간의 일치 여부를 두 인스턴스의 멤버 변수를 기반으로 판별하기 때문에
  • hashCode 메서드와 equals 메서드 간의 계약 관계를 유지하도록 hashCode 메서드도 인스턴스의 멤버 변수를 기반으로 하여 오버라이딩 해야 한다.
  • equals 메서드의 default 구현을 객체의 동등성을 판단하도록 오버라이딩을 해놓고 hashCode 메서드는 인스턴스의 멤버 변수를 기반으로 한 오버라이딩을 하지 않는다면
  • hashCode 메서드와 equals 메서드 간의 계약 관계를 이용하여, 자신의 요소 인스턴스를 탐색, 수정, 삭제하는 컬렉션 인스턴스의 메서드가 제대로 동작하지 않는다.
  • 즉, 두 인스턴스 간의 equals 메서드의 반환값은 실제 true인데 두 인스턴스의 hashCode 메서드의 반환값은 같지 않을 수 있다는 것이다.
  • 원래는 equals 메서드의 반환값이 true면 두 인스턴스의 hashCode 메서드의 반환값은 반드시 같았어야 한다.
  • equals 메서드는 객체의 동등성을 판단하도록 오버라이딩 해놓고 hashCode 메서드는 오버라이딩하지 않은 클래스의 인스턴스를 요소로 갖는 HashMap 컬렉션에서 get 메서드 호출 시 예상과 다르게 동작한다.
public class Key {
    public int id;

    public Key(int id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object obj) {
        if(obj instanceof Member) {
            Key key = (Key) obj;
            if(id == key.id){
                return true;
            }
        }
        return false;
    }
}
public class KeyExample {
    public static void main(String[] args) {
        HashMap<Key> hashMap = new HashMap<Key, String>();

        hashMap.put(new Key(1), "Kim");

        String name = hashMap.get(new Key(1));
        System.out.println(name); // null
    }
}
  • Kim이 출력될 것이라는 예상과 달리 null이 출력된다.
  • hashCode 메서드와 equals 메서드의 계약 관계를 유지하기 하도록 인스턴스 멤버 변수를 기반으로 Key 클래스의 hashCode 메서드를 오버라이딩한다.
public class Key {
    ...
    @Override
    public int hashCode() {
        return id;
    }
}
  • 그런데 인스턴스 멤버 변수를 기반으로 하지 않아도, hashCode 메서드와 equals 메서드 간의 계약 관계를 유지하도록 hashCode 메서드를 오버라이딩할 수 있기는 하다.
  • 어떤 인스턴스 멤버 변수값을 갖던 상관없이 모두 동일하게 같은 값을 반환하도록 hashCode 메서드를 오버라이딩하면 된다.
public class Key {
    ...
    @Override
    public int hashCode() {
        return 42;
    }
}
  • 그런데 위와 같은 hashCode 메서드 오버라이딩은 너무나 큰 비효율성을 야기한다.
  • 이런 방식의 hashCode 메서드 오버라이딩을 한 Key 클래스의 인스턴스를 컬렉션 요소로 갖는 HashMap 컬렉션의 요소 탐색은 굉장히 비효율적일 것이다.
  • 인자로 주어진 Key 클래스 인스턴스의 hashCode 메서드 반환값과 각 요소 인스턴스의 hashCode 메서드 반환값이 전부 다 동일할 것이므로
  • 인자로 주어진 Key 클래스 인스턴스와 같을 수 있는 후보군은 HashMap 컬렉션의 모든 요소 인스턴스들이 된다.
  • 이는 해당 HashMap 컬렉션의 모든 요소 인스턴스들의 내용을 전부 다 확인해봐야 한다는 것이다.
  • HashMap 컬렉션의 요소 개수가 늘어날수록 이러한 비효율성은 더욱 증대될 것이다.
  • 권장하는 hashCode 메서드 오버라이딩
  • hashCode 메서드 오버라이딩은 단순히 hashCode 메서드와 equals 메서드 간의 계약 관계를 유지하도록만 하면 되는 것이 아니라, 그와 동시에 해쉬 충돌을 최대한 줄임으로써 효율성을 가질 수 있는 방향으로 이뤄져야 한다.
  • 따라서 hashCode 메서드 오버라이딩 시, Objects.hash 메서드나 Arrays.hashCode 메서드와 같은 유틸리티 메서드를 이용하는 것을 권장한다.
public class Member {
    public int id;
    public String name;

    public Member(int id, String name) {
        this.id = id;
        this.name = new String(name);
    }

    @Override
    public boolean equals(Object obj) {
        if(obj instanceof Member) {
            Member member = (Member) obj;
            if(id == member.id && name.equals(member.name)){
                return true;
            }
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name)
    }
}
// Objects.java

public static int hash(Object... values) {  
    return Arrays.hashCode(values);  
}

toString 메서드

  • Object 클래스의 toString 메서드 기본 구현
public String toString() {  
    return getClass().getName() + "@" + Integer.toHexString(hashCode());  
}
  • Object 클래스의 toString 메서드의 기본 구현은 자바 애플리케이션에서는 별 값어치가 없으므로 Object 하위 클래스는 toString 메서드를 간결하고 유익한 정보를 반환하도록 재정의해야 한다.
public class SmartPhone {
    private String company;
    private String os;

    public SmartPhone(String company, String os){
        this.company = company;
        this.os = os;
    }

    @Override
    public String toString(){
        return company + ", " + os;
    }
}

자바 16+의 record는 위의 요구사항들을 자동으로 만족하도록 equals/hashCode/toString을 만들어준다:

public record Person(String name, int age) {}

clone 메서드

  • Object 클래스의 clone 메서드는 인스턴스를 복제할 목적으로 사용되는 메서드다.
@IntrinsicCandidate  
protected native Object clone() throws CloneNotSupportedException;
  • 그런데 Object 클래스의 clone 메서드의 기본 구현은 필드 단위로 메모리 블록을 그대로 복제한다.
  • 즉, 원시 타입(int, boolean 등) 필드는 값 자체가 복사되고, 참조 타입(String, List 등) 필드는 참조(포인터)만 복사되는 얕은 복사를 수행한다.
  • 따라서 임의의 클래스 인스턴스를 해당 클래스의 clone 메서드를 사용하여 복제한다고 할 때
  • 기존 clone 메서드의 얕은 복사로 인한 사이드 이펙트가 발생하지 않게 하기 위해선 해당 클래스의 clone 메서드가 깊은 복사를 수행하도록 오버라이딩 해야한다.

clone 메서드가 깊은 복사를 수행하도록 오버라이딩할 때 주의사항

  1. clone 메서드의 기본 구현은 접근 지정자가 protected로 선언되었기 때문에 오버라이딩 시에는 public 으로 선언해야 한다.
  2. clone 메서드를 오버라이딩하는 클래스는 해당 클래스가 clone 메서드를 사용하는 클래스임을 표시하는 마커 인터페이스인Cloneable 인터페이스를 implents 하도록 해야 한다.
    • clone 메서드가 깊은 복사를 수행하도록 하기 위해서는 우선 super.clone()으로 얕은 복사가 선행되어야 하는데, super.clone()은 결국 Object 클래스 clone 메서드의 기본 구현이다.
    • Object 클래스 clone 메서드의 기본 구현은 native 선언된 메서드로 내부에는 해당 인스턴스가 Cloneable인터페이스를 구현하고 있는지 검사하는 로직이 존재한다.`
    • if (!(this instanceof Cloneable)) { throw new CloneNotSupportedException(); }
    • 따라서 Object 클래스의 clone 메서드를 오버라이딩하는 클래스가 Cloneable 인터페이스를 implements 하지 않으면 깊은 복사를 위해 재정의한 clone 메서드의 구현 내에서 super.clone() 실행 시 CloneNotSupportedException이 발생한다.
  3. 오버라이딩한 clone 메서드가 super.clone()을 호출하는 이상 CloneNotSupportedException 발생 가능성을 품고 있기 때문에 try-catch 구문과 같은 예외 처리 구문이 필요하다.
  4. clone 메서드 구현 내에서 깊은 복사하려는 데이터가 ArrayList와 같이 JDK에 이미 포함된 클래스가 아닌 새로 만든 클래스의 인스턴스 데이터라면 해당 클래스 또한 Cloneable 인터페이스를 implements 하고 해당 클래스의 clone 메서드가 깊은 복사를 수행하도록 오버라이딩 해야한다.
  5. 또한 clone 메서드 내에선 해당 메서드를 호출한 인스턴스 클래스의 생성자를 이용하지 않으므로 인스턴스 초기화 로직을 거치지 않게 되어 생성자 초기화 로직에도 주의해야 한다.
public class MyClass implements Cloneable {
    private List<String> listField = new ArrayList<>();
    private Child child;

    @Override
    public MyClass clone() {
        try {
            MyClass copy = (MyClass) super.clone(); // ① 얕은 복제 수행
            // ② mutable 참조 필드에 대해 깊은 복사
            copy.listField = new ArrayList<>(this.listField);
            copy.child = this.child.clone();
            return copy;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e); //절대 발생하지 않음 (Cloneable 구현)
        }
    }
}

public class ArrayList<E> extends AbstractList<E>  
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
    ...

    public Object clone() {  
    try {  
        ArrayList<?> v = (ArrayList<?>) super.clone();  
        v.elementData = Arrays.copyOf(elementData, size);  
        v.modCount = 0;  
        return v;  
    } catch (CloneNotSupportedException e) {  
        // this shouldn't happen, since we are Cloneable  
        throw new InternalError(e);  
    }  
}
}

public class Child implements Cloneable {
    private String name;
    private List<String> list = new ArrayList<>();
    ...
    @Override
    public Object clone() {  
    try {  
        Child copy = (Child) super.clone();
        copy.name = new String(name);
        copy.list = new ArrayList<>(this.list); // 깊은 복사
        return copy;
    } catch (CloneNotSupportedException e) {  
        ...
    }  
}

clone 메서드를 깊은 복사를 수행하도록 오버라이딩하는 건 너무 번거롭다.

  • Object 클래스의 clone 메서드를 깊은 복사를 수행하도록 오버라이딩할 때는 신경써야 할 게 너무 많다.
  • protected 선언된 것을 public으로 바꿔줘야 하고 Cloneable 인터페이스를 implements 해줘야 하고 예외 처리 구문을 만들어야 하고, 다른 참조 타입의 필드를 깊은 복사하기 위해 해당 타입의 클래스에서 clone 메서드를 같은 메커니즘으로 오버라이딩해야 한다.
  • 이 과정은 너무 복잡하기 때문에 보통 인스턴스를 깊은 복사할 시엔, 위와 같이 Object 클래스의 clone 메서드를 이용하는 것을 버리고, 복사 생성자와 같은 아예 새로운 패턴을 이용한다.

clone 메서드 대신 복사 생성자로 인스턴스 깊은 복사

public class MyClass {
    private List<String> listField = new ArrayList<>();
    private Child child;

    public MyClass(ArrayList<String> listField, String name){
        this.listField = new ArrayList<>(listFiled);
        this.child = new Child(name);
    }

    // 복사 생성자
    public MyClass(MyClass myClass) {
        this(myClass.listField, myClass.child.getName()); // 깊은 복사
    }
}

getClass 메서드

  • 런타임에 그 객체가 실제로 속한 클래스의 Class<?> 인스턴스를 반환한다.
  • Object에 선언된 final 메서드라 서브클래스에서 오버라이드할 수 없다.
@IntrinsicCandidate  
public final native Class<?> getClass();
  • 런타임 타입: 컴파일 시에는 참조 타입이 일반화되어 있을 수 있지만, getClass()는 실제 객체의 클래스 정보를 준다.
Object o = "hello";
System.out.println(o.getClass()); // class java.lang.String
  • Class 객체는 유니크: JVM 내에서 하나의 클래스에 대해 하나의 Class 객체만 존재하므로 ==로 비교해도 정확한 판단이 가능하다.
if (a.getClass() == b.getClass()) { ... } // 같은 런타임 클래스인지 체크

getClass 메서드 흔한 사용 사례

  1. 정확한 타입 비교 (getClass() vs instanceof) : 이 방식은 서브클래스와의 비교를 거부한다(다형성 허용 안 함). instanceof서브타입도 허용하므로 설계 의도에 따라 선택해야 한다.
if (obj.getClass() != this.getClass()) return false; // equals 구현에서
  1. 리플렉션의 출발점: getClass()로 얻은 Class<?> 객체를 통해 생성자, 필드, 메서드 등을 런타임에 조사하거나 호출할 수 있다.
Method m = obj.getClass().getMethod("toString");
String s = (String) m.invoke(obj);
  1. 방어적 검증 / 런타임 타입 제한: API 입력값을 엄격히 제한할 때
if (input == null || input.getClass() != String.class) {
    throw new IllegalArgumentException("정확히 String만 허용");
}
  1. 디버깅 / 로깅
logger.debug("처리 중인 객체 유형: {}", obj.getClass().getName());

getClass 메서드 내부 동작과 관련된 주의점

  1. 클래스 로더와 Class 객체: 같은 클래스 이름이라도 다른 클래스 로더에 의해 로드되면 서로 다른 Class 객체가 된다. => getClass() 비교 시 클래스 로더 차이도 의미 있게 작용한다.
Class<?> a = loader1.loadClass("com.example.Foo");
Class<?> b = loader2.loadClass("com.example.Foo");
// a != b
  1. 제네릭 타입 소거와의 한계: getClass()는 런타임 객체의 실제 클래스만 알려주므로, 제네릭 타입 파라미터 정보(List<String> vs List<Integer>)는 알 수 없다.

Class

  • 자바는 클래스와 인터페이스의 메타 데이터를 java.lang 패키지에 소속된 Class 클래스로 관리한다.
  • 여기서 메타 데이터란 클래스의 이름, 생성자 정보, 메서드 정보를 말한다.

클래스 객체 얻기

  1. 클래스로부터 얻는 방법
Class clazz = String.class;
Class clazz = Class.forName("java.lang.String");
  1. 객체로부터 얻는 방법
String str = "김자바";
Class clazz = str.getClass();

.class vs obj.getClass() vs Class.forName(...)

표현 설명
SomeClass.class 컴파일 타임에 타입이 정해진 클래스 리터럴. 해당 클래스의 Class 객체를 바로 얻음.
obj.getClass() 런타임 객체의 실제 클래스. null이면 호출 불가.
Class.forName("fully.qualified.Name") 문자열 이름으로 클래스를 로딩(및 초기화될 수 있음)하여 Class 객체를 얻음.