모든 클래스의 부모클래스인 Object클래스(toString(), hashCode(), equals())


java.lang.Object


자바에서 어떤 클래스를 상속받으려면 클래스에 extends를 붙여주면 됩니다.

(자바의 상속은 여기 참고)


그런데 extends를 붙여주지 않아도 자바의 모든 클래스는 기본적으로

java.lang.Object를 상속받습니다.

만약

public class class1 extends class2

이렇게 코드를 작성해서 class2라는 클래스를 상속받는다면,

자바는 이중 상속이 불가능하기 때문에, class1은 object를 직접적으로 상속받지는 않지만

class2가 object클래스를 상속받으므로, 결과적으로 class1도 object클래스를 상속받게 됩니다.

즉, class1 -> class2 -> object 인 셈입니다.


이는 JAVA API에서도 확인할 수 있습니다.

구글에서 검색해 JAVA API를 들어갑니다.

img_google_java_api

Java API - Oracle Help Center 입니다.

들어가서 아무 클래스나 클릭해봅니다.

저는 자바에서 제일 많이 사용하는 java.lang 패키지에서

제일 처음에 있는 Boolean클래스를 클릭했습니다.

img_java_api_boolean

빨간 테투리 안을 보면 Boolean클래스도 Object클래스를 상속받는 걸 확인할 수 있습니다.


그런데 어떤 클래스는 Object클래스를 확장하지 않는것도 있습니다.

예를 들어 Float클래스를 클릭해보면,

img_java_api_float

Number클래스를 상속받는다고 나옵니다.

하지만 이 Number클래스를 클릭해서 어떤 클래스를 상속받는지 살펴보면

img_java_api_number

결국에는 Object클래스를 상속받는 걸 확인할 수 있습니다.


이렇게 모든 클래스가 Object클래스를 상속받는 이유는

Object클래스에 있는 메소드들을 통해서 클래스의 기본적인 행동을 정의할 수 있기 때문입니다.


전화기로 비유해 보겠습니다.

전화기에는 여러 종류가 있습니다. 유선전화기, 폴더폰, 스마트폰, 탱크폰…

img_tank_phone (어렸을때 집에서 봤었던 모토로라 탱크폰…)

전화기는 종류마다 특색이 다르지만, 전화기의 메인 기능은 통화기능입니다.

이 통화기능이 없으면 전화기라고 부를 수 없죠

전화기(Object) 클래스는 통화 메소드를 가지고 있는거고,

다른 유선전화, 폴더폰, 스마트폰 클래스들은 전화기 클래스를 상속받아

전화기의 한 종류라는 타이틀을 가질 수 있게 되는 것입니다.


Object클래스에는 여러 메소드들이 존재하는데,

이 중, toString(), hashCode(), equals()만 알아보겠습니다.

(아직 초짜라서..)




toString() 메소드


toString()은 객체의 이름이라고 볼 수 있습니다.

package c.inheritance;

public class ToString {
    public static void main(String[] ar){
        ToString ex = new ToString();

        ex.toStringMethod(ex);
//        c.inheritance.ToString@2471cca7
//        c.inheritance.ToString@2471cca7
//        plus c.inheritance.ToString@2471cca7
        System.out.println();
        ex.toStringMethod2();
//        c.inheritance.ToString@2471cca7
//        c.inheritance.ToString@2471cca7
//        plus c.inheritance.ToString@2471cca7
    }

    public void toStringMethod(Object obj){
        System.out.println(obj);
        System.out.println(obj.toString());
        System.out.println("plus " + obj);
    }

    public void toStringMethod2(){
        System.out.println(this);
        System.out.println(toString());
        System.out.println("plus " + this);
    }
}

toStringMethod를 먼저 살펴보면 객체를 받아서

객체, 객체.toString(), 문자열+객체

이렇게 출력하고 있습니다.

그냥 객체를 출력하면 해당 객체의 부모 클래스인 Object클래스의 toString()메소드가 실행됩니다.

문자열+객체 로 출력하면 문자열+객체.toString()이 출력됩니다.


toStringMethod2()에서는 toStringMethod()를 더 간단히 만든 것입니다.

여기서 this는 자신의 객체 즉, ex.toStringMethod2()에서 ex를 참조합니다.


출력 결과(주석)을 바탕으로

toString()이 Object클래스에 어떻게 구현되어 있는지 살펴보겠습니다.

실제 Object클래스에는 toString()메소드가 다음과 같이 구현되어 있습니다.

getClass().getName() + “@” + Integer.toHexString(hashCode())

c.inheritance.ToString@2471cca7 이렇게 나왔는데요

getClass().getName()이 패키지이름.클래스이름 으로 나오는 걸 볼 수 있습니다.

뒤에 hashCode()는 객체의 고유값을 리턴합니다.

이 hashCode()를 Integer.toHexString()으로 16진수로 변환하고

여기서는 그 결과값으로 2471cca7이 나왔습니다.


