본문으로 건너뛰기
김신건의 로그

[Java] Object

· 수정 · 📖 약 3분 · 1,096자/단어 #java #fundamentals #oop #root-class
Object, java.lang.Object, Java Object, Object class, 오브젝트, java Object

정의

java.lang.Object 는 Java 의 모든 클래스의 최상위 부모 (root) 클래스. extends 가 명시되지 않은 클래스는 컴파일러가 자동으로 extends Object 를 붙인다. 인터페이스는 클래스가 아니라 Object 를 직접 상속하지는 않지만, 인터페이스 구현체는 결국 Object 의 메서드를 호출 가능.

이 단일 root 가 Java 의 collection, reflection, equality, threading 의 기초가 된다. Object 의 메서드를 이해하지 않고는 Collection, HashMap, Iterable 어느 것도 정확히 쓸 수 없다.

핵심 메서드 11개

메서드역할자주 override?
equals(Object)동등성 비교
hashCode()해시 코드 (Map/Set 용)
toString()문자열 표현
getClass()런타임 클래스 (final)
clone()얕은 복사 (protected)
finalize()GC 직전 호출 (deprecated)
wait(), wait(long), wait(long, int)모니터 대기 (final)
notify(), notifyAll()모니터 깨우기 (final)

final 인 것은 override 불가, 나머지는 가능.

equals / hashCode 의 5가지 규약

equals(Object) 를 override 하면 hashCode() 도 반드시 override 해야 한다. JCF (Java Collections Framework) 의 HashMap, HashSet 이 두 메서드의 일관성에 의존하기 때문.

equals(Object) 규약:

  1. 반사 (reflexive): x.equals(x) == true
  2. 대칭 (symmetric): x.equals(y) 이면 y.equals(x)
  3. 추이 (transitive): x.equals(y) && y.equals(z) 이면 x.equals(z)
  4. 일관 (consistent): 객체가 변하지 않으면 같은 결과
  5. null 비교: x.equals(null) == false

hashCode() 규약:

  • equals 가 true 인 두 객체는 같은 hashCode 를 반환해야 한다 (역은 아님)
  • 객체가 변하지 않으면 같은 hashCode 반환
public class Point {
    private final int x;
    private final int y;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point p)) return false;
        return x == p.x && y == p.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
}

IMPORTANT

equals 만 override 하고 hashCode 를 안 하면 HashMap, HashSet조용히 잘못 동작 한다. 같은 점이 두 번 들어가거나, 넣은 값을 못 찾는 식. @Override annotation 으로 컴파일러 검사를 유도하라.

toString, 디버깅의 기본

기본 구현은 ClassName@hexHashCode 같은 형태로 거의 쓸모없다. 자기 자신을 의미있게 표현하도록 override 권장.

@Override
public String toString() {
    return "Point(%d, %d)".formatted(x, y);
}

Java 16+ 의 record 는 자동 생성. lombok 도 @ToString 으로 생성.

wait / notify, monitor 패턴

wait(), notify(), notifyAll()모니터 락 (monitor lock) 위에서만 호출 가능. 그래서 반드시 synchronized 블록 안에서 호출해야 한다.

synchronized (obj) {
    while (!condition) {
        obj.wait();   // 락 해제하고 대기
    }
    // condition 충족, 처리
}

// 다른 스레드
synchronized (obj) {
    condition = true;
    obj.notifyAll();  // 대기 중인 모든 스레드 깨움
}

규칙:

  • synchronized 밖에서 호출하면 IllegalMonitorStateException
  • 조건은 항상 if 가 아닌 while 로 검사 (spurious wakeup 가능)
  • notify 보다 notifyAll 이 안전 (단일 wakeup 으로 누가 깨어났는지 알 수 없음)

CAUTION

새 코드에서는 wait/notify 대신 ReentrantLock + Condition, CountDownLatch, Semaphore, BlockingQueue 같은 고수준 동기화 도구 를 권장한다. wait/notify 는 미묘한 버그 (spurious wakeup, lost notification) 를 만들기 쉽다.

