java

자바 - Object 클래스

Feb 19, 2020

Object: 보편적 슈퍼클래스

목차

1. Object 클래스
2. toString 메서드
3. equals 메서드
4. hashCode 메서드
5. 객체 복제하기


1. Object 클래스

자바에서 모든 클래스는 Object 클래스를 직/간접적으로 확장한다. 클래스에 명시적인 슈퍼클래스가 없으면 암시적으로 Object를 확장한다.

Object 클래스는 모든 자바 객체에 적용할 수 있는 메서드를 정의한다.

2. toString 메서드

getClass().getName()을 호출하면 서브클래스에서도 toString 메서드가 잘 동작한다.

public class Employee {
    ...

    public String toString() {
        return getClass().getName() + "[name=" + name
            + ",salary=" + salary + "]";
    }
}

서브 클래스에서는 super.toString()을 호출하고, 해당 서브 클래스의 인스턴스 변수를 별도의 대괄호에 작성했다.

public class Manager extends Employee {
    ...

    public String toString() {
        return super.toString() + "[bonus=" + bonus + "]";
    }
}

객체를 문자열과 연결하면 자바 컴파일러가 해당 객체의 toString 메서드를 자동을 호출한다.

public class Main {
    public static void main(String[] args) {
        Manager manager = new Manager("joyseo", 20);
        manager.setBonus(1.1);

        // 같다.
        System.out.println("print : " + manager);
        System.out.println("print : " + manager.toString());
    }
}

Object 클래스에 정의된 toString 메서드는 클래스명과 해시 코드를 출력한다.

System.out.println(System.out);

// java.io.PrintStream@610455d6
// PrintStream 클래스의 구현자가 toString 메서드를 오버라이드 하지 않았기 때문이다.



3. equals 메서드

equals 메서드는 한 객체를 다른 객체와 같은지 검사한다.

public class Item {
    private String description;
    private double price;

    public Item(String description, double price) {
        this.description = description;
        this.price = price;
    }

    public boolean equals(Object otherObject) {
        // 두 객체가 동일한지 알아보는 빠른 검사
        if (this == otherObject) return true;

        // otherObject 파라미터가 null이면 false를 반환해야 한다.
        if (otherObject == null) return false;

        // otherObject가 Item의 인스턴스인지 검사한다.
        if (getClass() != otherObject.getClass()) return false;

        // 인스턴스 변수들의 값이 같은지 검사한다.
        Item other = (Item) otherObject;
        return Objects.equals(description, other.description)
            && price == other.price;
    }
}

서브 클래스용 equals 메서드를 정의할 때는 먼저 슈퍼클래스의 equals를 호출한다.

public class DiscountedItem extends Item {
    private double discount;

    public DiscountedItem(String description, double price, double discount) {
        super(description, price);
        this.discount = discount;
    }

    public boolean equals(Object otherObject) {
        if (!super.equals(otherObject)) return false;
        DiscountedItem other = (DiscountedItem) otherObject;
        return discount == other.discount;
    }
}



4. hashCode 메서드

해시코드는 객체에서 파생되는 정숫값이다. 해시 코드는 값이 중복될 수 있다. 즉 x와 y가 서로 다른 객체면 x.hashCode와 y.hashCode의 값이 중복될 수 있다. 하지만 그 빈도는 낮아야 한다.

String 클래스는 다음과 같은 알고리즘으로 해시코드를 생성한다.

int hash = 0;
for(int i = 0; i < lenght(); i++) {
    hash = 31 * bash + charAt(i);
}

hashCode와 equals 메서드는 반드시 호환되어야 한다. x.equals(y)면 x.hashcode == y.hashcode여야 한다.

Object.hashCode 메서드는 구현체 나름의 방식으로 해시 코드를 만들어낸다. 구현체에서는 객체의 메모리 주소나 객체와 함께 캐시되는 숫자(순차적 또는 무작위), 또는 이 둘의 조합으로 해시코드를 만들 수 있다. Object.equals는 객체가 같은지 검사하므로 같은 객체들의 해시 코드를 같게 만들기만 하면 된다.

