객체의 실제 클래스를 사용할 상황은, 오직 생성자로 생성할 때 뿐이다.
따라서 적합한 인터페이스만 있다면 매개변수 뿐 아니라 반환값, 변수, 필드 전부 인터페이스 타입으로 선언해야 한다. 인터페이스를 타입으로 사용하는 프로그램은 훨씬 유연해지기 때문이다.
Set<Son> sonSet = new LinkedHashSet<>(); // 인터페이스를 타입으로 사용
LinkedHashSet<Son> sonSet = new LinkedHashSet<>(); // 클래스를 타입으로 사용
만약 나중에 구현 클래스를 교체하고자 한다면, 새 클래스의 생성자를 호출해주기만 하면 된다. 다른 코드는 전혀 손대지 않고 새로운 클래스로 교체가 가능해지는 것이다!
Set<Son> sonSet = new HashSet<>();
하지만 LinkedHashSet
과 달리 HashSet
은 반복자의 순회 순서를 보장하지 않는 특징을 가지고 있다.
이처럼 원래 클래스가 인터페이스의 일반 규약 의외의 특별한 기능을 제공하며, 주변 코드가 이 기능에 기대어 동작한다면 새로운 클래스도 같은 기능을 제공해야 한다는 것을 주의하자.
String
, BigInteger
와 같은 값 클래스들은 적합한 인터페이스가 없다. (주로 final로 확장을 막아놓은 경우가 많다)
이런 값 클래스는 매개변수, 변수, 필드, 반환 타입으로 사용해도 괜찮다.
클래스 기반으로 작성된 프레임워크가 제공하는 객체들은, 적합한 인터페이스가 없다.
OutputStream
등 java.io
패키지의 여러 클래스가 이 부류에 속한다.
PriorityQueue
클래스는 Queue
인터페이스에 없는 comparator
메서드를 제공한다. 클래스 타입을 직접 사용하는 경우는 이런 추가 메서드를 꼭 사용해야 하는 경우로 최소화해야 한다.
📚 핵심 정리
적합한 인터페이스가 있다면 타입으로 사용하고, 적합한 인터페이스가 없다면 클래스의 계층구조 중 필요한 기능을 만족하는 가장 덜 구체적인(상위의)클래스 타입으로 사용하자.