index

Object

최상위 클래스 상속 관계를 가진 타입으로 자바의 모든 클래스는 암묵적으로 Object를 상속한다

모든 타입을 다룰 수 있는 공통 타입으로 다형성을 활용할 수 있으며 객체로서 기본적으로 가져야 할 메서드를 제공한다

자바의 객체는 heap 영역에 저장되며 Object 클래스의 메서드는 method area에 메타데이터로 저장된다

equals, hashCode

객체 비교와 해시 기반 컬렉션(HashMap, HashSet)에서 사용하는 메서드로 두 객체가 동등한지 판단하기 위해 사용된다

identity

동일성이란 객체의 참조가 같은지를 의미하는 것으로 == 연산자를 사용하여 비교한다

== 연산자를 객체 간에 사용하는 경우 참조 변수가 가리키는 메모리 주소를 기준으로 비교하므로 동일한 객체를 가리킬 경우에만 true를 반환한다

@Test
void objectIdentityTest() {
    Object o1 = new Object();
    Object o2 = new Object();

    Object o1_other = o1;

    // false: identity of two different objects
    assertThat(o1 == o2).isFalse();

    // true: identity of two variable that has same reference
    assertThat(o1 == o1_other).isTrue();
}

equality

동등성은 객체의 상태(값)가 같은지를 의미하는 것으로 데이터를 기준으로 비교하여 같은 데이터를 가진 두 객체는 동등하다고 논리적으로 판단한다

같은 데이터를 가진 두 객체를 동등하다고 판단하려면 비교하려는 객체에서 Object의 equals 메서드를 오버라이딩해야 한다

자바에서 equals와 hashCode 메서드는 밀접한 관계를 가지는데, 이는 해시 기반 자료구조(HashMap, HashSet, HashTable 등)에서는 요소를 비교하기 위해 두 메서드를 함께 사용하기 때문이다

해시 기반 자료구조의 객체 비교 로직은 일반적으로 hashCode 값이 다르면 서로 다른 객체라고 판단하고, 같다면 equals 메서드로 객체가 논리적으로 같은지 확인한다

@Test
void objectEqualityTest() {
    Person p1 = new Person("hansanhha", "developer", 10);
    Person p2 = new Person("hansanhha", "developer", 10);

    // false: identity of two different objects
    assertThat(p1 == p2).isFalse();

    // true: equality of two different objects that has same state
    assertThat(p1.equals(p2)).isTrue();
}

equals

Object 클래스의 equals 메서드는 기본적으로 두 참조 변수의 메모리 주소를 비교한다

public boolean equals(Object obj) {
    return (this == obj);
} 

hashCode

Object 클래스의 hashCode 메서드는 native 라이브러리를 호출하여 객체의 메모리 주소를 해싱한 값(고유 식별 번호)을 반환한다

@IntrinsicCandidate
public native int hashCode();

equals, hashCode overriding

아래의 Person 클래스의 상태(필드)를 기준으로 동등한지 판단하기 위해 equals와 hashCode를 재정의한다

public class Person {

    String name;
    String job;
    int age;

    public Person(String name, String job, int age) {
        this.name = name;
        this.job = job;
        this.age = age;
    }

    // equals overriding for equality comparison
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;

        Person person = (Person) obj; // down casting
        return Objects.equals(name, person.name)
                && Objects.equals(job, person.job)
                && age == person.age;
    }

    // hashCode overriding according to equals overriding
    @Override
    public int hashCode() {
        return Objects.hash(name, job, age);
    }

}

자바에는 equals와 hashCode 메서드가 반드시 지켜야 하는 규칙이 있다

사용자가 각 메서드를 오버라이딩하더라도 객체 비교와 해시 기반 자료구조에서 정상적으로 동작하도록 하기 위함이다

equals 오버라이딩 규약

hashCode 오버라이딩 규약

equals의 결과 값이 참인 객체는 같은 해시코드를 가져야 하는 규칙에 따라 equals 메서드를 오버라이딩할 때 hashCode도 반드시 오버라이딩해줘야 한다

만약 둘 중 하나만 오버라이딩한 경우 객체 비교 동작 과정에서 오류가 발생할 수 있다

