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의 파라미터 타입, 리턴 타입이 결정되는 것이다.
- T는 타입 파라미터로,
다이아몬드 문법
- 제네릭 타입을 갖는 객체를 선언할 때
- 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를 리턴값으로 잡기도 하고,
- 파라미터의 타입으로 받기도 한다.
- 위의 MyList 예시를 보면,
타입 삭제
- 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
- 서로 할당할 수 없는 변하지 않는 성질이다.
- Covariance
- 용어가 너무 헷갈려서 일단 바로 예시를 본다.
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.