Post

Generic





참고





Generic

  • 자바의 Collections는 아주 유용한 라이브러리이다.
  • 그러나 초기의 Collections는 상당히 큰 제한이 있었다.
    • 컬렉션 내의 데이터들의 타입을 모호하게 한다는 점이었다.
    • 데이터 은닉과 캡슐화는 객체 지향의 훌륭한 원칙이지만,
    • 이런 경우에는 오히려 많은 문제를 야기할 수 있었다.



1
2
3
4
5
6
7
8
9
10
List animals = new ArrayList();

animals.add(new Dog());
animals.add(new Cat());

// 리턴 타입은 Object이다. 따라서 타입 캐스팅을 해줘야 한다.
Object o = animals.get(0);

Dog dog = (Dog) animals.get(0);
Dog cat = (Dog) animals.get(1); // 런타임 에러 발생
  • 이 코드는 컴파일 시점에는 아무 문제가 없다. 하지만 결국엔 프로그램이 터져버렸다.
  • animals 리스트는 자신이 갖고 있는 객체의 타입을 알지 못한다.
    • 이 리스트는 서로 다른 타입의 객체를 넣을 수 있다.
    • 만약 리스트가 자신이 어떤 타입의 객체들을 갖고 있어야 하는지 알고 있다면,
      • 컴파일 시점에 위의 에러를 잡을 수 있었을 것이다.



1
2
3
4
5
6
List<Dog> dogs = new ArrayList(); // 리스트가 어떤 타입의 객체를 갖는지 명시해준다.

dogs.add(new Dog());
dogs.add(new Cat()); // 컴파일 에러 발생

Dog dog = dogs.get(0);
  • 이 코드는 리스트가 Dog 타입을 가져야한다고 명시해줬다.
    • <> 이걸 사용해서
  • Cat 타입의 객체를 리스트에 넣으려고하자 컴파일 에러로 사전에 잡아낼 수 있었다.
  • 별도의 타입 캐스팅 없이 Dog 리스트에서 Dog 객체를 꺼낼 수 있게 되었다.


  • 이렇게 객체를 둘러싸는 컨테이너의 타입과
    • 안에 들어가는 객체의 타입을
    • 결합한 타입을 제네릭 타입이라고 한다.
  • 아래와 같이 선언할 수 있다.
1
2
3
4
interface MyList<T> extends Collection<T> {
    void add(T element);
    T get();
}
  • MyList는 모든 타입의 객체를 보유할 수 있는 구조로 선언된 것이다.
    • T는 타입 파라미터로,
      • String이 될 수도, Integer가 될 수도 혹은 유저 정의 클래스가 들어올 수도 있다.
    • T가 어느 타입인지 결정되면,
      • 아래 메서드 add와 get의 파라미터 타입, 리턴 타입이 결정되는 것이다.



다이아몬드 문법

  • 제네릭 타입을 갖는 객체를 선언할 때
    • new 연산자로 인스턴스를 생성하는 오른쪽 구문에는
    • 타입 파라미터를 또 적을 필요가 없다.
    • 이것을 타입 유추라고 한다.
  • 타입을 명시하는 왼쪽 구문에서 명확한 제네릭 타입을 정의했다면,
    • 컴파일러가 인스턴스의 타입을 유추할 수 있기 때문에
    • 오른쪽 구문에서 또 써줄 필요가 없다.
1
2
3
4
5
// 이렇게 써줘도 된다.
MyList<String> strings = new MyList<String>();

// 그러나 이렇게 생략해도 된다.
MyList<Integer> integers = new MyList<>();





Type Parameter

  • 위 예시 코드에서 봤던 를 타입 파라미터라고 한다.
  • 타입 파라미터를 가지는 타입을 선언할 때,
    • 추정하는 형태로 선언해서는 안된다.
    • List< E > dogs = new ArraysList<>(); - X
    • List< String > dogs = new ArraysList<>(); - O
    • 이렇게 구체적인 값을 할당해야 한다.
    • 타입 파라미터는 참조형 타입이어야 한다. 원시형 타입은 올 수가 없다.
  • 타입 파라미터는 실제 타입처럼 메서드 시그니처와 본문에서 사용할 수 있다.
    • 위의 MyList 예시를 보면,
      • 타입 파라미터인 T를 리턴값으로 잡기도 하고,
      • 파라미터의 타입으로 받기도 한다.



타입 삭제

  • jdk 5부터 Generic이 생겼다.
    • 이 때문에 이전의 non-generic 컬렉션들과의 호환이 문제였다.
    • non-generic 컬렉션들은 row 타입 컬렉션이라고도 한다.
      • 현재도 컴파일 에러가 없는 합법적인 자바 코드이다.
      • 물론 좋지 못한 코드이다.
  • 기존의 non-generic 컬렉션들과 새로운 제네릭 컬렉션들을 함께 사용할 수 있는 방법을 찾을 필요가 생겼다.
    • 이 설계에 대한 문제 해결 방법으로 사용한게 타입 캐스팅이었다.