또한 equals에서 비교하는 모든 필드는 반드시 hashCode에도 포함되어야 한다

해시 기반 컬렉션은 객체의 해시코드를 먼저 비교한 후 equals를 호출하여 동등성을 확인하기 때문에 equals 메서드에 사용된 필드가 hashCode 메서드에도 포함되지 않으면 동등한 객체를 찾지 못해서 해시 기반 컬렉션에서 객체를 찾지 못하는 문제가 생길 수 있다

toString

toString 메서드의 기본 구현은 클래스명과 hashCode 값을 반환한다

자바는 모든 클래스에서 toString 메서드를 사람이 읽을 수 있는 형태로 간단히 객체를 표현할 수 있는 문자열 값을 반환하도록 오버라이딩할 것을 권장한다

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

자바의 toString 메서드는 객체를 문자열로 변환할 필요가 있을 때 암묵적으로 호출되는 규칙을 가진다

Person p1 = new Person("hansanhha", "developer", 10);

// hansanhha.classes.built_in.object.Person@a413a2b5
System.out.println(p1);

getClass

getClass 메서드는 객체의 런타임 클래스를 나타내는 Class 객체를 반환한다

리플렉션에서 클래스 정보에 접근하기 위해 많이 사용된다

@IntrinsicCandidate
public final native Class<?> getClass();
Person p1 = new Person("hansanhha", "developer", 10);

assertThat(p1.getClass().getName()).isEqualTo("hansanhha.classes.built_in.object.Person");
assertThat(p1.getClass().getSimpleName()).isEqualTo("Person");

clone

clone은 현재 인스턴스를 복제하여 새로운 Object 타입의 객체를 반환한다

new 키워드를 이용하여 모든 필드를 직접 복사하는 코드를 작성할 필요없이 원본 객체의 상태를 유지하면서 새로운 객체를 생성할 수 있다

그러나, 이 메서드는 다음과 같은 제약이 있다

@IntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
@Test
void cloneMethod() {
    Person person = new Person("hansanhha", "developer", 10);
    Person clone = person.clone();

    assertThat(person).isEqualTo(clone);
}

shallow copy vs deep copy

shallow copy는 clone 메서드의 기본 동작으로 객체의 primitive type 필드 값은 새로 복제되지만 reference type은 새 객체를 생성하지 않고 기존 객체의 메모리 주소만 복제한다

따라서 원본 객체와 복제 객체가 동일한 객체를 참조하게 되어 한 쪽에서 해당 객체를 수정하면 다른 객체에 영향을 준다

@Test
void shallowCopyReferenceTypeTest() {
    ShallowAddress seoul = new ShallowAddress("seoul");
    ShallowOrder order = new ShallowOrder("12345", seoul);
    ShallowOrder cloneOrder = order.doClone();

    // Order 클래스는 Object.toString을 호출하는데, 이 메서드는 객체의 참조 주소값을 사용한다
    // 복제된 클래스는 기존 객체와 다른 메모리 주소를 가지기 때문에 동일성을 만족하지 못한다
    assertThat(order).isNotEqualTo(cloneOrder);

    // clone 메서드는 shallow copy로 동작하여 참조 타입의 필드에 대해 메모리 주소만 복제한다
    // 원본 객체와 복제 객체 모두 객체를 공유하기 때문에 해당 참조의 상태를 변경하면 다른 객체에 모두 영향을 준다
    cloneOrder.getAddress().setCity("busan");
    assertThat(cloneOrder.getAddress().getCity()).isEqualTo(order.getAddress().getCity());
}

deep copy는 참조 타입 필드까지 새롭게 복제하는 방식으로 원본 객체와 동일한 객체를 공유하지 않는다

다만 참조 타입의 객체가 많거나 각 객체의 생성 비용이 클수록 복제 비용이 커질 수 있다

자바에서 deep copy는 아래와 같이 직접 구현해야 한다

DeepAddress는 DeepOrder에서 참조하는 객체로 Cloneable 인터페이스를 구현하고 자신의 상태를 기반으로 새로운 인스턴스를 생성하는 동작으로 clone 메서드를 재정의한다