이 toString()메소드를 그대로 사용하는 것보단

오버라이딩해서 쓰는게 좋습니다.

예를 들어,

package c.inheritance;

public class ChampionDTO {
    public String name;
    public int power;
    public int defense;

    public String toString(){
        return "name: " + name + ", Power: " + power + ", Defense: " + defense;
    }

    public ChampionDTO(String name, int power, int defense){
        this.name = name;
        this.power = power;
        this.defense = defense;
    }

    public static void main(String[] ar){
        ChampionDTO teemo = new ChampionDTO("teemo", 90, 20);
        System.out.println(teemo);
//        name: teemo, Power: 90, Defense: 20
    }
}

이렇게 toString()을 오버라이딩하여

해당 객체의 변수를 확인하는 용도로 쓰면 편의성이 증대됩니다.

이렇게 안쓰면 해당 객체의 변수를 확인하려면

teemo.name, teemo.power, teemo.Defense

이런식으로 확인해야하기 떄문에 매우 번거롭습니다.

이렇게 toString()을 오버라이딩해서 일일히 써주는 것도 번거롭기 때문에

보통 개발툴에서는 자동으로 오버라이딩 해주는 기능이 있습니다.

(여기에 설명되어 있습니다.)




equals() (동일 객체인지 확인하기)


다음과 같은 ChampionDTO가 있고,

package c.inheritance;

public class ChampionDTO {
    public String name;
    public int power;
    public int defense;

    public String toString(){
        return "name: " + name + ", Power: " + power + ", Defense: " + defense;
    }

    public ChampionDTO(String name, int power, int defense){
        this.name = name;
        this.power = power;
        this.defense = defense;
    }

    public static void main(String[] ar){
        ChampionDTO teemo = new ChampionDTO("teemo", 90, 20);
        System.out.println(teemo);
//        name: teemo, Power: 90, Defense: 20
    }
}

동일한 ChampionDTO 객체를 두개 만들어 비교하는 코드를 작성합니다.

package c.inheritance;

public class Equals {
    public static void main(String[] ar){
        Equals ex = new Equals();
        ex.equalMethod1();
//        teemo1 != teemo2
        ex.equalMethod2();
//        teemo1.equals(teemo2) returns false
    }

    public void equalMethod1(){
        ChampionDTO teemo1 = new ChampionDTO("teemo", 95, 20);
        ChampionDTO teemo2 = new ChampionDTO("teemo", 95, 20);

        if(teemo1 == teemo2){
            System.out.println("teemo1 == teemo2");
        }else{
            System.out.println("teemo1 != teemo2");
        }
    }

    public void equalMethod2(){
        ChampionDTO teemo1 = new ChampionDTO("teemo", 95, 20);
        ChampionDTO teemo2 = new ChampionDTO("teemo", 95, 20);

        if(teemo1.equals(teemo2)){
            System.out.println("teemo1.equals(teemo2) returns true");
        }else{
            System.out.println("teemo1.equals(teemo2) returns false");
        }
    }
}

teemo1과 teemo2 객체는 속성값이 동일하지만,

결과는 주석에 나온대로 모두 다르다고 나옵니다.


==는 참조자료형일 경우, 객체의 주소값을 비교하기 때문에 당연히 다르다고 나옵니다.

Object클래스에 equals()메소드는 두 객체의 해쉬코드값을 비교합니다.

해쉬코드 역시 각 객체별로 고유한 값이므로 teemo1과 teemo2는 다른 값을 가지며

즉, teemo1.equals(teemo2)는 false를 리턴합니다.


하지만 저는 같은 속성값(name, power, defense)를 가지면

equals()값으로 true를 리턴받고 싶습니다.

이때는 equals()과 hashCode()를 오버라이딩 받아주면 됩니다.

equals를 오버라이딩한 ChampionDTO3을 새로 만들어서 비교해보겠습니다.

package c.inheritance;

public class ChampionDTO3 {
    public String name;
    public int power;
    public int defense;

    public ChampionDTO3(String name, int power, int defense){
        this.name = name;
        this.power = power;
        this.defense = defense;
    }

    public boolean equals(Object obj){
        // 주소값이 같으면 true
        if(this == obj) return true;
        // 비교대상 객체가 null이면 false
        if(obj == null) return false;

        // getClass() 출력해봄...
        System.out.println("getClass(): " + getClass());

        // 두 객체의 클래스가 다르면 false
        if(getClass() != obj.getClass()) return false;

        // 같은 클래스이므로 캐스팅 가능
        ChampionDTO3 other = (ChampionDTO3)obj;

        // 각 인스턴스 변수 값 비교
        // name은 String(참조자료형)이므로
        // null check, equals로 비교
        if(name == null){
            if(other.name != null) return false;
        }else if(!name.equals(other.name)) return false;

        if(power != other.power) return false;

        if(defense != other.defense) return false;

        return true;
    }

