7 minute read

컴파일시 타입을 체킹하자 - 지네릭스

목차

  1. 지네릭스란?
  2. 타입변수
  3. 지네릭스 용어
  4. Iterator, HashMap 과 지네릭스
  5. 제한된 지네릭 클래스
  6. 와일드 카드 <?>

1. 지네릭스란?

  • 컴파일시 타입을 체크해 주는 기능 (compile-time type check)

이전으 컴파일시 타입체킹의 한계를 넘어서기 위해 개발된것이 지네릭스이다.

ArrayList는 기본적으로 Object 배열을 가지고 있다.

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

그러므로 모든 종류의 객체를 저장하는 것이 가능하다. 즉 제네릭스가 존재하기 이전에는 아래와 같은 코드가 가능했었다.

public class GenericTest {

    public static void main(String[] args) {
        ArrayList tvList = new ArrayList();
        tvList.add(new Tv()); 
        tvList.add(new Radio());  // 원하지 않는 Radio 객체가 Object 타입으로 ArrayList에 add가 된다.
        tvList.add(10); // 심지어 참조값 뿐만아니라 primitive 값또한 가능하다.

        Integer i = (Integer) tvList.get(0); // Tv 객체를 integer 로 케스팅 하겠다는것도 컴파일상 아무 문제가 없다..

        System.out.println();
    }

    @Getter
    public static class Radio {
        private int name;
    }

    @Getter
    public static class Tv {
        private int name;
    }
}

이렇게 되면 나중에 잘못된 사용 (타입오용 이건 아래에서 설명) 을 하기가 쉽다 또한 실행시 에러는 치명적이므로, 컴파일 에러로 변경 하는것이 훨씬 안전하고 유지 보수에 유리하다.

컴파일 단계에서 이 타입을 채킹을 하기 위해서는 아래와 같이 사용해야 한다.

public class GenericTest {

    public static void main(String[] args) {
        ArrayList<Tv> tvList = new ArrayList<Tv>();
        tvList.add(new Tv());
//        arrayList.add(new Radio()); // 컴파일 에러. Tv외의
        Tv t = tvList.get(0) // 타입 체크와 형변화능ㄹ 생략하여 코드가 간결해진다.
    }

    @Getter
    public static class Radio {
        private int name;
    }

    @Getter
    public static class Tv {
        private int name;
    }
}

이제 컴파일시 타입을 채킹하는것이 가능해졌다.

그리고 지네릭스를 사용하는 클래스는 아래와 같이 사용해주는것이 JDK 1.5 이상부터의 정석적이다.


// 1.5 이전
ArrayList list = new ArrayList();

// 1.5 이후 지네릭스 클래스 선언
ArrayList<Object> list = new ArrayList<Object>();

물론 에러가 나는것은 아니지만, (빌드시 warning 은 뜸) 지네릭스 클래스를 사용할땐 반드시 타입을 명시해주도록 하자.

2. 타입변수

클래스를 작성할 떄, Object 타입 대신 타입 변수 (E) 를 선언하여 사용

다시 ArrayList 를 살펴보자

JDK 1.5 이전 ArrayList 클래스의 모습이다.

public class ArrayList extends AbstractList {
    ... // 생략

    private transient Object[] elementData;

    public boolean add(object o) {...}
    public Object get(int index) {...}

    ...
}

이러했던 모습이 지네릭스가 도입된후 아래와 같이 변한다.

public class ArrayList<E> extends AbstractList<E> { // E 는 타입변수 
    ... // 생략
    
    private transient E[] elementData;
    public boolean add(E o) {...}
    public E get(int index) {...}

    ...
}

JDK 1.5 이후 부터 Object 를 포함하는 클래스는 대부분 지네릭스 클래스로 변경이 되었다고 한다.

위에 클래스에서 사용하였던 타입 E 는 어떤걸 써도 상관이 없다, 한글자가 아니고 여러개를 사용해도 크게 상관은 없다.

2-2 타입 변수에 대입하기

  • 객체를 생성시, 타입 변수 E 대신 실제 타입(참조 타입) Tv를 지정(대입) 한다.
ArrayList<Tv> tvList = new ArrayList<Tv>();
  • 참조 변수에 대입된 타입과 생성자에 지정된 타입은 일치해야 한다.
  • 프리미티브 타입에 대해서는 지네릭스를 지원하지 않는다.

