ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 5장 - 제네릭
    📕 book/이펙티브 자바 2022. 11. 1. 17:21

    제네릭은 자바 5부터 사용할 수 있다.

    제네릭을 지원하기 전에는 컬렉션에서 객체를 꺼낼 때마다 형변환을 해야 했다.

    • 누군가 실수로 엉뚱한 타입의 객체를 넣어두면 런타임에 형변환 오류가 발생

    ⇒ 제네릭을 사용해 컴파일 과정에서 차단하여 더 안전하고 명확한 프로그램을 만들자.

    📌 로 타입은 사용하지 마라


    클래스와 인터페이스 선언에 타입 매개변수(type parameter)가 쓰이면, 이를 제네릭 클래스 혹은 제네릭 인터페이스라고 한다.

    제네릭 타입 (generic type)

    • 제네릭 클래스와 제네릭 인터페이스를 통틀어 제네릭 타입이라 한다.
    • 각각의 제네릭 타입은 일련의 매개변수화 타입 을 정의한다.
      • ex) List<String> 은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입
      • String이 정규 타입 매개변수 E에 해당하는 실제(actual) 타입 매개변

    제네릭 타입을 하나 정의하면 그에 딸린 로 타입(raw type)도 함께 정의된다.

    로 타입

    • 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말한다.
    • List<E> 의 로 타입은 List 이다.

    로 타입을 사용하면 제네릭이 안겨주는 안전성과 표현력을 모두 잃게 된다.

    • 왜 만듬❓ ⇒ 호환성 때문
    • 자바가 제네릭을 받아들이기까지 제네릭 없이 짠 코드가 많았다.
    • 따라서, 기존 코드를 모두 수용하면서 제네릭을 사용하는 새로운 코드와도 맞물려 돌아가게 하도록

    List<Object> 같은 매개변수화 타입을 사용할 때와 달리 List 같은 로 타입을 사용하면 타입 안전성을 잃게된다.

    // static int numElementsInCommon(Set s1, Set s2) - 잘못된 예 Set이 뭔지 모른다.
    
    static int numElementsInCommon(Set<?> s1, Set<?> s2) {
    	int result = 0;
    	for(Object o1 : s1) 
    		if(s2.contains(o1)
    			result++;
    	return result;
    }

    비한정 와일드카드 타입(unbounded wildcard type)을 대신 사용

    • 제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지않다면 물음표(?)를 사용하자

    로 타입은 어떤 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉽다.

    반면, 비한정 와일드카드 타입을 사용하면 null 이외에는 어떤 원소도 넣을 수 없다.

    예외

    1. class 리터럴에는 로 타입을 사용해야 한다.
    2. instance of 연산자
      • 런타임에는 제네릭 타입 정보가 지워진다
      • instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다.
      if(o instanceof Set){
        Set<?> s = (Set<?>) o;
        ...
      }

    정리

    • 로 타입을 사용하면 런타임에 예외가 일어날 수 있다.
    • Set<Object> : 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입
    • Set<?> : 모종의 타입 객체만 저장할 수 있는 와일드카드 타입
    • Set<Object>, Set<?>는 안전하지만, 로 타입인 Set은 안전하지 않다.
    한글 용어 예시
    매개변수화 타입 List<String>
    실제 타입 매개변수 String
    제네릭 타입 List<E>
    정규 타입 매개변수 E
    비한정적 와일드카드 타입 List<?>
    로 타입 List
    한정적 타입 매개변수 <E extends Number>
    재귀적 타입 한정 <T extends Comparable<T>>
    한정적 와일드카드 타입 List<? extends Number>
    제네릭 메서드 static <E> List<E> asList(E[] a)
    타입 토큰 String.class

    📌 비검사 경고를 제거하라


    제네릭을 사용하면 많은 컴파일러 경고가 있다.

    • 대부분의 비검사 경고는 쉽게 제거할 수 있다.

    경고를 제거할 수는 없지만 타입 안전하다고 확신할 수 있다면 @SuppressWarnings(”unchecked”) 애너테이션을 달아 경고를 숨기자

    • 개별 지역변수 선언부터 클래스 전체까지 어떤 선언에도 달 수 있다.
    • 항상 가능한 좁은 범위에 적용하자
    • 절대로 클래스 전체에 적용해서는 안된다
      • 심각한 경고를 놓칠 수 있다.
    • 사용시 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야 한다.

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


    배열과 제네릭의 차이

    • 배열은 공변이다, 제네릭은 불공변이다.
      • Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이다.
      • 제네릭은 서로 다른 타입 Type1, Type2가 있을 때, List<Type1>는 List<Type2>의 하위 타입도 상위타입도 아니다.
    Object[] objectArray = new Long[1];
    ObjectArray[0] = "타입이 달라 넣을 수 없다"; // 런타임에 실패
    
    // 컴파일 되지 않는다.
    List<Object> ol = new ArrayList<Long>();
    ol.add("타입이 달라 넣을 수 없다");
    • 배열은 실체화 된다.
      • 배열은 런타임에도 자신이 담기로한 원소의 타입을 인지하고 확인
      • 하지만, 제네릭은 타입 정보가 런타임에 소거
        • 원소 타입을 컴파일타임에만 검사하며 런타임에는 알수조차 없다.

    정리

    배열은 런타임에는 타입 안전하지만 컴파일타임에는 그렇지 않다.

    제네릭은 반대

    ⇒ 둘을 섞어 쓰기 쉽지 않다.

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


    일반 클래스를 제네릭 클래스로 만드는 첫 단계는 클래스 선언에 타입 매개변수를 추가하는 일이다.

    • 이 때, 타입 이름으로는 보통 E를 사용
    public class Stack<E> {
    	private E[] elements;
    	...
    	public Stack() {
    		elements = new E[...]; // 문제 발생
    	}
    	...
    }

    배열을 사용하는 코드를 제네릭으로 만들려할 때 문제 발생

    • E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다.

    → 해결 방법 2가지

    제네릭 배열 생성을 금지하는 제약을 대놓고 우회

    elements = (E[]) new Object[...];
    • 가독성이 좋고 확실하게 E타입의 배열을 선언하여 한번만 형변환을 해주면 되고 코드가 짧다.
    • 이 방법을 선호
    • 하지만, 배열 런타임 타입이 컴파일타임 타입과 달라 힙 오염을 일으킨다.

    elements 필드의 타입을 E[]에서 Object[]로 바꾸기

    • 데이터를 꺼낼 때마다 형변환을 해주어야 한다.

    타입 매개변수에 제약을 두는 제네릭 타입

    class DelayQueue<E extends Delayed> implements BlockingQueue<E>
    ...

    타입 매개변수 목록인 <E extends Delayed>는

    java.util.concurrent.Delayed 의 하위 타입만 받는다는 뜻

    이러한 타입 매개변수 E를 한정적 타입 매개변수라 한다.

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


    클래스와 마찬가지로, 메서드도 제네릭으로 만들 수 있다.

    메서드 선언에서 입력, 반환의 원소 타입을 타입 매개변수로 명시하고 메서드 안에서도 이 타입 매개변수만 사용

    public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    	Set<E> result = new HashSet<>(s1);
    	result.addAll(s2);
    	return result;
    }

    때때로, 불변 객체를 여러 타입으로 활용할 수 있게 만들어야 할 때가 있다.

    • 제네릭은 런타임에 타입 정보가 소거되므로 하나의 객체를 어떤 타입으로든 매개변수화할 수 있다.
    • 이렇게 하기 위해선 요청한 타입 매개변수에 맞게 매변 그 겍체의 타입을 바꿔주는 정적 팩터리를 만들어야 한다.
    • 제네릭 싱글턴 팩터리
    public static void main(String[] args) {
    	String[] strings = {"삼베", "대마", "나일론"};
    	UnaryOperator<String> sameString = identityFunction();
    	for(String s : strings) 
    		System.out.println(sameString.apply(s));
    	
    	Number[] numbers ={1, 2.0, 3L};
    	UnaryOperator<Number> sameNumber = identityFunction();
    	for(Number n : numbers)
    		System.out.println(sameNumber.apply(n));
    }
    • 제네릭 싱글턴을 사용하는 예
      • UnaryOperator<String> 과 UnaryOperator<Number>로 사용

    자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있다.

    재귀적 타입 한정(recursive type bound)

    • 주로 타입의 자연적 순서를 정하는 Comparable 인터페이스와 함께 쓰인다.
    public static <E extends Comparable<E>> E max(Collection<E> c);

    <E extends Comparable<E>>

    • “ 모든 타입 E는 자신과 비교할 수 있다 “

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


    매개변수화 타입은 불공변

    → List<String> 은 List<Object>가 하는 일을 제대로 수행하지 못하니 하위 타입이 될 수 없다.

    (리스코프 치환 원칙에 어긋)

    자바는 이런 상황을 대처할 수 있는 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원한다.

    • 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용

    공식 - PECS(펙스)

    PECS : producer-extends, consumer-super

    매개변수화 타입 T가 생산자라면?

    • T를 확장하는 와일드카드 타입을 사용
    • <? extends T>

    매개변수화 타입 T가 소비자라면

    • <? super T>

    제대로 사용하면 클라이언트는 와일드카드 타입이 쓰인지 의식하지 않아도 된다

    클래스 사용자가 와일드카드 타입을 신경써야한다면 API에 문제가 있을 가능성이 크다.

    Comparable

    • 언제나 소비자
    • Comparable<E> 보다는 Comparable<? super E>를 사용하는 것이 낫다.

    Comparator

    • Comparator<E> 보다 Comparator<? super E>를 사용하는 것이 낫다.

    타입 매개변수와 와일드카드

    타입 매개변수와 와일드카드에는 공통되는 부분이 많아, 메서드를 정의할 때 둘 중 어느 것을 사용해도 괜찮을 때가 많다.

    public static <E> void swap(List<E> list, int i, int j);
    public static void swap(List<?> list, int i, int j);
    • 어떤 것이 더 나을 까?

    기본 규칙 : 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하라

    public static void swap(List<?> list, int i, int j) {
    	swapHelper(list, i, j);
    }
    
    // 와일드카드 타입을 실제 타입으로 변경해주는 private 도우미 메서드)
    private static <E> void swapHelper(List<E> list, int i, int j) {
    	list.set(i, list.set(j, list.get(i)));
    }
    • List<?>은 null외에는 어떤 값도 넣을 수 없어 문제가 발생
    • 와일드카드 타입의 실제 타입을 알려주는 메서드를 private 도우미 메서드로 따로 작성하여 활용
      • 실제 타입을 알기 위해서 이 메서드는 제네릭 메서드여야 함.

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


    가변 인수 메서드는 제네릭과 함께 자바 5때 함께 추가되어 서로 잘 어우러질것 같지만 그렇지 않다.

    • 가변 인수는 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있게 해주는데 구현에 허점이 존재

    → 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어 진다.

    내부로 감춰야 했을 이 배열을 클라이언트에 노출하는 문제가 발생

    그 결과 varargs 매개변수에 제네릭이나 매개변수화 타입이 포함되면 알기 어려운 컴파일 경고가 발생

    가변 인수 (Variable argument, varargs)

    • 매개변수의 개수를 동적으로 지정하는 기능
    public static print_val(String... str) {
    	for (String s : str) 
    		System.out.print(s + " ");
    }
    

    제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.

    @SafeVarargs 에노테이션은 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치

    • 메서드가 이 배열에 아무것도 저장하지 않고 그 배열의 참조가 밖으로 노출되지 않는다면 안전
    • varargs 매개변수 배열이 호출자로부터 그 메서드로 순수하게 인수들을 전달하는 일만 한다면 안전

    제네릭 varargs 매개변수 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않다.

    예외

    • @SafeVarargs로 제대로 에너테이트된 또 다른 varargs 메서드에 넘기는 것은 안전
    • 배열 내용의 일부 함수를 호출만 하는 일반 메서드에 넘기는 것도 안전

    제네릭 varargs 매개변수를 안전하게 사용하는 메서드

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

    제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs를 달자

    다음 두 조건을 모두 만족하는 제네릭 varargs 메서드는 안전

    • varargs 매개변수 배열에 아무것도 저장하지 않는다.
    • 그 배열을 신뢰할 수 없는 코드에 노출하지 않는다.

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


    제네릭은 Set, Map 등의 컬렉션과

    ThreadLocal<T>, AtomicReference<T> 등의 단일원소 컨테이너에도 흔히 쓰인다.

    • 매개변수화 되는 대상은 (원소가 아닌) 컨테이너 자신이다.
    • 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다.

    하지만, 더 유연한 수단이 필요할 때도 종종 있다.

    → 해결 방법

    • 컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공

    이런 설계 방식을 타입 안전 이종 컨테이너 패턴 이라 한다.

    각 타입의 Class 객체를 매개변수화한 키 역할로 사용

    • class의 클래스가 제네릭이기 때문에 이런 방식이 동작한다
    • 컴파일타임 타입정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고 받는 class 리터럴을 타입토큰(type token)이라 한다.
    // 타입 안전 이종 컨테이너 패턴 API
    public class Favorites {
    	public <T> void putFavorite(Class<T> type, T instance);
    	public <T> T getFavorite(Class<T> type);
    }
    // 타입 안전 이종 컨테이너 패턴 - 클라이언트
    public static void main(String[] args) {
    	Favorites f = new Favorties();
    
    	f.putFavorite(String.class, "Java");
    	f.putFavorite(Integer.class, 0xcafebabe);
    	f.putFavorite(Class.class, Favorites.class);
    
    	String s = f.getFavorite(String.class);
    	int i = f.getFavorite(Integer.class);
    	Class<?> c = f.getFavorite(Class.class);
    
    }

    제약 사항

    • 악의적인 클라이언트가 Class 객체를 (제네릭이 아닌)로 타입 으로 넘기면 Favorite 인스턴스의 타입 안전성이 쉽게 깨짐
    • 실체화 불가 타입에는 사용할 수 없다.
      • String, String[]은 저장할 수 있어도
      • List<String>은 저장할 수 없다.

    한정적 타입 매개변수

    • 한정적 와일드카드를 사용하여 표현 가능한 타입을 제한하는 타입 토큰

    참고 자료

    http://www.yes24.com/Product/Goods/65551284

    댓글

Designed by Tistory.