hashCode 메서드를 재정의하려면 간단하게 인스턴스 변수의 해시 코드를 결합하면 된다.

public class Item {
    private String description;
    private double price;

    ...

    ...

    public int hashCode() {
        return Objects.hash(description, price);
    }
}



5. 객체 복제하기

clone 메서드를 오버라이드 하는 일은 복잡할 뿐만 아니라 필요한 경우도 드물다. 마땅한 이유가 없다면 clone을 오버라이드 하지 말아야 한다.

clone 메서드의 목적은 객체의 복제본을 만드는 것이다. 두 객체 중 하나의 상태를 변경해도 나머지 하나는 변하지 않는다.

Employee fred = new Employee("fred", 3000);
Employee cloneOfFred = fred.clone();
cloneOfFred.raiseSalary(10); // fred는 변하지 않는다.

clone 메서드는 Object 클래스에 protected로 선언되어 있어서 clone 메서드를 사용하려면 반드시 clone 메서드를 오버라이드 해야 한다.

Object.clone 메서드는 얕은 복사를 수행한다. 원본 객체에 있는 모든 인스턴스 변수를 단순히 복사하는 것이다. 인스턴스 변수가 기본 변수나 불변 객체가 아닌 경우 문제가 발생할 수 있다.

public class Message {
    private String sender;
    private ArrayList<String> recipients;
    private String text;
    
    
    
    public Message(String sender, String text) {
        this.sender = sender;
        this.text = text;
        recipients = new ArrayList<>();
    }

    public void addRecipient(String recipient) { 
        recipients.add(recipient);
    };

    ...
}

Message 객체의 얕은 복사본을 만들면 원본과 복사본이 recipients 리스트를 공유한다. 이럴 경우에는 clone 메서드를 오버라이드해서 깊은 복사를 수행해야 한다.

일반적으로 클래스를 구현할 때 다음 상황을 판단해야 한다.

  1. clone 메서드를 구현하지 않아도 되는가?
  2. 구현해야 한다면 상속받은 clone 메서드를 사용해도 괜찮은가?
  3. 그렇지 않으면 clone 메서드에서 깊은 복사를 수행해야 하는가?

1번의 경우에는 아무 일도 안 하면 되고, 2번의 경우에는 반드시 Cloneable 인터페이스를 구현해야 한다. 이 인터페이스는 아무 메서드가 없는 인터페이스로, 태킹 또는 마커 인터페이스라고 한다. Object.clone 메서드는 얕은 복사를 수행하기에 앞서 클래스가 Clonealbe 인터페이스를 구현했는지 검사하고, 구현하지 않았으면 CloneNotSupportedException을 반환한다. 아울러 clone의 유효 범위를 protected에서 public으로 올리고 반환 타입을 변경한다.

public class Employee implements Cloneable {

    ...

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

3번의 깊은 복사를 수행하는 경우 Object.clone 메서드를 사용할 필요가 없다.

public Message clone() {
    Message cloned = new Message(sender, text);
    cloned.recipients = new ArraysList<>(recipients);
    return cloned;
}

ArrayList.clone 메서드는 반환 타입이 Object다. 따라서 타입 변환 연산자를 사용해야 한다.

// 경고
cloned.recipients = (ArraysList<String>) recipients.clone()

타입 변환 연산자는 실행 시간에 완전히 검사받지 못하므로 경고를 받는다. @SuppressWarnings 애너테이션으로 경고를 누를 수 있지만 @SuppressWarnings는 선언에만 붙일 수 있다.

public Message clone() {
    try {
        Message cloned = (Message) super.clone();
        @SuppressWarnings("unchecked") ArrayList<String> clonedRecipients
            = (ArrayList<String>) recipients.clone();
        cloned.recipients = clonedRecipients;
        return cloned;
    } catch (CloneNotSupportedException ex) {
        return null;
    }
}

Message 클래스가 final이고 Clonable이며, ArrayList.clone은 CloneNotSupportedException이 일어날 수 없다.