타입으로 견고하게 다형성으로 유연하게 ‐ 두 다형성의 만남 - thought-corner/backend-roadmap GitHub Wiki

타입으로 견고하게 다형성으로 유연하게 - 두 다형성의 만남

묻는 것
제네릭 클래스와 상속 물려받을 때 타입 인자를 전달할 것인가 고정할 것인가
타입 매개변수 제한 T에 대해 최소한 무엇을 알아야 하는가
가변성 Integer <: Number일 때 Cell<Integer>Cell<Number>는 무슨 관계인가

제네릭 클래스와 상속

class Chest<T> extends Box<T>        // 전달 - 하위 타입도 제네릭으로 남는다
class StringBox extends Box<String>  // 고정 - 타입 파라미터가 사라진다
  • 타입 파라미터를 그대로 전달하거나, 특정 타입으로 고정하거나. 그리고 고정하는 쪽에서는 컴파일러가 몰래 메서드를 하나 만든다.

타입 매개변수 제한

  • 바운드는 능력을 주는 장치가 아니라 요구를 적는 장치다.
class NumberBox<T extends Number>     // T가 Number라는 것만 안다
  • <T>만 적었다면 원소로 할 수 있는 일이 없다. 아무것도 모르는 타입에는 부를 메서드가 없으니까, T extends Number라고 적는 순간 doubleValue()를 부를 수 있게 된다.
  • 그래도 TInteger인지 Double인지는 여전히 모른다.

타입 매개변수 제한 3가지 형태

형태 쓰는 이유
상한 <T extends Number> T의 메서드를 부르려면 필요하다
다중 바운드 <T extends Number & Comparable<T>> 두 능력이 동시에 필요할 때
재귀 바운드 <SELF extends Request<SELF>> 상속해도 구체 타입을 유지하려고
  • Java에 union 타입은 없지만 intersection 타입은 바운드 위치에서 쓸 수 있다.
  • 제약을 늘리는 만큼 받아들일 수 있는 타입이 줄어든다.

재귀 바운드 — 상속과 체이닝의 화해

  • 상속과 메서드 체이닝은 사이가 나쁘다.
  • 상위 클래스의 path()Request를 돌려주면 체이닝이 거기서 상위 타입으로 주저앉고, 그 뒤에 하위 타입 메서드를 못 부른다.
new JsonRequest().path("/users").body("{}")
//                              ^^^^ path()가 Request를 돌려줬다면 여기서 끊긴다
  • 그래서 "내 하위 타입이 무엇인지"를 타입 파라미터로 받는다.
abstract class Request<SELF extends Request<SELF>>
  • SELF의 바운드 안에 자기 자신이 다시 나오기 때문에 재귀적이다.
  • 대가가 있다. self()를 하위 타입이 구현해야 하고, 컴파일러는 SELF가 정말 this의 타입인지 검증하지 못한다.

Java에 하한 바운드는 없다

class Bound<T super Integer> { }   // error: > expected
  • super는 와일드카드 위치에만 있다.

가변성

  • Integer <: Number일 때 Cell<Integer>Cell<Number>는 무슨 관계인가. 답은 타입 파라미터가 어디에 나타나는가로 갈린다.
타입 T가 나타나는 곳 결론
Source<T> 반환 위치에만 공변? extends
Sink<T> 파라미터 위치에만 반공변? super
Cell<T> 양쪽 모두 불변
  • 생산자는 위로 넓히고, 소비자는 아래로 넓힌다.

캡처 — 물음표에 이름을 붙이는 과정

  • List<?>는 "원소 타입이 무엇인지는 모르지만 하나로 정해져 있다"는 뜻이다.
  • 문제는 ?가 나올 때마다 검사기가 같은 타입이라고 보장하지 못한다는 것이다.
static void swap(List<?> list, int i, int j) {
    list.set(i, list.get(j));      // error: Object cannot be converted to CAP#1
}
⚠️ **GitHub.com Fallback** ⚠️