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

[Java] Object

· 수정 · 📖 약 4분 · 1,452자/단어 #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 어느 것도 정확히 쓸 수 없다.

사용 상황

상황관련 메서드 / 접근법
객체를 Map/Set 키로 사용equals + hashCode 동시 override 필수
로그, 디버깅 출력toString override
멀티스레드 대기/신호wait / notify / notifyAll (synchronized 내)
런타임 타입 판별getClass() 또는 instanceof
복사본 생성복사 생성자 권장 (clone 대신)
GC 이후 자원 해제AutoCloseable + try-with-resources (finalize 대신)
하위 호환 컨테이너Object 타입 (타입 erasure 이전 레거시)

상속 계층 시각화

Java 표준 라이브러리의 주요 클래스는 모두 Object 에서 출발한다.

flowchart TD
    O["java.lang.Object"]
    O --> S["java.lang.String"]
    O --> N["java.lang.Number"]
    O --> AL["AbstractList"]
    O --> AM["AbstractMap"]
    N --> IN["Integer"]
    N --> LO["Long"]
    N --> DB["Double"]
    AL --> ARL["ArrayList"]
    AL --> LL["LinkedList"]
    AM --> HM["HashMap"]
    AM --> TM["TreeMap"]

모든 클래스의 공통 조상이므로 obj instanceof Object 는 항상 true (null 제외).

핵심 메서드 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 반환
flowchart LR
    A["equals override?"]
    A -->|"yes"| B["hashCode 도 override 필수"]
    A -->|"no"| C["참조 동일 시에만 equal"]
    B --> D["같은 필드로 hashCode 계산"]
    D --> E["HashMap, HashSet 정상 동작"]
    C --> F["HashMap 키 오작동 위험"]
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 으로 생성.

Objects 유틸리티 클래스

java.util.Objects 는 null-safe 헬퍼를 제공해 equals / hashCode / toString 구현을 간결하게 한다.

import java.util.Objects;

// null-safe equals: a == null 이어도 NPE 없음
Objects.equals(a, b);

// 복수 필드 hashCode 계산
Objects.hash(field1, field2, field3);

// null 이면 fallback 반환
Objects.toString(obj, "N/A");

// null 시 NPE + 메시지
Objects.requireNonNull(x, "x must not be null");

// 메서드 참조로 null 체크
stream.filter(Objects::nonNull).toList();

Java 16+ record 를 쓰면 equals, hashCode, 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() 기반으로 빈을 만든다. 자세한 내용은 java-reflection 참고.

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 등 설계자들도 이를 후회한다고 밝힌 적이 있다.

함정

1. equals 만 override 하고 hashCode 빠뜨리기

// 위험: hashCode override 없이 equals 만 구현
class Key {
    String value;
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Key k)) return false;
        return value.equals(k.value);
    }
    // hashCode 없음 → HashMap.get 이 null 반환
}

Map<Key, String> map = new HashMap<>();
map.put(new Key("x"), "hello");
map.get(new Key("x"));  // null (같은 내용인데 못 찾음)

2. instanceof 와 getClass() 의 혼용

class Animal {}
class Dog extends Animal {
    @Override
    public boolean equals(Object o) {
        // instanceof Animal 로 비교하면 Dog 와 Animal 간 대칭 규약 깨짐
        if (!(o instanceof Dog d)) return false;
        return true;
    }
}

Dog d = new Dog();
Animal a = new Animal();
// d.equals(a) 와 a.equals(d) 결과가 다를 수 있음

equals 에서 instanceof 를 쓸 때는 상속 관계에서 대칭 규약 이 깨지지 않도록 주의. getClass() 비교가 더 엄격한 동등성을 보장.

3. synchronized 없이 wait 호출

// 잘못된 코드
obj.wait();   // IllegalMonitorStateException 발생

// 올바른 코드
synchronized (obj) {
    obj.wait();
}

관련 위키

이 글의 용어 (10개)
[Java] BlockingQueuejava
정의 는 요소를 가져올 때 비어 있으면 대기, 넣을 때 가득 차 있으면 대기 하는 thread-safe 큐 인터페이스. 생산자-소비자 (producer-consumer) 패턴의 …
[Java] Collectionjava
정의 는 그룹으로 묶인 객체들을 표현하는 최상위 인터페이스. JCF (Java Collections Framework) 의 입구이자, / / / 모두 이를 확장한다. 자체는 직접…
[Java] CountDownLatchjava
정의 는 한 스레드 또는 여러 스레드가 다른 스레드들의 작업 완료를 기다리는 일회용 synchronizer. 으로 초기화하고, 각 worker 가 작업 후 호출. waiter 는…
[Java] Fail-Fast Iteratorjava
정의 Fail-Fast Iterator 는 순회 도중 컬렉션의 구조가 변경되면 즉시 을 던지는 iterator. , , 등 대부분의 컬렉션의 iterator 가 이 방식. 목적은…
[Java] HashMapjava
정의 는 해시 테이블 로 구현된 . 평균 O(1) 의 put/get/remove 를 보장하고 단일 스레드 환경의 사실상 표준. JDK 1.2 도입, Java 8 에서 충돌 시 l…
[Java] HashSetjava
정의 는 을 백킹으로 사용하는 구현. 내부적으로 모든 원소를 의 key 로 저장하고, value 는 dummy 상수 로 채운다. - 평균 O(1) 의 / / - 순서 보장 없음 …
[Java] Iterablejava
정의 는 루프로 순회 가능한 모든 타입의 최상위 인터페이스. 단 하나의 추상 메서드, 를 정의한다. 인터페이스가 을 extends 하므로 , , , 등 모든 컬렉션이 자동으로 대…
[Java] ReentrantLockjava
정의 는 키워드와 같은 상호 배제 (mutual exclusion) 를 제공하는 클래스 기반 락. JSR-166 (Java 5) 에서 추가됐다. "재진입 (reentrant)" …
[Java] Reflection: Class, Method, Field, MethodHandlejava
정의 Reflection 은 런타임에 클래스 메타데이터 (필드, 메서드, 어노테이션, 생성자) 를 조회하고, 컴파일 타임에 알 수 없는 객체를 생성/조작할 수 있게 해 주는 Ja…
[Java] Semaphorejava
정의 는 N 개의 permit (허가) 을 발급/회수 하는 synchronizer. permit 이 있으면 진행, 없으면 또는 즉시 실패. 자원 풀, rate limiting, …

💬 댓글

사이트 검색 / 명령어

검색

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