DEV ℧ Developer Diary

[EffectiveJava] item40 - @Override 애너테이션을 일관되게 사용하라

@Override는 메서드 선언에만 달 수 있으며, 이 애너테이션이 달렸다는 것은 상위 타입의 메서드를 재정의했음을 뜻한다.

이 애너테이션을 일관되게 사용하면 여러가지 악명 높은 버그들을 예방해 준다.

다음 예제를 살펴보자. 다음 예제에는 버그 두가지가 있다. 무엇일까?

public class Bigram {
    private final char first;
    private final char second;

    public Bigram(char first, char second) {
        this.first = first;
        this.second = second;
    }

    public boolean equals(Bigram b) {
        return b.first == this.first && b.second == this.second;
    }
    public int hashCode() {
        return 31 * this.first + this.second;
    }

    public static void main(String[] args) {
        Set<Bigram> s = new HashSet<>();
        for (int i = 0; i < 10; i++)
            for (char ch = 'a'; ch <= 'z'; ch++)
                s.add(new Bigram(ch, ch));
        System.out.println(s.size());
    }
}

@Override

위의 예제는 main메서드를 보면 똑같은 소문자 2개로 구성된 바이그램 26개를 10번 반복한다음, Set에 넣고 집합의 크기를 출력한다.
Set은 중복을 허용하지 않으므로 26이 출력되어야 하지만 실제 결과를 보자.

260

260이 출력된다. 작성한 코드가 무엇인가 잘못된 것이다. 같은 객체임을 비교하기 위해 equalshashCode를 구현했지만 무엇이 문제였을까?

답은 equals재정의(Overriding) 한것이 아니라 다중정의(overloading) 해버린 것 이다.

Object의 equals를 재정의 하려면 매개변수는 Object이지만, 위의 코드엔 Bigram을 매개변수로 가지고 있다. 그래서 Object에서 상속한 equals와는 다른 equals 메서드를 정의한 것이 되어, 객체의 비교가 되지 않은 것이다.

Object의 equals는 == 연산자와 똑같이 객체 식별성(identity)만을 확인한다. 따라서 같은 소문자를 소유한 바이그램 10개를 각각이 서로 다른 객체로 인식되고, 결국 260을 출력한 것이다.

이 버그를 고치기 위해서는 @Override를 달아주면 된다.

@Override
public boolean equals(Bigram b) {
    return b.first == this.first && b.second == this.second;
}

하지만 재정의한 메서드와 매개변수가 동일하지 않으므로 컴파일 오류가 발생한다.

java: method does not override or implement a method from a supertype
@Override public boolean equals(Bigram b)
^

잘못된 부분을 명확히 알려주므로 바로 수정이 가능하다.

@Override
public boolean equals(Object o) {
    if (!(o instanceof Bigram))
        return false;
    Bigram b = (Bigram) o;
    return b.first == first && b.second == second;
}

또한 hashCode 메서드에도 동일한 @Override를 달아준다면 결과는 아까와 다르게 출력될 것이다.

26

그러므로 상위 클래스의 메서드를 재정의하려는 모든 메서드에 @Override 애너테이션을 달자.
예외는 한 가지 뿐이다. 구체 클래스에서 상위 클래스의 추상 메서드를 재정의할 때는 굳이 @Override를 달지 않아도 된다. 구체 클래인데 아직 구현하지 않은 추상메서드가 남아있다면, 컴파일러가 그 사실을 바로 알려주기 때문이다.

보통 IDE는 @Override를 일관되게 사용하도록 부추기기도 한다. @Override가 달려있지 않은 메서드가 실제로는 재정의를 했다면 경고를 준다. @Override를 일관되게 사용한다면 이처럼 실수로 재정의 했을 때 경고해줄 것이다.
재정의할 의도였으나 실수로 새로운 메서드를 추가했을 때 알려주는 컴파일 오류의 보완재 역할로 보면 되겠다.

@Override는 클래스뿐 아니라 인터페이스의 메서드를 재정의할 때도 사용할 수 있다.

default 메서드를 지원하기 시작하면서 인터페이스를 구현한 메서드에도 @Override를 다는 습관을 들이면 시그니처가 올바른지 재차 확신 할 수 있다.

또한 추상 클래스나 인터페이스에서는 상위 클래스나 상위 인터페이스의 메서드를 재정의하는 모든 메서드에 @Override를 다는 것이 좋다.