티스토리 뷰

Language/Java

Object 클래스

DUCKBAE's 2024. 11. 5. 14:32

클래스를 생성할 때 IDE나 프레임워크에서 제공하는 어노테이션으로 toString, equals, hashCode 메서드를 쉽게 재정의를 해왔었던 것 같다.
그래서 이 메서드들이 정의된 최상위 클래스인 Object 클래스와 해당 메서드들에 대해 알아보려고한다.


Object 클래스

Object 클래스는 자바 클래스 계층 구조의 최상위 클래스이다.
자바의 모든 클래스는 기본적으로 Object 클래스의 자식 클래스이므로, 클래스를 선언할 때 extends 키워드를 사용하여 명시적으로 상속하지 않더라도 컴파일러가 자동으로 Object 클래스를 상속받도록 처리한다.

//일반적인 클래스 선언
public class Car {}

//실제로는 Object 클래스를 상속함
public class Car extends Object {}

 

객체의 비교, equals()

다른 객체와 동등한지 여부를 나타내는 메서드이다.

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

Object 클래스의 equals 메서드는 == 연산자를 사용하여 객체 비교를 한다. 이는, Heap 영역에 할당 된 두 객체의 주소를 비교하는 것이다.
Object 의 equals 메서드를 재 정의하여 두 객체를 비교해서 논리적으로 동등한지를 구현할 수 있다. 논리적으로 동등하다는 것은 객체의 주소를 비교하는 것이 아니라, 객체가 저장하고 있는 데이터가 동일하다는 것을 의미한다.
예를 들어, Object 의 equals 메서드를 재 정의한 String 의 equals 메서드는 객체의 주소를 비교하는 것이 아니라 문자열의 값이 동일한지를 확인한다.

// String.java
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    return (anObject instanceof String aString)
            && (!COMPACT_STRINGS || this.coder == aString.coder)
            && StringLatin1.equals(value, aString.value);
}

// StringLatin1.java
public static boolean equals(byte[] value, byte[] other) {
        if (value.length == other.length) {
            for (int i = 0; i < value.length; i++) {
                if (value[i] != other[i]) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

먼저 두 객체가 동등한지 판단하고, 동등하다면 바로 true 를 리턴한다. 주소가 같으면 데이터 값도 동일하기 때문이다.
만약 두 객체가 동등하지 않았을 때에는 비교 대상의 객체 타입, 인코딩 방식, 두 객체의 문자를 하나씩 비교하는 것을 볼 수 있다.

public class Main {
    public static void main(String[] args) {
        String s1 = new String("hello");
        String s2 = new String("hello");

        System.out.println(s1 == s2); //false
        System.out.println(s1.equals(s2)); //true
    }
}

주소값을 비교하는지 데이터값을 비교하는지 확인하기 위해 new 연산자를 사용하여 동일한 데이터 값을 갖는 String 객체를 생성하였다. (동일한 값을 갖는 문자열 리터럴을 생성한다면, 동일한 String 객체를 참조하기 때문에 == 연산자의 결과로 true 가 출력된다.)
Heap 영역의 서로 다른 공간에 각 객체가 할당되기 때문에 주소 값을 비교하는 == 연산자의 결과로는 false 가 출력된다. 하지만, 두 객체 모두 동일한 데이터 값을 갖기 때문에 equals 결과로는 true 가 출력된다.
 

객체 해시코드, hashCode()

객체를 식별하는 정수값인 해시코드를 리턴하는 메서드이다.
Object 클래스의 hashCode 메서드는 객체의 메모리 주소를 이용해서 해시코드를 만들어 리턴하기 때문에 객체마다 다른 값을 가진다.

public class Main {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();

        System.out.println(o1.hashCode()); //1175962212
        System.out.println(o2.hashCode()); //918221580
    }
}

 

이 메서드는 해시 기반 컬렉션에서 중요한 역할을 담당한다. hashCode의 결과값은 해시 기반 자료구조에서 빠르게 검색될 수 있도록 도와주는 값이 된다.
그래서 hashCode 메서드를 재정의할 때에는 객체의 속성 값에 기반하여 고유한 정수 값을 생성해야한다.
 

equals() 와 hashCode() 관계

객체의 동등함을 비교하기 위해서 두 메서드가 같이 사용되어야 한다.
equals 메서드에 따라 두 객체가 동등한 경우에 두 객체 모두 hashCode는 같은 값을 반환해야한다.
그렇지 않으면 해시 기반 컬렉션 프레임워크 사용할 때 문제가 발생할 수 있다.
equals 메서드만 재정의한 자동차 클래스를 하나 생성해보았다.

class Car {
    private int price;
    private String brand;
    private String type;

    public Car(int price, String brand, String type) {
        this.price = price;
        this.brand = brand;
        this.type = type;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) return true;
        if (other == null || getClass() != other.getClass()) return false;

        Car car = (Car) other;

        return price == car.price &&
                Objects.equals(brand, car.brand) &&
                Objects.equals(type, car.type);
    }
}

자동차 객체를 생성하고 hash 기반 컬렉션 프레임워크를 사용하여 객체의 동등성을 확인해보았다.

public class Main {

    public static void main(String[] args) {
        Car car1 = new Car(30000000, "hyundai", "suv");
        Car car2 = new Car(30000000, "hyundai", "suv");
        
        System.out.println(car1 == car2); //false
        System.out.println(car1.equals(car2)); //true
        System.out.println(car1.hashCode()); //918221580
        System.out.println(car2.hashCode()); //2055281021

        Set<Car> cars = new HashSet<>();
        cars.add(car1);
        cars.add(car2);

        System.out.println(cars.size()); //2
    }   
}

== 연산자를 사용했을 때에는 서로 다른 주소값에 할당되어있기 때문에 결과는 false이다.
equals 메서드를 수행하였을 때에는 결과는 true가 나온다. 이는 Car 클래스에 equals 메서드를 재정의하였고, 실제 데이터의 값을 비교하도록 구현하였기 때문이다.
hashCode 값은 서로 다르게 출력된다. 이는 hashCode를 재정의하지 않았으므로 Object 클래스의 hashCode 메서드가 호출된 것이다.
두 객체를 HashSet 자료구조에 추가하고 데이터 사이즈를 출력해보면 결과는 2가 나온다. 이는 hashCode가 다르기 때문에 동등한 객체로 보지 않은것이다.

class Car {
    ...
    
    @Override
    public boolean equals(Object other) {
        if (this == other) return true;
        if (other == null || getClass() != other.getClass()) return false;

        Car car = (Car) other;

        return price == car.price &&
                Objects.equals(brand, car.brand) &&
                Objects.equals(type, car.type);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(price, brand, type);
    }
}

public class Main {
    public static void main(String[] args) {
        Car car1 = new Car(30000000, "hyundai", "suv");
        Car car2 = new Car(30000000, "hyundai", "suv");
        
        Set<Car> cars = new HashSet<>();
        cars.add(car1);
        cars.add(car2);

        System.out.println(cars.size()); //1
    }
}

hashCode 메서드를 재정의하고 다시 HashSet 자료구조에 추가된 데이터의 사이즈를 출력해보면 1이 나온다는 것을 알 수 있다.
따라서, 객체의 동등 비교를 위해서는 equals와 hashCode 메서드를 함께 재정의하여 논리적으로 동등한 객체일 경우 동일한 해시코드가 리턴되도록 해야한다.
 

객체의 문자 정보, toString()

객체 정보를 문자로 리턴하는 메서드이다.
Object 클래스의 toString 메서드는 다음과 같이 클래스명@16진수해시코드로 구성된 문자 정보를 리턴한다.

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

Object 의 하위 클래스들은 toString 메서드를 재정의하여 객체의 상태 정보를 의미있게 리턴하도록 구현되어있다.
예를 들어, HashMap 클래스는 map 에 저장되어 있는 key와 value 를 출력하도록 toString 메서드를 재정의하였다.

//HashMap.java
public final String toString() { return key + "=" + value; }

public class Main {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "kona");
        map.put(2, "tucson");
        map.put(3, "ionicq");
        
        System.out.println(map);
    }
}
...
{1=kona, 2=tucson, 3=ionicq}

