본문 바로가기

이팩티브 자바

제네릭

728x90

머리말

친구가 이팩티브 자바 책을 읽으면서 가장 어려웠던 부분이 제네릭이라고 해서 더욱더 열심히 보았던 파트였습니다. 5장을 읽으면서 자바 제네릭에 대하여 학습해보고, 제네릭의 장점을 살리고 단점을 최소화 하는 방법을 이번장을 통해서 익히도록 하겠습니다.

용어정리

  • 공변, 반공변 타입 : 리스히코프 치환법칙을 적용할 수 있는 타입
    • **타입끼리 다운캐스팅(공변), 업캐스팅(반공변)**이 가능한 타입
    // 공변성
    Object[] Covariance = new Integer[10];
    
    // 반공변성
    Integer[] Contravariance = (Integer[]) Covariance;
    
    // 공변성
    ArrayList<Object> Covariance = new ArrayList<Integer>();
    
    // 반공변성
    ArrayList<Integer> Contravariance = new ArrayList<Object>();
    
  • 비공변 타입 : 리스히코프 치원법칙을 적용 할 수 없는 타입, 제네릭은 비공변 타입이다.
    • 타입끼리 업캐스팅, 다운캐스팅이 불가능한 타입 : Object와 String의 관계
  • 비한정적 와일드카드 타입 : 알려지지 않는 타입의 리스트 List<?> 로 선언하여 null 값만 넣을 수 있다.
    • 모든 타입을 수용 할 수 있다.
  • 한정적 와일드카트 타입 : 타입 매개변수가 특정 타입의 서브타입 이거나 슈퍼타입이라는 것을 제한을 둡니다.
    • Upper Bound Wildcards : List<? extends Foo> list : 지정된 타입의 현재타입 혹은 하위타입만 허용한다.
    • Lower Bound Wildcards : List<? super Foo> list : 지정된 타입의 상위 타입만 허용한다.
    • PECS ( Producer-Extends, Consumer-Super ) 생산자라면 <? extends T> 를 사용하고, 소비자라면 <? super T> 를 사용하라.

로 타입(raw type)은 사용하지 말라

로 타입이라고 한다면 제네릭 매개변수를 활용하지 않을 때를 이야기한다. 예를들어 다음과 같이 List, Map, Collections 선언하게 된다면 로 타입(raw type)이다.

로타입(raw type)으로 선언하게 될경우 의도 하지 않는 객체를 넣을 수 있고, 나중에 객체를 꺼낼때 ClassCastException을 맞이 할 수 있다. 로타입(raw type)을 사용할 경우 런타임에 예외사항이 발생하기 때문에 찾기 어렵게된다. 최대한 에러는 컴파일 시점에서 찾아야하고, 컴파일 시점에 찾기 위해서는 타입을 컴파일러에게 명시해주어야한다.

로타입(raw type) 대신하여 비한정적 와일드카드 타입을 사용하는데, 어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화 타입이다.

하지만 로타입을 써도되는 예가 있는데 제네릭 타입에 instanceof 를 사용하는 올바른 예이다.

if (o instanceof Set) {  // 로 타입
	Set<?> s = <Set<?> o;  // 와일드 카드 타입
	...
}

<aside> ✏️ Set<Object>와 Set<?> 는 안전하나, 로 타입 Set은 안전하지 못하다. 로 타입은 런타임에 에러를 발생 시킬 수 있다. 로타입을 사용하는 이유는 제네릭이 없던시절의 레거시 코드 때문이다.

</aside>

비검사 경고를 제거하라

  • 코드가 타입 안전하다고 판단되면 할 수 있는 한 모든 비검사 경고를 제거하라.
  • @SuppressWarings 에너테이션은 항상 가능한 좁근 범위에 적용하라.
    • @SuppressWarings(”unchecked”) 를 사용할경우 항상 주석으로 남겨야한다.

배열보다 리스트를 사용하라

배열은 공변타입이고, 제네릭은 불공변 타입이다. Type1과 Type2가 있을때 List<Type1> 과 List<Type2>는 불 공변 타입이다. 따라서 배열보다 제네릭과 호환이 맞는 리스트를 사용한다.

배열은 런타임에는 타입 안전하지만 컴파일타임에는 그렇지 않다. 제네릭은 그 반대이다. 따라서 둘이 섞어쓰기란 쉽지 않다.

이왕이면 제네릭 타입으로 만들라

클라이언트에서 직접 형변환 해야하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다. 그러니 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라. 그렇게 하려면 제네릭 타입으로 만들어야 할 경우가 많다. 기존 타입중 제네릭이 있어야하는게 있다면 제네릭 타입으로 변경하자. 기존 클라이언트는 아무 영향을 주지 않으면서, 새로운 사용자를 훨씬 편하게 해주는 길이다.

이왕이면 제네릭 메서드로 만들라

제네릭 타입과 마찬가지로, 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환 해야하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기 쉽다. 타입과 마찬가지로, 메서드도 형변환 사용없이 사용할 수 있는 편이 좋으며, 많은 경우 그렇게 하려면 제네릭 메서드가 되어야한다. 역시 타입과 마찬가지로, 형변환을 해줘야하는 기존 메서드는 제네릭으로 리팩터링 하자. 기존 클라이언트는 그대로 둔 채 새로운 사용자의 삶을 훨씬 편하게 만들어 줄 것이다.

한정적 와일드카드를 사용해 API의 유연성을 높이라

