[Effective Java] 제네릭(Generic) 활용하기 (1)
오늘은 제네릭에 대해 정리해두려 한다.
제네릭 용어 정리
한글 용어 | 영문 용어 | 예 |
매개변수화 타입 | parameterized type | List<String> |
실제 타입 매개변수 | actual type parameter | String |
제네릭 타입 | generic type | List<E> |
정규 타입 매개변수 | formal type parameter | E |
비한정적 와일드카드 타입 | unbounded wildcard type | List<?> |
로 타입 | raw type | List |
한정적 타입 매개변수 | bounded type parameter | <E extends Number> |
재귀적 타입 한정 | recursive type bound | <T extends Comparable<T>> |
한정적 와일드카드 타입 | bounded wildcard type | List<? extends Number> |
제네릭 메서드 | generic method | static <E> List<E> asList(E[] a) |
타입 토큰 | type token | String.class |
로 타입 사용 금지
아주 레거시 한 자바 프로젝트의 코드를 보면 컬렉션 객체가 로 타입으로 선언 된 것이 꽤 보인다. 이전의 버전처럼 요즘 버전의 자바도 역시나 로 타입을 지원하는데 절대로 사용해서는 안된다.
아래 예시를 보자.
List rawTypeList = new ArrayList<>();
rawTypeList.add("문자열");
rawTypeList.add(100);
만약에 이런 코드가 있다면.. 어이없게도 컴파일 가능하다. 왜냐면 List 자체가 로타입으로 별도의 타입에 대한 내용이 정의되지 않았기 때문이다. 하지만 이러한 코드가 들어가면 몇번째에 어떤 타입의 데이터가 들어갔는지 알 수 있는 방법이 없기에 추후에 런타임에서 에러가 발생할 가능성이 있다.
Effective java에서는 조회시엔 비한정적 와일드카드 타입을 사용하는 것을 권장한다.
하지만 비한정적 와일드 카드 타입을 사용할때 add 는 null을 제외하고는 불가능한 것을 알아두자.
private int addUnboundedWildcardTypeList(List<?> uwtList) {
uwtList.add(100); // 에러발생
return 1;
}
자바 코드를 작성할때에는 명확하게 타입추론이 가능하도록 작성하는 것이 중요하다. 만약 그렇게 하지 않는다면 런타임 시에 예외를 피할 수가 없다. 만약에 아래 코드처럼 작성했다고 가정하자.
static List twoListMerge(List l1, List l2) {
List temp = new ArrayList();
temp.addAll(l1);
temp.addAll(l2);
return temp;
}
위 코드와 같이 짜면 타입을 정확하게 명시하지 않아 런타임 단계에서 오류가 발생할 가능성이 아주아주 많다. 굉장히 조심해야할 코드이다.(요즘에는 이런식으로 짜는 자바개발자는 없지만, 여러분도 실무의 레거시 코드를 다루다 보면 한번씩 볼 가능성이 있다..)
로 타입이 아니라 List<Object> 를 사용해주면 명확하게 Object라는 타입을 제시한 것이 된다. List 와 같은 표현이라 오해할 수 있지만 전혀 그렇지 않다. List<Object>는 문제가 될경우 차라리 컴파일 단계에서 에러를 내어 버리기에 오히려 디버깅이 편하다.
드물게 로 타입을 써야할 때가 있다.
먼저 리터럴 에는 로 타입을 써줘야한다.
List.class
그리고 또 타입 체크할때도 로타입을 쓴다.
if(s1 instanceof Set) {
Set<?> s2 = (Set<?>) s1;
for(Object o : s2) {
if(o instancof String) {
//...
}
}
}
다만 한가지 주의할 점이 런타임 시에 제네릭 타입 정보가 지워져서 로타입으로 들어가기 때문에
위와 같이 캐스팅 하고 , 각각의 오브젝트들을 가져와서 체크를 해줘야한다.
그렇다면 로 타입을 쓰지않고 어떻게 쓰는게 좋을까? 제네릭 클래스와 제네릭 인터페이스를 쓰는 것이다. 어려운 말이 아니다. 여러분들이 지금 코드에 쓰던대로 쓰면 되는 것이다.
List<String> stringList = new ArrayList<>();
List<Member> memberList = new ArrayList<>();
List<Address> addressList = new ArrayList<>();
여러분들은 이렇게 당연스럽게 제네릭을 사용하고 있었다. 실제로 List 인터페이스와 ArrayList 클래스를 뜯어보면 제네릭이 사용된것을 볼 수 있다.
public interface List<E> extends Collection<E> {
...
}
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
...
}
다른 컬렉션 클래스나 인터페이스를 뜯어봐도 마찬가지로 위와 같은 형태로 구현되어 있다.
Complier가 보내는 warning 제거하기
제네릭을 이용하다 보면 수많은 경고에 노출된다. 아래 코드를 컴파일 시켜보자.
Set<String> set = new HashSet();
이펙티브 자바에서는 이렇게 경고가 출력되는걸 다 제거하는 것을 추천한다. (경고가 없다는 말 자체가 안정성이 검증되었다는 이야기가 되니깐 ...)
만약에 코드가 확실하게 안전하다고 확신한다면 @SuppressWarnings("unchecker") 어노테이션을 사용해 경고를 숨길 수 있다.
절대로 경고를 무시해서는 안된다. 경고를 그대로 두고가면 우리는 잠재적인 에러를 두고 가는 것이라고 할 수 있다. 요즘에 다양한 프로젝트들은 내가 짜는 코드도 많지만 다양한 라이브러리의 의존성을 추가하여 작업하는 경우가 많다. 이러한 경우 수많은 경고가 노출되고 컴파일은 되는 경우가 종종 있다. 또한 문제가 없던 코드도 라이브러리가 업데이트 되면서 문제가 생길 수도 있다. 이러한 경고를 무시하고 간다면 내가 짠 코드도 아니기에 미리 미리 처리를 해놓지 않으면 추후에 문제가 생겼을때 디버깅을 하기 여간 쉬운게 아니다.
배열보다는 리스트를 사용하기
책에서는 또 배열보다는 리스트를 사용하라는 말을 한다.
배열은 모두가 아시는 것처럼 공변이다. 즉 , 같이 변한다는 뜻이다.
아래 코드를 보자.
Object[] arr = new Long[1];
arr[0] = "string";
위의 코드의 문제점은 컴파일 에러가 아닌 런타임에러가 난다는 것이다. 이러한 코드가 여러분의 프로젝트 내에 숨어있다고 생각해보라. 얼마나 끔찍한가...
만약에 위의 코드를 리스트로 구현하면...
List<Object> list = new ArrayList<Long>(); //compile error
컴파일 단계에서 이미 에러를 내어버린다. 물론 배열이 리스트에 비해서는 성능적인 측면에서는 좋지만, 왠만한 프로그램에서는 리스트를 쓴다고 성능에 치명적인 문제를 일으키지 않으니 그냥 안전하게 리스트를 쓰는것을 추천한다.
Type parameter Naming Conventions
여러분들이 실제로 제네릭을 이용하여 인터페이스나 클래스를 만들고자한다면 아래의 컨벤션을 지켜서 작업을 해주면 좋다.
그렇다고 무조건 지키기보다는 더 명확한 표현이 있다면 그 표현을 써도 무방하다.
E | Element |
K | Key |
N | Number |
T | Type |
V | Value |
S,U,V ... | 2,3,4... Type |