적합한 인터페이스가 있다면 매개변수뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하자. 실제 클래스를 사용해야 할 상황은 '오직' 생성자로 생성할 때 뿐이다. 선언 타입에 대해서는 클래스를 생각하기 전에 '인터페이스'부터 생각하는 습관을 길러야 겠다.
ArrayList<String> list = new ArrayList<String>();
생성자를 생성할 때는 클래스를 사용했고, 타입도 클래스를 사용했다. 이건 좋지 못한 코드이다.
아래와 같이 ArrayList의 인터페이스인 List를 사용하는 것이 좋다.
List<String> list = new ArrayList<String>();
인터페이스는 유연성을 더한다.
인터페이스를 도입했다는 것은 인터페이스를 사용하는 클래스가 구현부를 알지 못한다는 것이다. 즉, 클래스의 구현체 코드를 알지 못한다는 것이다. 클래스가 구현체 코드를 알지 못한다는 건 안좋은거 아닌가? 완전 바보 클래스 아니야? 라고 생각할 수 있지만, 반대로 생각하면 바보에게도 먹히는게 인터페이스인 것이다. 같은 이름의 과자 포장지 안에 내용물을 다르게 넣어도 되는것이다.
만약 위 예제에서 ArrayList 대신 LinkedList로 구현체를 교체하고싶다면, 생성자 부분만 교체하면 된다. 어차피 List<String> 타입 변수는 애초에 구현체 클래스를 알지 못했으니 이러한 변화에 영향을 받지 않게된다.
List<String> list = new LinkedList<String>();
주의할 점
원래의 클래스가 인터페이스의 일반 규약 이외의 특별한 기능을 제공하여, 주변 코드가 이 기능에 기대어 동작한다면 새로운 클래스도 반드시 같은 기능을 제공해야한다.
예를들어 LinkedHashSet이 따르는 순서 정책을 가정하고 동작하는 상황에서 HashSet으로 바꾸면 문제가 될 수 있다. HashSet은 순회 순서를 보장하지 않기 때문이다.
구현타입을 바꾸려는 동기
기존 사용하던 것보다 성능이 좋거나 좋은 기능을 제공할 수 있기 때문이다. 예를들어 HashMap을 참조하던 변수가 EnumMap으로 바꾸면 속도가 빨라지고 순회 순서도 키의 순서와 같아진다. 단, EnumMap은 키가 열거 타입일 때만 사용 할 수 있다.
또한 LinkedHashMap으로 바꾼다면 성능은 비슷하게 유지하면서 순회 순서를 보장하도록 설정할 수 있다.
선언 타입과 구현 타입을 동시에 바꾸면 안되나
프로그램이 컴파일 되지 않을 수도 있다. 클라이언트에서 기존 타입에서만 제공하는 메서드를 사용했거나, 기존 타입을 사용해야 하는 다른 메서드에 그 인스턴스를 넘겼다면, 타입이 바뀌는 순간 컴파일 에러가 발생할 것이다.
적합한 인터페이스가 없다면?
적합한 인터페이스가 없다면 당연히 클래스로 참조해야 한다. String이나 Integer 같은 클래스가 이에 해당한다.
인터페이스에는 없는 특별한 기능을 제공하는 클래스를 사용하는 경우도 있다. 예를들어 PriorityQueue 클래스는 Queue 인터페이스에는 없는 comparator 메서드를 제공한다. 클래스 타입을 직접 사용하는 경우는 이런 추가 메서드를 꼭 사용해야 하는 경우로 최소화해야 한다.
정리하면
타입으로 클래스 타입을 사용하는 것보다 확장성을 고려하여 인터페이스나 가장 덜 구체적인 상위타입의 클래스 타입을 사용하는 것이 좋다. 클래스를 직접 사용하는 경우는 최소화하자.