예제 코드는 여기서 보실 수 있습니다.
0. 들어가며
보통 IDE 에서 재정의 기능을 사용하면 자동으로 @Override
어노테이션을 붙여줍니다.
그래서 저는 @Override
에 대해 깊게 고민해본적이 없는데요.
이번 아이템은 개발도구가 도와주지 않다고 가정하고 자세히 알아보겠습니다.
1. @Override 를 붙이지 않는다면
@Override
어노테이션의 가장 중요한 역할은 해당 메서드가 정확하게 오버라이드하고 있는지 확인해주는 것 입니다.
class Something {
private final int num;
public Something(int num) { ... }
// equals, hashCode 는 문제없다고 가정
public boolean equals(Something obj) { ... } // num 이 같으면 같은 객체
public int hashCode() { ... } // num 을 이용한 hashCode
}
public class SomethingSet {
public static void main(String[] args) {
Set<Something> set = new HashSet<>();
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
set.add(new Something(j));
}
}
System.out.println(set.size()); // 25 출력
}
}
위 예제는 컴파일 에러가 전혀 없는 코드입니다.
2중 for 문을 이용하여 set 에 5x5번 추가하니 총 25번 추가 작업을 합니다.
그렇지만 같은 번호는 0~4만 추가하기 때문에 출력 결과는 5를 예상할 수 있습니다.
하지만 이 코드를 실제로 실행하면 25가 나오게 됩니다.
1.1 예기치 않은 오버로딩
HashSet 은 equals 를 호출하여 입력된 객체가 기존 객체와 같은 객체인지 확인합니다.
class Something {
private final int num;
public Something(int num) { ... }
// equals, hashCode 는 문제없다고 가정
public boolean equals(Something obj) { ... } // num 이 같으면 같은 객체
public int hashCode() { ... } // num 을 이용한 hashCode
}
하지만 위 코드에서는 Object 클래스의 equals 와는 다르게 파라미터가 Something 타입입니다.
그래서 오버라이딩하려던 의도였지만 예기치 않게 오버로딩되어버린 것이죠.
결국 HashSet 에서는 Something 의 equals 는 사용하지 않고, Object 의 equals 를 사용합니다.
1.2 해시 충돌
Object 의 equals 는 인스턴스의 동일성을 비교하기 때문에 생성된 객체는 모두 서로 다릅니다.
class Something {
private final int num;
public Something(int num) { ... }
// equals, hashCode 는 문제없다고 가정
public boolean equals(Something obj) { ... } // num 이 같으면 같은 객체
public int hashCode() { ... } // num 을 이용한 hashCode
}
public class SomethingSet {
public static void main(String[] args) {
Set<Something> set = new HashSet<>();
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
set.add(new Something(j));
}
}
System.out.println(set.size()); // 25 출력
}
}
그래서 위 코드에서 set.add(new Something(j))
에서 문제가 발생합니다.
내부적으로 다른 객체라고 판명된 경우 set 에 입력하게 되는데요.
하지만 Something 클래스의 hashCode 는 제대로 오버라이딩되어 문제가 발생합니다.
그건 바로 서로 다른 개체가 같은 해시값을 가지게 되는 것, 즉 해시충돌입니다.
그래서 HashSet 은 해시충돌에 대한 대응 로직을 수행하게되고, 성능에도 악영향을 끼치게 됩니다.
위는 잘못 작성된 코드 실행 결과로 100만번 실행하여 약 11초가 걸렸지만, 심지어 원하는 결과 500 이 아닌 1,000,000이 나왔습니다.
위는 제대로 작성된 코드 실행 결과로 100만번 실행하여 약 0.5초 이하,원하는 결과 500을 얻었습니다.
하지만 이 모든 문제들은 @Override
만 잘 붙였다면 발생하지 않을 문제였기 때문에 어노테이션을 잘 붙이는것으로 간단하게 해결할 수 있습니다.
2. 붙이는 경우
붙이는 경우에도 신경쓸 부분이 있습니다.
그건 바로 @Override
가 생략 가능한점 때문인데요.
다음은 재정의시 어떤 클래스를 상속했느냐에 따라 달라지는 생략가능 여부 입니다.
- 클래스 상속 : 무조건 붙여야함(안붙이면 컴파일 에러)
- 추상 클래스 상속 : 생략 가능
- 인터페이스 상속 : 생략 가능
public class Child {
public static void main(String[] args) {
ChildClass childClass = new ChildClass();
childClass.test(); // ChildClass.Test() 출력
ChildAbstractClass childAbstractClass = new ChildAbstractClass();
childAbstractClass.test(); // ChildAbstractClass.Test() 출력
ChildInterface childInterface = new ChildInterface();
childInterface.test(); // ChildInterface.Test() 출력
}
}
class ChildClass extends ParentClass {
@Override // 없으면 컴파일 에러 발생
void test() {
System.out.println("ChildClass.Test()");
}
}
class ChildAbstractClass extends ParentAbstractClass {
// @Override // 어노테이션 생략 가능
void test() {
System.out.println("ChildAbstractClass.Test()");
}
}
class ChildInterface implements ParentInterface {
/// @Override // 어노테이션 생략 가능
public void test() {
System.out.println("ChildInterface.Test()");
}
}
하지만 어떤 클래스에는 붙이고, 어떤 클래스에는 붙이지 않는다면 코드에 일관성이 떨어지게 됩니다.
그렇게되면 다른사람이 해당 클래스를 분석할 때 새로 추가한 기능인지, 상속한 기능을 재정의한건지 확인하기 힘들 수 있습니다.
그래서 책에서는 일관되게 모두 붙이는 것을 권장하고 있습니다.