1
2
3
4
5
// 기존의 non-generic 컬렉션
List oldList = new ArrayList();

// 타입 캐스팅으로 변환
List<String> newList = (List<String>) oldList;
  • 이 예시에서 List를 List으로 타입 캐스팅했다는 것은
    • 두 타입이 어느 정도 호환된다는 것을 의미한다.
    • 자바는 타입 삭제를 이용해 이런 호환성을 유지할 수 있었다.
  • 웬 타입 삭제?
    • 제네릭 타입 파라미터는 컴파일 시점까지만 볼 수 있으며,
    • 컴파일러에 의해 삭제되고
    • 런타임 시점 즉, 바이트코드에는 반영되지 않는다.
1
2
3
4
5
6
7
interface MyInterface {

    void printList(List<Integer> integerList);

    void printList(List<String> stringList);

}
  • 위의 예시는 얼핏 보면 그냥 오버로드로 보인다.
    • 그러나 컴파일 에러가 발생한다.
  • 두 printList 메서드가 받는 파라미터는 컴파일 시점에 타입 파라미터가 삭제된다.
    • 결국엔 두 메소드 모두 파라미터로 raw type인 List를 받는 것으로
    • 완전 똑같은 모양이 되어버린다.
    • void printList(List list); 이렇게



Bounded Type Parameter

  • 타입 파라미터로 제한된 경계 내에서 타입을 받고 싶은 경우가 있다.
  • 예를 들어,
    • < T >가 Number의 서브클래스 였으면 한다.
1
2
3
4
interface Box<T> {
    void box(T t);
    T unbox();
}
  • 이 경우는 Box의 타입 파라미터로 모든 참조형 타입이 올 수 있다.
  • 근데 나는 타입 파라미터로 숫자를 나타내는 타입이 왔으면 좋겠다.


1
2
3
4
5
6
7
8
interface NumberBox<T extends Number> {
    void box(T t);
    T unbox();

    default int getInt(T t) {
        return t.intValue();
    }
}
  • 그렇다면 이렇게 타입 파라미터의 경계를 설정해줄 수 있다.
    • Number와 호환되는 클래스들 즉, Number나 Number의 서브클래스들만이
    • NumberBox의 타입 파라미터로 올 수 있다.
  • 예시의 getInt()를 보면 intValue() 메서드를 사용한걸 볼 수 있다.
    • 원래의 타입 파라미터라면 어떤 타입이 올지 모르기 때문에 메서드를 호출할 수 없다.
    • 그러나 경계를 정한 타입 파라미터는 최소 Number의 메서드를 사용할 수 있기 때문에 저렇게 코드를 짤 수가 있다.



1
2
NumberBox<Integer> intBox = new NumberBox<>();
NumberBox<String> strBox = new NumberBox<>(); // 컴파일 에러 발생
  • 만약 타입 파라미터의 경계가 < T extends Number >로 정해진 NumberBox를
  • 경계에서 벗어나는 타입으로 인스턴스화하려 한다면 컴파일 에러가 발생한다.


1
2
3
NumberBox numBox = new NumberBox(); // raw type
numBox.box("babo"); // 경계에서 벗어난 값이 들어갈 수 있다.
numBox.getInt(); // 런타임 에러 발생
  • 경계를 정한 타입 파라미터 객체를 raw type으로 만드는 것은 주의해야 한다.
  • raw type으로 만들면 경계에서 벗어난 값들을 얼마든지 넣을 수 있다.
    • 이래도 컴파일 에러나 런타임 에러가 발생하진 않는다.
  • 그러나 < T extends Number >라고 예상하고 짜여진 다른 메서드들을 수행할 때 런타임 에러가 발생할 것이다.





Introducing Covariance

  • 어.. 일단 할당에 관한 문제이다.
  • 3가지 성질이 있다.
    • Covariance
      • 상위 클래스가 하위 클래스를 할당받을 수 있다.
    • Contravariance
      • 하위 클래스가 상위 클래스를 할당받을 수 있다.
    • Invariance
      • 서로 할당할 수 없는 변하지 않는 성질이다.
  • 용어가 너무 헷갈려서 일단 바로 예시를 본다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
String str = new String();
Object obj = new Object();

str = obj; // X
obj = str; // O

String[] strArr = new String[0];
Object[] objArr = new Object[0];

strArr = objArr // X
objArr = strArr // O

List<? extends String> strings = new ArrayList<>();
List<? extends Object> objects = new ArrayList<>();

strings = objects;
objects = strings;

Covariance

  • 우리가 알고 있는 가장 일반적인 할당 흐름이다.
    • 부모 타입은 자식 타입 인스턴스를 할당받을 수 있고,
    • 자식 타입은 부모 타입 인스턴스를 할당받을 수 없다.


1
2
3
4
5
List<String> stringList = new ArrayList<>();
List<Object> objectList = new ArrayList<>();

stringList = objectList;  // X
objectList = stringList;  // X

Invariance

  • 일반적인 제네릭 타입 객체에서의 할당 흐름이다.
    • 수퍼 클래스, 서브 클래스 할 거 없이 서로 할당받을 수 없다.
    • 변하지 않는 성질이다.


1
2
3
4
5
List<? super String> strings = new ArrayList<>();
List<? super Object> objects = new ArrayList<>();

strings = objects;
objects = strings;

Contravariance

  • 우리가 알던 흐름과는 정반대이다.
    • 부모 타입은 자식 타입을 할당받을 수 없고,
    • 자식 타입은 부모 타입을 할당받을 수 있다.



Wildcard

  • 제네릭 타입 클래스는 타입을 구체적으로 정해주지 않으면 인스턴스화 할 수 없다.
    • ArrayList< T >의 객체를 만들 수가 없다는 뜻이다.
    • ArrayList< String > 이렇게 구체적으로 정해줘야 만들 수 있다.
  • 그렇다면 만약 구체적인 타입을 모를 때는 어떻게 해야할까
    • 위에서 봤듯이 Object를 타입 파라미터로 사용하는 것은 실패할 것이다.
  • 그럴 때 사용하는 것이 와일드카드이다.
    • List< ? > 이렇게 물음표를 사용하는 것을 말한다.
    • < ? > 얘는 < T > 이런 애들이랑 달리 변수가 가질 수 있는 완전한 타입이다.


1
2
3
4
5
6
7
8
9
10
11
List<?> list = getStringList(); "a", "b", "c"

for(Object o : list) {
    System.print.out(o); // "a", "b", "c"
}

list = getIntList(); // 1, 2, 3

for(Object o : list) {
    System.print.out(o); // 1, 2, 3
}
  • 이 코드는 가장 간단한 와일드카드의 예시이다.
  • 타입 파라미터로 구체적인 타입을 알지 못할 때 < ? >를 사용할 수 있다.
  • 타입 파라미터로는 참조 타입이 와야하고, 모든 참조 타입은 Object를 상속받기 때문에
    • 리턴값은 Object를 사용하면 아무 문제가 없다.
    • 그러나 반대로 이런 상황은 안된다.
      • 이 예시의 코드에서 list.add(new Object()); 이건 컴파일 에러다.
      • 위에서 살펴본 List< String >에 Object 객체를 넣을 수 없는 것과 마찬가지이다.
      • < ? >로 어떤 타입이 올지 모르니 Object를 넣을 수 없다는 것이다.
      • < ? > 타입에 막 넣을 수 있는 것은 null이 유일하다. null은 모든 참조 타입에 할당할 수 있으니까~



Bounded Wildcard

  • 그냥 와일드카드는 너무너무 광범위하다.
  • 경계를 나눈 와일드카드를 통해 타입의 상속 구조를 설명할 수 있다.
    • < ? extends Number >
    • 뒤에 오는 타입이 class든 interface든 extends 키워드를 사용한다.
  • 이를 통해 Number의 하위 클래스라는 것을 알 수 있다.



Type Variance

  • 컨테이너 타입의 상속과 페이로드 타입의 상속 간의 어떤 관련이 있는지에 대한 이론이다.
  • Type Covariance
    • 컨테이너 타입이 페이로드 타입과 동일한 관계를 갖는다는 의미이다.
      • 그냥 우리가 아는 일반적인 흐름과 같다는 뜻이다.
    • extends 키워드를 사용한다.
  • Type Contravariance
    • 컨테이너 타입이 페이로드 타입과 반대되는 관계를 갖는다는 의미이다.
      • 그냥 우리가 아는 일반적인 흐름과 반대라는 뜻이다.
    • super 키워드를 사용한다.


Covariance

1
2
3
4
5
List<? extends String> strings = new ArrayList<>();
List<? extends Object> objects = new ArrayList<>();

strings = objects; // X
objects = strings; // O
  • 우리가 아는 자연스러운 할당 흐름이다.
    • 상위 타입이 하위 타입을 할당받을 수 있고,
    • 하위 타입이 상위 타입을 할당받을 수 없다.
  • 이게 살짝만 생각해보면 당연한게
    • 하위 타입을 상속받은 ? 타입은
    • 결국 상위 타입도 상속받은 애니까..


Contravariance

1
2
3
4
5
List<? super String> strings = new ArrayList<>();
List<? super Object> objects = new ArrayList<>();

strings = objects; // O
objects = strings; // X
  • 우리가 아는 흐름이랑 정반대이다.
    • 상위 타입이 하위 타입을 할당받을 수 없고,
    • 하위 타입이 상위 타입을 할당받을 수 있다.
  • 이건 조금 이해가 안감..
    • 아직 이해가 더 필요하다 얘는





This post is licensed under CC BY 4.0 by the author.