문제 상황을 이해하기 위해서 다음과 같은 문제를 풀어 보자
interface Fruite {
Integer getSize();
}
// apple.java
class Apple implements Fruit, Comparable<Apple> {
private final Integer size;
public Apple(Integer size) {
this.size = size;
}
@Override public Integer getSize() {
return size;
}
@Override public int compareTo(Apple other) {
return size.compareTo(other.size);
}
}
// orange.java
class Orange implements Fruit, Comparable<Orange> {
private final Integer size;
public Orange(Integer size) {
this.size = size;
}
@Override public Integer getSize() {
return size;
}
@Override public int compareTo(Orange other) {
return size.compareTo(other.size);
}
}
//main.java
class Main {
public static void main(String[] args) {
Apple apple1 = new Apple(3);
Apple apple2 = new Apple(4);
apple1.compareTo(apple2);
Orange orange1 = new Orange(3);
Orange orange2 = new Orange(4);
orange1.compareTo(orange2);
apple1.compareTo(orange1); // Error: different types
}
}
일단 우선 위의 예제에서는 compareTo라는 함수가 중복된다. 이렇게 되면 Fruit를 상속받는 모든 클래스가 이 compareTo를 Implement하는 문제가 생긴다. 이걸 수정하기 위해서 다음과 같이 해보자
- Common 클래스로 중복 옮기기
class Fruit implements Comparable<Fruit> { private final Integer size; public Fruit(Integer size) { this.size = size; } public Integer getSize() { return size; } @Override public int compareTo(Fruit other) { return size.compareTo(other.getSize()); } }
class Apple extends Fruit {
public Apple(Integer size) {
super(size);
}
}
class Orange extends Fruit {
public Orange(Integer size) {
super(size);
}
}
CompareTo를 Common class로 추출해서 위와 같이 작성하면 쉽게 구현체를 만들 수 있다. 이 코드가 자바라서 class를 사용했지만 java8에서는 default Method가 지원되서 interface를 이용해도 가능하다.
이 때 문제점은 두개의 서로 다른 타입을 비교 했을 때 에러가나지 않는다는 점이다. 우리는 서로 다른 타입을 비교하면 에러가 나기를 원한다.
apple1.compareTo(orange1); // No error
이 때 사용되는 것이 바로 타입 파라미터이다.
class Fruit<T> implements Comparable<T> {
private final Integer size;
public Fruit(Integer size) {
this.size = size;
}
public Integer getSize() {
return size;
}
@Override public int compareTo(T other) {
return size.compareTo(other.getSize()); // Error: getSize() not available.
}
}
그런데 Fruit에 타입 파라미터를 추가하니 T의 메서드인 getSize()는 컴파일러가 알 수 없다. 왜냐면 우리의 클래스 Fruit클래스의 타입파라미터 T는 어떠한 Bound도 없기 때문이다. 그래서 T는 어떠한 클래스도 될 수 있으며, 모든 클래스가 getSize()를 가질 수도 없다.
이때 도입되는 것이 Recursive Type Bound이다.
class Fruit<T extends Fruit<T>> implements Comparable<T> {
private final Integer size;
public Fruit(Integer size) {
this.size = size;
}
public Integer getSize() {
return size;
}
@Override public int compareTo(T other) {
return size.compareTo(other.getSize()); // Now getSize() is available.
}
}
class Apple extends Fruit<Apple> {
public Apple(Integer size) {
super(size);
}
}
class Orange extends Fruit<Orange> {
public Orange(Integer size) {
super(size);
}
}
우선 컴파일러에게 이 T가 Fruit를 상속 받은 T라는 것을 말해주자. 다른 말로 Upper bound가 “T extends Fruit<T>“라는 뜻이 된다.
이것은 오로지 Fruit의 SubType만이 Type Argument로 사용될 수 있음을 뜻한다.
모든 문제가 해결 되었을까?
아니다. 이 패턴에는 함정이 하나 있다. 컴파일러는 다른 서브타입에 대한 Type argument를 막지 못한다. 예를 들어보자.
class Orange extends Fruit<Orange> {}
class Apple extends Fruit<Orange> {} // no compile error
이 문제를 스칼라에서는 self:E라는 것을 통해서 풀어본다고 하는데 어떻게 하는지 나중에 살펴보자