클래스에서 사용하는 타입을 타입 매개변수로 미리 정해둠으로써 런타임 오류를 방지하고 타입 안정성을 확보해주는 제네릭은 오늘날 자바의 컬렉션 프레임워크 사용에 없어서는 안될 중요한 특징입니다! 이에 관해 얘기하는 5장 제네릭을 살펴보겠습니다.
26. raw type은 사용하지 말자
- 타입 매개변수 T를 사용하는 클래스에서, T를 정의하지 않고 사용하는 것을 raw type을 사용한다고 말하는데(List list = new ArrayList<>()와 같은 용법), 런타임 오류를 방지하는 제네릭의 장점을 사용하지 못하는 코드
- 반드시 꺾쇄(혹은 다이아몬드) 안의 타입 매개변수를 명시하고 사용할 수 있도록 하자
- 제네릭 타입을 쓰고 싶지만, 실제 타입 매개변수를 고려하고 싶지 않을 때 <?>라는 와일드카드 타입 사용 가능 -> 해당 컬렉션에 원소 추가 불가능
- List<Object>는 모든 타입의 객체 저장 가능, List<?>는 와일드카드로 사용된 특정 객체 타입만 저장된 컬렉션
27. 비검사 경고를 제거하여 타입 안정성을 확보하자
- 제네릭을 사용하면서 발생할 수 있는 수많은 human error는 런타임 예외를 던지는데, 이는 컴파일러가 던지는 경고를 모두 고려하는 선에서 막을 수 있다
- 필요한 경고 제거를 위해 @SurpressWarning("unchecked") 어노테이션을 사용할 경우, 해당 경고의 내용과 무시해도 아전한 이유를 꼭 주석으로 남기자
28. 배열 < 리스트
- 배열은 공변이기 때문에, 하위타입의 배열이 상위타입 배열의 하위타입이 되며 런타임 오류를 발생시킬 가능성이 높아진다
- 리스트는 실체화되지 않기 때문에 컴파일 타임에만 리스트에 추가되는 원소의 타입을 확인하고, 런타임에는 확인하지 않는다
- 제네릭의 장점, 오류를 사전에 발견하고 타입 안정성을 얻는 것을 보장하기 위해 배열보단 리스트를 사용하자
29. 이왕이면 제네릭 타입
- Object 기반으로 선언된 필드는 제네릭으로 바꾸는 것이 훨씬 좋다
- 필드의 각 제네릭 타입 요소를 사용할 때 명시적인 형변환이 필요하며, 해당 과정마다 타입 안정성이 깨질 위험이 존재
- Object 필드가 존재하는 코드를 클라이언트에서 사용할 때 직접 형변환하게 하지 말고, 클래스 내부에서 제네릭으로 처리할 수 있도록 하자
30. 이왕이면 제네릭 메소드
- 제네릭 타입을 사용해야하는 이유와 마찬가지로, 제네릭 클래스 기능을 쓰는 클라이언트 코드가 아니라 클래스 안에서 제네릭으로 타입 형변환을 미리 시켜주는 것이 좋다 (메소드 앞에 <T>를 붙이는 등)
31. 한정적 와일드카드로 유연성을 높이자
- 한정적 와일드카드란, 사용하는 제네릭 타입의 상위/하위 타입을 명시함으로써 해당 역할을 수행하는 제네릭 타입의 공변성을 정의하여 제네릭에서도 다형적인 코드를 작성할 수 있다
- 예를 들어, <? extends T>라는 제네릭 타입은 T를 상속/구현하는 모든 타입을 허용한다는 의미 (반대로 <? super T>는 와일드 타입이 상위 타입임을 뜻함)
- PECS(Producer Extends, Consumer Super) 원칙에 따라, T가 와일드카드 타입의 생산자일 경우 extends를, 소비자일 경우 super를 사용하자 -> 사실 논리적으로 생각해보면 당연함
- 아무튼 제네릭을 사용하는 클라이언트 쪽에서 와일드카드 타입을 신경쓰지 않아도 되도록 하자
32. 제네릭과 가변인수를 같이 쓸 때 조심하자
- 가변인수 메소드란, method(T... args) 와 같이 메소드 인자가 고정되지 않은 메소드를 말하는데, 컴파일러가 args의 배열(varargs)을 만드는 코드를 자동으로 추가한다 -> 이때 타입은 Object[]
- 따라서 해당 varargs를 캐스팅할 때 타입 안정성이 보장되지 않기 때문에, @SafeVarArgs를 붙일 수 있도록 코드 단계에서 컴파일 안정성을 꼭 보장할 수 있도록 하자
- 사실 가변인수에 제네릭 안쓰는게 최선인듯
33. 타입 안전 이종 컨테이너
- 제네릭 타입을 사용하는 일반적인 클래스는 사용할 수 있는 제네릭 타입에 한계 존재
- 이 때, Class<T>를 키로, T 타입 인스턴스를 값으로 같는 map 형식의 "타입 안전 이종 컨테이너"를 사용하여 여러가지 제네릭 타입을 사용할 수 있음
- Map<Class<?>, Object>로 map 필드를 선언하고, 값을 저장하고 꺼낼 때 Class의 cast()메소드를 사용해서 타입 안정성을 보장하는 방법을 사용 가능
독후감
제네릭에 대해 물었을 때. 단순히 컬렉션의 "<>"안에 들어가는 여러가지 타입이라고 밖에 답하지 못했을 때가 있었으나 이젠 아닙니다! 어떤 클래스에서 사용하는 비정형 타입이 있을 때, 이를 매개변수화하여 객체 생성 시점에 고정하여 사용함으로써 런타임에 타입 안정성을 확보시켜주는 어떠한 매커니즘, 혹은 기능이라고 설명할 수 있겠습니다.
여러 타입에 대해 동작해야하는 제네릭 클래스를 작성할 때, 이를 사용하는 클라이언트 쪽에서 타입 캐스팅을 최대한 신경쓰지 않아도 되도록 설계하는 것이 중요한 것 같습니다. 제네릭이 없었을 땐 Object 타입으로 선언한 배열에 값을 넣고 뺄 때마다 타입 캐스팅이 일어났고, 이 때 타입 안정성이 보장되지 않았기 때문에 CastException이 자주 발생했으며 런타임 이외에 이것을 확인할 방법이 없었다고 합니다. 제네릭을 사용하기 시작하면서 컴파일 타임에 이런 오류를 발견하고, 런타임엔 타입 정보를 소거시키게 되었습니다.
또 "?"를 사용하는 와일드카드 타입에 대해 조금 더 자세히 알게된 것 같습니다. 와일드카드 타입을 사용함으로써, 제네릭의 타입 파라미터에도 클래스의 상속 관계를 적용하여 유연한 코드를 달성할 수 있게 된 것입니다!
사실 지금까지 어플리케이션 코드를 작성해왔을때 타입 계층에 따른 다형성을 이용한 적은 있어도 제네릭 자체의 이점을 이용한 적은 많이 없습니다. 제네릭에 관해 가장 크게 의식했을 땐 .. 제네릭을 사용하는 가장 큰 이유인 컬렉션 프레임워크를 사용했을 때 뿐.. (DB에서 값을 꺼내왔을 때 List 자료구조에 담아왔다)
아!! 배열보단 제네릭 타입의 List가 선호되는 이유로 제네릭 클래스끼리는 파라미터 타입에 대한 계층관계가 적용되지 않기 때문에 조금 더 타입 안정적이라고 볼 수 있고.. 컬렉션 자체에서 제공하는 기능이 조금 더 많고 편리하므로 웬만하면 제네릭을 사용해야겠다고 생각하게 되었습니다.