제네릭이 불공변타입으로 유연성을 극대화 하려면 생산자, 소비자 관계를 파악하여 <? extends E>, <? super E> 매개변수에 한정적 와일드 카드를 타입을 사용하자. 자바7까지는 명시적 타입인수를 사용해야한다.

  • 메서드 선언에 타입 매개변수가 한번만 나오면 와일드 카드로 대체하라.
  • private 메서드 도우미를 작성하여 활용하라
public static void swap(List<?> list, int i, int j)
{
	swapHelper(list, i, j);
}

private static <E> void swapHelper(List<E> list, i, j)
{
	list.set(i, list.set(j, list.get(i)));
}
  • PECS 공식을 기억하자. Producer - Extends, Consumer - Super
    • Comparable과 Comparator는 모두 소비자 라는 사실도 잊지말자.

제네릭과 가변인수를 함께 쓸 때는 신중하라.

가변인수는 기본적으로 배열이다. 배열은 제네릭과 어울리지 못한다. 해당 메서드가 타입 안전한지 확인 한다음 @SafeVaragrs 에너테이션을 달아 사용하는데 불편함이 없게끔 하자.

  • varargs매개변수 배열이 호출자로부터 그 메서드로 순수하게 인수들을 전달하는 일만 한다면 그 메서드는 안전하다.

아래의 예제를 보면 toArray를 통하여 Object[] 를 반환하게되고, 클라이언트 코드에서 pickTwo를 호출하여 결과 값을 받을때 String[]으로 캐스팅 하기때문에 ClassCastException이 발생한다.

  • 제네릭 varargs 매개변수 배열에 다른 메서드가 접곤하도록 허용하면 안전하지 않다는걸 상기시킨다.
public static void main(String[] args)
{
	String[] attributes = pickTwo("좋은", "빠른", "저렴한"); // ClassCastException 발생
}

static <T> T[] pickTwo(T a, T b, T c) {
	swtich(ThreadLocalRandom.current().nextInt(3)) {
		case 0 : return toArray(a, b);
		case 1 : return toArray(a, c);
		case 2 : return toArray(b, c);
	}
	throw new AssertionError(); // 도달 할 수 없다.
}

아래의 예제는 varargs 매개변수를 안전하게 사용하는 메서드이다.

  • @SafeVaragrs 에너테이션은 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치이다.
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
	List<T> result = new ArrayList<>();
	for (List<? extends T? list : lists)
		result.addAll(list);
	
	return result;
}

제네릭 varargs 매개변수를 list로 대체한 예 - 타입 안전하다

static <T> List<T> flatten(List<List? extends T>> lists {
		List<T> result = new ArrayList<>();
	for (List<? extends T? list : lists)
		result.addAll(list);
	return result;
}

pickTwo 를 타입안전하게 바꾼 예제

결과 코드는 배열 없이 제네릭만 사용하므로 타입 안전하다.

public static void main(String[] args)
{
	List<String> attributes = pickTwo("좋은", "빠른", "저렴한");
}

static <T> List<T> pickTwo(T a, T b, T c)
{
	swtich(ThreadLocalRandom.current().nextInt(3)) {
		case 0 : return list.Of(a, b);
		case 1 : return list.Of(a, c);
		case 2 : return list.Of(b, c);
	}
	throw new AssertionError(); // 도달 할 수 없다.
}

타입 안전 이종 컨데이너를 고려하라

컬랙선 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수가 고정되어있다. 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 이종 컨테이너를 만들 수 있다. 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런식으로 쓰이는 Class 객체를 타입 토큰이라 한다. 또한, 직접 구현한 키 타입도 쓸 수 있다. 예건데 데이터베이스의 행(컨테이너)을 표현한 DatabaseRow 타입에는 제네릭 타입은 Column<T>를 키로 사용 할 수 있다.

한정적 타입 토큰을 안전하게 형 변환한다.

static Annoation getAnnoation(AnnotatedElement element, String annoationTypeName) {
	Class<?> annoationType = null; // 비 한정적 타입 토큰
	try {
		annoationType = Class.forName(annoationTypeName);
	} catch (Exception ex) {
		throw new IllegalArgumentException(ex);
	}
	return element.getAnnoation(annoationType.asSubclass(Annoation.class));
}

회고

이번시간에는 여러가지 개념들을 배웠는데요, 실무에서 제네릭을 많이 사용해보지않아서 배우는데 조금 시간이 걸렸습니다. 흥미로운 개념들을 배워서 지금까지 봤던 파트중에 제일 흥미가 갔었네요, 한정적 타입과 비한정적 타입 공변 타입, 반공변타입, 불공변타입, 한정적 와일드카드, 비한정적 와일드카드의 개념을 알게되었고, 어떻게 사용해야할지도 감이 잡혔습니다. 자바에서의 제네릭을 어떻게 사용해야하는지 왜 사용해야하는지 알 수 있는 시간이 되었습니다.

요약

  • 가능한 제네릭을 사용하자 제네릭을 사용하면 클라이언트가 타입 캐스팅을 신경쓰지 않아도 된다. 그 뿐더러 코드의 유연성까지 따라온다. raw 타입은사용하지 말자.
  • 제네릭은 배열와 어울리지 않는다. 배열은 공변타입이고 제네릭은 불공변 타입이기때문이다. 배열 대신에 리스트를 적극 활용하자.
  • 한정적 와일드카드를 사용하여 API의 유연성을 증진시키자.
  • 비경고 검사 메세지를 모두 제거하라
728x90

'이팩티브 자바' 카테고리의 다른 글

열거 타입과 에너테이션  (0) 2024.03.17
클래스와 인터페이스  (0) 2024.03.17
모든 객체의 공통 메서드  (0) 2024.03.17
객체 생성과 파괴  (0) 2024.03.10