객체를 출력할 때 toString 메서드를 호출하지 않은 이유는 println 메서드 내에서 자동으로 toString 메서드를 호출하기 때문이다.
map 객체를 출력하면 HashMap에서 재정의한 toString 메서드 구현을 기반으로 객체의 정보가 출력되는 것을 확인할 수 있다.
 

예제를 통해 Object 클래스의 메서드 재정의해보기

class Car {
    private int price;
    private String brand;
    private String type;

    @Override
    public boolean equals(Object other) {
        if (this == other) return true; //두 객체의 주소가 같으면 true
        if (other == null || getClass() != other.getClass()) return false;

        Car car = (Car) other;
        
        return price == car.price &&
                Objects.equals(brand, car.brand) &&
                Objects.equals(type, car.type);
    }

    @Override
    public int hashCode() {
        return Objects.hash(price, brand, type);
    }

    @Override
    public String toString() {
        return "[자동차 정보] 가격:" + price +
                ", 브랜드: " + brand +
                ", 종류: " + type;
    }
}

Object 클래스는 자바 클래스 계층의 최상위 클래스로, 모든 클래스는 자동으로 이 클래스를 상속받는다.
모든 클래스가 공통적으로 사용할 수 있는 기본 메서드들을 정의하여 하위 클래스들이 재정의할 수 있도록한다.객체 문자열을 표현할 때에는 toString 메서드를, 해시 기반 컬렉션을 사용할 때에는 hashCode 를, 객체 비교시에는 equals, hashCode를 함께 구현한다.
다음에는 hash 기반 컬렉션의 동작에 대해 알아봐야겠다.

'Language > Java' 카테고리의 다른 글

Hash에 대해서  (0) 2024.11.08
List Collection  (0) 2024.11.07
예외와 에러에 대하여  (5) 2024.11.03
상속에 대하여  (0) 2024.10.31
인터페이스에 대하여  (0) 2024.10.27
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함