DeepOrder도 마찬가지로 Cloneable 인터페이스를 구현한 뒤 clone 메서드에서 복제 DeepOrder 인스턴스의 deepAddress 필드에 DeepAddress의 clone 메서드를 통해 새로운 참조 타입의 객체를 할당한다

public class DeepAddress implements Cloneable {

    String city;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return new DeepAddress(city);
    }
}
public class DeepOrder implements Cloneable {

    DeepAddress deepAddress;

    @Override
    public DeepOrder clone() {
        try {
            DeepOrder cloned = (DeepOrder) super.clone();
            cloned.deepAddress = (DeepAddress) deepAddress.clone();
            return cloned;
        } catch (
                CloneNotSupportedException e) {
            throw new RuntimeException();
        }
    }
}

위의 DeepAddress와 DeepOrder에 대한 deep copy 테스트 코드는 아래와 같다

@Test
void deepCopyReferenceTypeTest() {
    DeepAddress seoul = new DeepAddress("seoul");
    DeepOrder order = new DeepOrder("12345", seoul);
    DeepOrder cloneOrder = order.doClone();

    // deep copy는 참조 타입 필드까지 새로운 객체를 생성하기 때문에 원본 객체와 복제 객체가 동일한 객체를 공유하지 않는다
    cloneOrder.getDeepAddress().setCity("busan");
    assertThat(cloneOrder.getDeepAddress().getCity()).isNotEqualTo(order.getDeepAddress().getCity());
}

wait, notify, notifyAll

wait(), notify(), notifyAll() 메서드는 스레드 간 동기화 메커니즘에서 사용된다

멀티스레딩 환경에서 여러 스레드가 공유 자원에 접근하고자 할 때 스레드 간 모니터 락을 위해 사용되며 synchronized 블록 안에서만 호출할 수 있다

wait: 무기한 대기 또는 주어진 파라미터 값에 따라 일정 시간만 대기하는 메서드

notify: 같은 모니터를 사용하는 대기 중인 스레드 중 랜덤으로 하나를 깨운다

notifyAll: 같은 모니터를 사용하는 대기 중인 모든 스레드를 깨운다 (다만 모든 스레드가 실행되지 않고 순차적으로 락을 획득한 스레드부터 실행될 수 있음)

아래의 SharedResource는 공유 자원 필드인 data에 대한 동기화를 위해 wait와 notify 메서드를 사용한다

public class SharedResource {

    private String data;
    private boolean lock;

    public synchronized void produce(String value) throws InterruptedException {
        // 다른 스레드가 락을 가져갔다면 대기한다
        while (lock) {
            wait();
        }
        data = value;
        lock = true;
        System.out.println("produced: " + data);
        
        // 락을 가진 스레드가 작업을 끝내고 대기 중인 스레드를 깨운다
        notify();
    }

    public synchronized String consume() throws InterruptedException {
        while (!lock) {
            wait();
        }
        lock = false;
        System.out.println("consumed: " + data);
        notify();
        return data;
    }
}

finalize

@Deprecated(since="9", forRemoval=true)
protected void finalize() throws Throwable { }

객체가 가비지 컬렉터에 의해 소멸되기 직전에 호출되어 직접 객체를 정리할 수 있는 메서드로 protected 접근 제어자를 가진다

이 메서드는 자바 9부터 @Deprecated 되어 사용이 권장되지 않는데 그 이유는 다음과 같다

예측 불가능한 실행 지점: gc가 실행될 때만 finalize 메서드가 호출되므로, 객체가 언제 정리될지 예측할 수 없다 (즉시 필요한 리소스 해제 작업을 맡기기 부적절함)

성능 저하: finalize를 구현한 객체는 gc가 객체를 바로 수거하지 않고 “finalization queue”에 넣어 비동기 처리한다 (성능 저하 및 메모리 누수 가능성 증가)

부족한 신뢰성: finalize 내에서 예외가 발생하면 무시되거나 예상하지 못한 오류가 발생할 수 있다

위와 같은 이유로 deprecated 되었고 대신에 try-with-resources 문 또는 명시적인 close 메서드를 사용하여 리소스를 정리하는 것이 안전하다