    public int hashCode(){
        final int prime = 31;
        int result = 1;
        result = prime * result + ((name == null)? 0: name.hashCode());
        result = prime * result + power;
        result = prime * result + defense;

        return result;
    }
}

이제 비교하는 코드를 작성하겠습니다.

package c.inheritance;

public class Equals2 {
    public static void main(String[] ar){
        Equals2 ex =  new Equals2();
        ex.equalMethod1();
//        getClass(): class c.inheritance.ChampionDTO3
//        teemo1.equals(teemo2) returns true
    }

    public void equalMethod1(){
        ChampionDTO3 teemo1 = new ChampionDTO3("teemo", 95, 20);
        ChampionDTO3 teemo2 = new ChampionDTO3("teemo", 95, 20);

        if(teemo1.equals(teemo2)){
            System.out.println("teemo1.equals(teemo2) returns true");
        }else{
            System.out.println("teemo1.equals(teemo2) returns false");
        }
    }
}

오버라이딩한 equals() 메소드를 살펴보면 체크하는 순서는 다음과 같습니다.

  1. 두 객체가 주소값이 같은지
  2. 비교하는 객체가 null인지
  3. 클래스가 같은지
  4. 속성값이 모두 같은지

자세한 설명은 코드 안에 주석으로 남겨놨습니다.

그리고, Equals 클래스에서 비교하자 true로 나옵니다.


여기서 제가 공부하다가 생긴 궁금점 두가지는

  1. 비교는 equals()로만 하는데 hashCode()는 왜 오버라이딩하지?
  2. equals() 오버라이딩 코드에서 name은 참조자료형(String)이라서 equals를 썻는데, 그럼 이때도 this.name과 other.name의 해쉬코드 값이 다르므로 false가 리턴 되는거 아닌가?


먼저 1번의 답은 다음과 같습니다.

현재 속성값이 같은 객체인지 확인하기 위해 equals()메소드를 오버라이딩해서 수정했지만,

hashCode()의 값은 다르기 때문에 진정으로 같은 객체인 상태는 아닌겁니다.

그래서 진정으로 같은 객체로 만들어주기 위해서 hashCode()도 오버라이딩해서 수정해줍니다.


2번의 답은 다음과 같습니다.

자바에는 객체를 재사용하기 위해 Constant Pool이라는 것이 존재합니다.

예를 들어

String name1 = "teemo";
String name2 = "teemo";

이렇게 두 String변수의 값이 같다면,

String은 참조자료형이지만 예외적으로 Constant Pool을 사용하여

name1을 선언할 때 “teemo”라는 값이 Constant Pool에 저장되고,

name2를 선언할 때는 Constant Pool에 “teemo”라는 값이 있는지 확인하고

있으면 그걸 그대로 참조하는 겁니다.

그래서 equals를 쓰면 true값이 반환됩니다.

이에 대해서는 차후에 포스팅하겠습니다.


사실 equals()와 hashCode()를 오버라이딩해서 구현할 떄는

지켜야할 조건이 좀 있기 때문에 직접 구현하는 건 권장하지 않습니다.

조건에 대해서는 덧붙이는 말에 더 자세히 적어놓겠습니다.


아무튼 그래서 직접 작성하는 것보다는 개발툴에 있는 기능으로 하는걸 추천합니다.

하는 방법은 여기에 있습니다.




덧붙이는 말


equals()메소드를 오버라이딩할 때 반드시 지켜야할 다섯가지 조건

  • 재귀(reflexive): null이 아닌 x라는 객체의 x.equals(x)는 항상 true를 리턴해야한다.
  • 대칭(symmetric): null이 아닌 x, y객체에 대해서 x.equals(y) == y.equals(x)이여야 한다.
  • 타동적(transitive): null이 아닌 x, y, z객체에 대해서 x.equals(y)가 true이고 y.equals(z)가 true이면, x.equals(z)도 true여야 한다.
  • 일관(consistent): null이 아닌 x, y객체에 대하여 두 객체가 변동사항이 없으면 몇번을 호출해도 x.equals(y)의 결과는 같아야한다.
  • null과의 비교: null이 아닌 x객체에 대하여, x.equals(null)은 항상 false이여야 한다.

복잡해 보이지만 자세히 살펴보면 당연한 말들입니다.


hashCode()메소드를 오버라이딩할 때 반드시 지켜야할 세가지 조건

  • 자바 앱이 실행되는 동안 어떤 객체에 대해 이 메소드가 호출될 때는 항상 동일한 int값을 리턴해야한다. 하지만 자바 앱을 실행할 때마다 같은 값일 필요는 없다.
  • 어떤 두 객체 x, y에 대하여, x.equals(y)가 true이면 hashCode()값도 같아야한다.
  • 어떤 두 객체 x, y에 대하여, x.equals(y)가 false라고해서 hashCode()값이 무조건 달라야하는 건 아니다. 하지만 값이 다르면 hashtable 성능을 향상시키는데 도움이 된다.