3. 지네릭스 용어

Box<T> 지네릭 클래스 'T의 Box' 또는 'T Box' 라고 읽는다.
T 타입변수 또는 타입 매개변수.
Box 원시 타입 (raw type)
class Box<T> {} // 지네릭 클래스 선언

Box<String> b = new Box<String>(); // 여기서 String 은 대입된 타입(매개변수화된 타입, parameterized type) 이라 한다.

3-2 지네릭 타입과 다형성

  • 참조 변수와 생성자의 대입된 타입은 완전 일치해야 한다.
public class GenericTest {

    public static void main(String[] args) {
        ArrayList<Product> productList = new ArrayList<Product>(); // 일치
        
        Product p = new Tv(); // 다형성 가능
        // ArrayList<Product> products = new ArrayList<Tv>(); // 에러 불일치 
        
    }

    public static class Product {
        
    }

    public static class Tv extends Product{
        private int name;
    }

    public static class Radio extends Product{
        private int name;
    }
}

ArrayList<Product> products = new ArrayList<Tv>(); // 에러, 불일치

부모 자식간의 관계에서도 불가능하다는걸 볼수 있다. 대신,

  • 지네릭 클래스간의 다형성은 성립한다. (여전히 대인된 타입은 일치해야 한다.)
    List<Tv> list = new ArrayList<Tv>();
    
  • 매개변수의 다형성도 성립된다.
    • 참조타입과 생성타입은 다형성이 성립하지 않지만 위는 성립한다.
ArrayList<Product> list = new ArrayList<Product>();
list.add(new Product());
list.add(new Tv());
list.add(new Radio());

이것이 가능한 이유는 지네릭 타입 ____ 대신에 Product 를 지정해줬을때 클래스는 아래와 같이 변했다고 볼수 있다.

지정전

boolean add (E e) {...}

지정후

boolean add (Product e) {...}

4. Iterator, HashMap 과 지네릭스

4-1. Iterator

  • 클래스를 작성할 때, Object 타입 대신 T와 같은 타입 변수를 사용

일반 클래스

public interface Iterator {
    boolean hasNext();
    Object next();
    void remove();
}

// 사용 
Iterator it = list.iterator();
while(it.hasNext()) {
    Product p = (Product)it.next(); // 형변환으로 타입 불일치 필요
}

지네릭 클래스

public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}

// 사용
Iterator<Product> it = list.iterator();
while(it.hasNext()) {
    Product p = it.next(); // 형변환으로 타입 불일치 필요
}

4-2. HashMap<K, V>

  • 여러개의 타입 변수가 필요한 경우, 콤파(,) 를 구분자로 사용한다.
public class HashMap<K,V> extends AbstractMap<K,V> {
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    public V put(K key, V value) {...}
    public V remove(Object key) {...}
}

해쉬맵은 위와 같이 구현되어 있다. 그런데 지금까지 잘 따라왔다면, 왜 get 과 remove 는 Object 매개변수를 받는지 이해가 안갈것이다.

간단한 이유는 매소드 안에 있다.

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

이 매소드 안에 hash 메서드를 따라 가보면

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

hash 매서드 또한 Object를 받고 있다. 불필요한 형변환을 안하기 위함이다. https://stackoverflow.com/questions/857420/what-are-the-reasons-why-map-getobject-key-is-not-fully-generic

부족하다면 내 블로그 해쉬맵을 한번 찾아 보길 권한다.

5. 제한된 지네릭 클래스

  • extends로 대입할 수 있는 타입을 제한하자.
public class ProductBox<T extends Product> { // Produc의 자손 타입으로 지정이 가능하다.
    ArrayList<T> list = new ArrayList<T>();
}

// 사용 
ProductBox<Tv> tvBox = new ProductBox<Tv>(); // okay
ProductBox<Person> personBox = new ProductBox<Person>(); // 에러 Person은 Product의 자손이 아니다. 
  • 인터페이스인 경우에도 extends 를 사용한다.
    interface Eatable{}
    class FruitBox<T extends Fruit & Eatable> {...}
    