모든 컬렉션의 root 가 Object 인 이유

JCF 의 메서드는 거의 모두 Object 타입을 받는다.

public interface Collection<E> {
    boolean contains(Object o);   // ← Object, not E
    boolean remove(Object o);     // ← Object, not E
    boolean equals(Object o);
}

이는 타입 erasureinvariance 때문이다. List<Integer>.contains(double) 도 컴파일은 되지만 항상 false. 이런 미묘한 함정의 원인. Joshua Bloch Effective Java Item 65 가 이를 다룬다.

getClass, Reflection 의 입구

getClass()final 이라 override 불가. 런타임에 정확한 클래스를 알려준다.

Object x = "hello";
Class<?> c = x.getClass();          // String.class
String name = c.getName();          // "java.lang.String"
Method[] methods = c.getMethods();   // public 메서드 목록

Reflection, Annotation 처리, Serialization 등의 진입점. Spring 의 DI 컨테이너도 결국 getClass() 기반으로 빈을 만든다.

clone, 거의 안 쓰는 것이 답

Object.clone()protected 라 외부에서 호출 못 한다. 사용하려면 Cloneable 인터페이스를 구현하고 clone()public 으로 override 해야 한다. 그런데 깊은 복사 (deep copy) 는 직접 처리해야 하므로 함정이 많다.

IMPORTANT

대안: 복사 생성자 (new Foo(other)) 또는 정적 팩토리 메서드 (Foo.copyOf(other)) 가 훨씬 명확하다. Joshua Bloch Effective Java Item 13 이 “clone 은 피해라” 라고 단정.

finalize, deprecated

finalize() 는 GC 가 객체를 회수하기 직전에 호출되도록 설계됐지만:

  • 호출 시점 불확실, 영원히 안 불릴 수도 있다
  • 성능 저하, finalizer 가 있는 객체는 GC 가 두 번 본다
  • 보안 취약점, finalizer attack
  • Java 9+ 에서 @Deprecated, Java 18+ 부터 --finalization=disabled 옵션

대안: AutoCloseable + try-with-resources, Cleaner 클래스 (java.lang.ref.Cleaner).

Object 만 받는 메서드와 제네릭

Collection.contains(Object) 처럼 제네릭이 도입된 뒤에도 Object 인자가 남아있다. 이는 하위 호환성 때문, JDK 1.0 코드가 그대로 동작해야 했다. Joshua Bloch 등 설계자들도 이를 후회한다고 밝힌 적이 있다.

참고

  • Iterable
  • Collection
  • fail-fast iterator
  • Joshua Bloch, Effective Java (3rd ed.), Items 10-14 (equals, hashCode, toString, clone, comparable)
  • Brian Goetz, Java Concurrency in Practice, §14.2 Using Condition Queues
이 글의 용어 (4개)
[Java] Collectionjava
정의 는 그룹으로 묶인 객체들을 표현하는 최상위 인터페이스. JCF (Java Collections Framework) 의 입구이자, / / / 모두 이를 확장한다. 자체는 직접…
[Java] Fail-Fast Iteratorjava
정의 Fail-Fast Iterator 는 순회 도중 컬렉션의 구조가 변경되면 즉시 을 던지는 iterator. , , 등 대부분의 컬렉션의 iterator 가 이 방식. 목적은…
[Java] Iterablejava
정의 는 루프로 순회 가능한 모든 타입의 최상위 인터페이스. 단 하나의 추상 메서드, 를 정의한다. 인터페이스가 을 extends 하므로 , , , 등 모든 컬렉션이 자동으로 대…
[Java] ReentrantLockjava
정의 는 키워드와 같은 상호 배제 (mutual exclusion) 를 제공하는 클래스 기반 락. JSR-166 (Java 5) 에서 추가됐다. "재진입 (reentrant)" …

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기