책/Effective Java 3E

이펙티브 자바 - 제네릭

경딩 2025. 1. 16. 17:39

아이템26 . 로 타입을 사용하지 말라!

로 타입이란?  제네릭 타입에서 타입 매개변수가 정의되지 않을 때를 말한다. 

예를 들어 List<E> 의 로 타입은 List 이다.

로(raw) 타입을 사용하지 말아야하는 이유는 무엇일까? 

  • 즉 매개변수화 타입을 사용하라는 말이다.
  • 매개변수화 타입을 사용하면 런타임이 아닌 컴파일 타입에 문제를 찾을 수 있고(안정성 -> 로타임 컬렉션에는 아무원소나 넣은 수 있어 타입 불변식 훼손이 쉽다.)
  • 제네릭을 활용하면 이 정보가 주석이 아닌 타입 선언 자체에 녹아든다. (표현력 : ex ) List<Integer> -> Integer 라는 타입을 넣는다는 사실을 알수 있음)
  • 로 타입을 사용하면 안정성과 표현력을 잃게된다.

아이템27. 비검사 경고를 제거하라

 

"비검사(unchecked) 경고 란? "

컴파일러가 타입 안정성을 확인하는데 필요한 정보가 충분치 않을 때 발생시키는 경고

모든 비검사 경고는 런타임에 ClassCastException 을 일으킬 수 있는 잠재적 가능성을 뜻하니 최선을 다해 제거하자!

 

 대부분의 비검사 경고는 위와 같이 컴파일러가 알려준다. 컴파일러는 위와 같은 경우 타입 안정성을 보장하지 못해 위와 같은 오류를 알려준다. 

        Set<String> names = new HashSet<>();

 위와같이 제네릭을 사용하면 비검사 경고를 금방 고칠수 있다.

 내가 이미 알고 있는 경우이면 안전하다면 판단되면 

가능한한 좁은 범위에 @SuppressWarnings("unchecked") 을 활용하자. 그런다음 경고를 숨기기로 한 근거를 주석으로 남기자!
    public <T> T[] toArray(T[] a) {
        if (a.length < size) {
            /**
             * 이 애노테이션을 왜 여기서 선언했는지..
             */
            @SuppressWarnings("unchecked")
            T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
            return result;
        }
        System.arraycopy(elements, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

 

 

 

 아이템28. 배열보다는 리스트를 사용하라

  • 배열과 제네릭을 어울리지 않는다. 
  • 배열은 공변 (covariant)이며 , 제네릭은 불공변이다.
  • 배열은 실체화 (reify) 되지만 , 제네릭은 실체화 되지 않는다. (소거)
    • new Generic<타입>[배열] 은 컴파일 할 수 없다.
    • 제네릭 소거: 원소의 타입을 컴파일 타임에만 검사하며 런타임에는 알 수 없다. (하위 버전 호환성문제로 소거됨)

실제 바이트코드를 확인해보면 9번째 라인에서 John 이름이 셋팅된 이후 get 으로 꺼내올때 Object 타입으로 꺼내진것을 볼수 있다.  타입이 소거되어 Object 로 꺼내진 후 String 으로 캐스팅되는 것을  확인할 수 있다.

실체화가 된다는 말은 런타임에도 해당 타입을 보존한다는 말이다.

배열은 런타임에는 타입에 안전하지만 컴파일 타입에는 그렇지 않다. (에러발생 X)

 

변성 ? 타입 계층에서 서로 다른 타입간의 어떤 관계가 있는지 나타내는 개념

 

 공변, 불공변 ? 상속관계에서 생각해보자

1. 불변성 (무공변성, invariant)

상위타입 하위타입이 의미가 없다. 

상속 관계에 상관없이 자신의 타입만 허용하는 것을 뜻한다.  Generic 은 무공변성이기 때문에 타입 안정성을 보장한다.
        List<Object> list = new ArrayList<Integer>(); // 컴파일 에러
 
Object 와 Integer 는 다른 타입이다. 물론 상하관계가 있지만 해당 관계는 무시되며 자신의 타입만 허용된다.
        List<String> names = new ArrayList<>();
        List<Object> objects = names; // 컴파일 에러 발생
List<String> 타입은 List<Object> 타입과는 아무런 연관이 없다.
 

2. 공변성(covariant)

자기 자신과 자식 객체를 허용한다.

<? extends T> 와 같다.

 

배열은 공변이다.

Object[] object = new Integer[10];

 

Integer 가 Object 를 상속하고 있다. Object 가 최상위타입이다.

그러므로 Integer 의 타입을 Object 의 타입으로 변환이 가능한 호환이 가능한 타입이다.

Object 에 Integer 을 담을 수 있다. 즉 배열안에 들어가는 타입을 더 높은 타입으로 변환하는것이 가능하다.

Object 파일에 참조를 하고있지만 실제 인스턴스는 Integer 배열 타입이다.

        Object[] object = new Integer[10];
        object[0] = "test"; // ArrayStoreException 발생

 

배열은 공변이기 때문에 런타임에는 에러가 발생한다.

배열은 런타임에는 타입에 안전하지만 컴파일 타입에는 그렇지 않다. (에러발생 X)

 

 

배열과 리스트를 섞어 쓰다가 컴파일 오류나 경고를 만나면 가장먼저 배열을 리스트로  대체하자!

비검사 형변환 경고를 제거할 수 있다.

// 코드 28-6 배열 기반 Chooser
public class Chooser_Array<T> {
    private final T[] choiceList;
    
    @SuppressWarnings("unchecked")
    public Chooser_Array(Collection<T> choices) {
        choiceList = (T[]) choices.toArray();
    }
// 코드 28-6 리스트 기반 Chooser - 타입 안전성 확보! (168쪽)
public class Chooser<T> {
    private final List<T> choiceList;

    public Chooser(Collection<T> choices) {
        choiceList = new ArrayList<>(choices);
    }

 

3. 반공변성(contravariant)

공변성이 반대 - 자기 자신과 부모 객체만 허용한다.

<? super T> 와 같다.

 

        // 제네릭과 배열을 같이 사용할 수 있다면 ...
        List<String>[] stringList = new ArrayList<>[1]; // cannot create array with '<>'
        List<Integer> intList = List.of(1, 2, 3);
        Object[] objects = stringList;
        objects[0] = intList;
        String s = stringList[0].get(0); // classCast Exception
        System.out.println(s);

 

 

 

아이템 29.이왕이면 제네릭 타입으로 만들라

  • 배열을 사용하는 코드를 제네릭으로 만들때 해결책 두가지
  • 첫번째 방법 : 제네릭 배열 (E[]) 대신에 Object 배열을 생성한 뒤에 제네릭 배열로 형변환을 한다.
    • 형변환을 배열 생성시 한번만 한다.
    • 가독성이 좋다.
    • 힙오염이 발생할 수 있다.
  • 두번째 방벙 ; 제네릭 배열 대신에 Object 배열을 사용하고 배열이 반환된 원소를 E로 형변환 한다.
    • 원소를 읽을때 마다 형변환 해줘야한다.

클라이언트에서 직접 형변한해야하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다. 그러니 새로운 타입을 설계할 때는 형변환 없이도  사용할 수 있도록한다.

 

한정적 타입 매개변수

매개변수화 타입을 특정한 타입으로 한정 짓고 싶을때 사용할 수 있다.

<E extends Number> 선언할 수 있는 제네릭 타입을 Number 를 상속했거나 구현한 클래스로 제한한다.

 

 

아이템 30.이왕이면 제네릭 메서드로 만들라

  • 매개변수화 타입을 받는 정적 유틸리티 메서드
    • 한정적 와일드카드 타입을 사용하면 더 유연하게 개선할 수 있다.
  • 제네릭 싱글턴 팩터리
    • 소거방식이기때문에 불변객체 하나를 어떤 타입으로든 매개변수화 할 수 있다.
  • 재귀적 타입 한정
    • 자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정한다.

 

제네릭 타입과 마찬가지로 제네릭 메서드로 만들며 형변환에 안전하다.

매개변수화 타입을 받는 정적 유틸리티는 보통 제네릭이다.

raw 타입 선언시 unchecked 경고 발생

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

 

    // 코드 30-3 제네릭 메서드를 활용하는 간단한 프로그램 (177쪽)
    public static void main(String[] args) {
        Set<String> guys = Set.of("톰", "딕", "해리");
        Set stooges = Set.of("래리", "모에", "컬리");
        Set<String> all = union(guys, stooges);

        for (String o : all) {
            System.out.println(o);
        }

 

제네릭 메소드가 어떻게 static 이 가능할까? 제네릭 메소드의 타입파라미터는 메소드 호출시 결정되기때문이다.

이는 클래스의 인스턴스에 종속되지 않고, 메서드 레벨에서만 사용된다.

 

 

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

이종컨테이너란?

  • 서로 다른 타입의 컨테이너가 중첩되어있는것이다.
  • Nested 클래스처럼 서로 다른 타입의 컨테이너가 중첩래서 정의된 경우이다.

타입 안전 이종 컨테이너라?

말 그대로 중첩 컨테이너에 컨테이너 API 가 타입 안전성을 보장해준다는 제약이 추가된 이종 컨테이너이다.

 

P198

컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴을 타입 토큰이라 한다.

타입 토큰이라고 부르는 클래스 리터럴의 목적은 무엇일까?

타입 정보를 같이 들고 있다가 전달받은 타입정보를 같이  받고 전달받은 타입정보로 형변환을 해주어서 타입 안정성을 보장해주겠다라는것이 목적이다.

클래스라는 객체가 타입 정보를 들고 있는 것이다 Class<T> 제네릭 .

 

일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어있다.
하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다.타타입 매개변수가 고정되어있다는게 무슨 뜻일까?

 

처음 제네릭 타입을 선언하는 순간 그 객체의 타입 매개변수가 고정된다. 그 객체의 맵의 키 타입으로 String으로 선언한느 순간 그 맵은 스트링 타입의 키밖에 못받기 때문에 고정되었다는 뜻이다.

만약 키값으로 String, Integer 다 쓰고 싶다면  

클래스 이터럴은 t 타입에 대한 클래스 메타데이터(클래스 이름, 부모 클래스, 패키지 정보 등)를  가지고 있는것이다.

 

Class<?> classOfInteger1 = Integer.valueOf(1).getClass();
Class<?> classOfInteger2 = Integer.valueOf(2).getClass();

System.out.println(classOfInteger1 == classOfInteger2); // true
System.out.println(classOfInteger1 == Integer.class);   // true
System.out.println(classOfInteger2 == Integer.class);   // true

그래서 클래스 리터럴 객체를 비교하면 서로 같다.

 

해시 맵의 키가 클래스 리터럴로 정의되어 있으면 맵은 타입정보를 키로 받는다.

db 에 값을 넣을때 컬럼이라는 하나의 컨테이너 타입으로 추상화 하는데 실제로 DB 에서 그 컬럼에 여러가지 타입이 존재할 수 있으니까 그부분을 표현하기 위해서 클래스 리터럴를 쓴다.

 

package chapter05.item33.super_type_token;

import java.util.*;

public class Favorites2 {

    private final Map<Class<?>, Object> favorites11 = new HashMap<>();
    private final Map<TypeRef<?>, Object> favorites = new HashMap<>();

    public <T> void put2(Class<T> clazz, T thing) {
        favorites11.put(Objects.requireNonNull(clazz), thing);
    }
    public <T> void put(TypeRef<T> typeRef, T thing) {
        favorites.put(typeRef, thing);
    }

    @SuppressWarnings("unchecked")
    public <T> T get(TypeRef<T> typeRref) {
        return (T)(favorites.get(typeRref));
    }


    public <T> T get2(Class<T> clazz) {
        return clazz.cast(favorites11.get(clazz));
    }


    public static void main(String[] args) {
        Favorites2 f = new Favorites2();


        f.put2(Integer.class, 123);
        f.put2(Class.class, Favorites2.class);
        f.put2(List.class, List.of(1, 2, 3));
        f.put2(List.class, List.of("1", "2", "3"));
        List f4 = f.get2(List.class);
        Class f2 = f.get2(Class.class);
        System.out.println("eedfsd" + f2);
        f4.forEach(System.out::println);

//        f.put(String.class, "est");

//        TypeRef<List<String>> stringTypeRef = new TypeRef<>() {};
//        System.out.println(stringTypeRef.getType());
//
//        TypeRef<List<Integer>> integerTypeRef = new TypeRef<>() {};
//        System.out.println(integerTypeRef.getType());
//
//        f.put(stringTypeRef, List.of("a", "b", "c"));
//        f.put(integerTypeRef, List.of(1, 2, 3));
//        f.get(stringTypeRef).forEach(System.out::println);
//        f.get(integerTypeRef).forEach(System.out::println);
    }

}