5-2 지네릭스의 제약

  • 타입 변수에 대입은 인스턴스 별로 다르게 가능
  • static 멤버에 타입 변수 사용 불가하다.
    • 모든 인스턴스에 공통이기 때문
      class Box<T> {
        static T item // 에러
        static int compare(T t1, T t2) {...} // 에러
      }
      
  • 객체 생성할 때 타입 변수 사용 불가. 타입 변수로는 선언 가능
    class Box<T> {
      T[] itemArr; // 가능
      T[] toArray() {
          T[] tmpArr = new T[itemArr.length]; // 에러 지네릭 배열 생성 불가
      }
    }
    

6. 와읻드 카드 ?

  • 하나의 참조 변수로 대입된 타입이 다른 객체를 참조 가능
    ArrayList<? extends Product> list = new ArrayList<Tv>(); // 가능
    ArrayList<? extends Product> list = new ArrayList<Audio>(); // 가능
    ArrayList<Product> list = new ArrayList<Tv>(); // 에러. 대입된 타입의 불일치
    

참조 타입과 생성 타입의 일치는 답답한 부분이 있다.

이러한 타입 일치를 해결하기 위한 방법이 와일드 카드 이다.

와일드 카드에는 아래 3가지가 존재한다.

<? extends T>: 와일드 카드의 상한제한. T와 그 자손들만 가능 (많이쓰이는 방법)
<? super T>: 와일드 카드의 하한 제한. T와 그 조상들만 가능 
<?> 제한없음. 모든 타입이 가능 <? extends Object> 와 동일하다.

예제

public class GenericTest {

    public static void main(String[] args) {

        ArrayList<Product> productList = new ArrayList<Product>(); // 일치

        Product p = new Tv(); // 다형성 가능
        ArrayList<Product> products = new ArrayList<Product>(); // 에러 불일치

        products.add(new Product());
        products.add(new Tv());
        products.add(new Tv());

//        ArrayList<Product> products2 = new ArrayList<Tv>(); // 에러
        ArrayList<? extends Product> products3 = new ArrayList<Tv>(); // 가능
        ArrayList<? extends Product> products4 = new ArrayList<Radio>(); // 가능
//        ArrayList<? extends Product> products5 = new ArrayList<Person>(); // 에러
        
        ArrayList<?> list = new ArrayList<Person>();
        ArrayList<?> list2 = new ArrayList<Tv>();
        ArrayList<?> list3 = new ArrayList<Radio>();

    }

    public static class Product {
    }

    public static class Radio extends Product {
        private String name;
    }

    public static class Tv extends Product {
        private String name;
    }
    
    public static class Person {
        private String name;
    }
}
ArrayList<? extends Product> products = new ArrayList<Tv>(); // 가능
// products.add(new Product()); 불가
// products.add(new Tv()); 불가
products.add(null); // 가능

위 코드에서 products에 add 를 하려할때 추가가 안되는걸 확인할 수 있다. 와일드 카드 타입을 지정하는건 알겠는데, 어떻게 사용하는걸까? 그리고 아래 예제는 왜 안되는지 살펴보자

선언 ArrayList<? extends Object> 는 알수 없는 유형의 타입이 포함된 ArrayList이며 이 유형에 대해 알려진 유일한 정보는 Object를 상속 한다는것 뿐이다. ArrayList 타입의 유형을 모르면 거기에 넣을 수 있는 유일한 값은 null밖에 없다. 컴파일러는 ArrayList에 넣는 값이 ArrayList 타입의 (알수없는) 유형으로 변환의 가능여부를 알수 없기 때문에, 컴파일러는 다른값을 입력하면 오류를 뱉어낸다.

만약 위 코드와 같이 사용하고 싶다면 extend 대신 super를 사용하면 된다.

ArrayList<? super Product> products = new ArrayList<Product>(); // 가능

products.add(new Tv());
products.add(new Product());

products.forEach(System.out::println);

결과

GenericTest$Tv@4c873330
GenericTest$Product@119d7047
  • 와일드 카드와 지네릭 메서드의 차이
    • 와일드 카드는 하나의 참조 변수로 서로 다른 타입이 대입된 여러 지네릭 객체를 다루기 위한 것
    • 지네릭 메서드는 메서드를 호출할 때마다 다른 지네릭 타입을 대입할 수 있게 한것

지네릭 메서드

static <T extends Fruit> Juice makeJuice(FuitBox<T> box) {
}

와일드 카드

static Juice makeJuice(FruitBox<? extends Fruit> box) {
}

참고

자바의 정석 3판

Categories:

Updated: