정적언어(C, C++, C#, Java)을 다뤄봤다면 제네릭(Generic)에 대해 한 번쯤은 들어봤을 것이다.
특히 자료구조 같이 구조체를 직접 만들어 사용할 때 많이 쓰이기도 하고 매우 유용하기도 하다.
ArrayList나 LinkedList 등을 만들 때 보통
객체<타입> 객체명 = new 객체<타입>(); 같이 만든다. <>안에 타입을 지정해줌으로써 특정 타입만 받아들이게 하는 것이다.
여기서 제네릭(Generic)은 타입이 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것을 의미한다. 즉, 특정 타입을 지정해주는 것이 아닌 필요에 의해 지정할 수 있도록 한다는 것이다.
컴파일 때 해당 타입으로 캐스팅하여 매개변수화 된 유형을 삭제하는 것이다.
제네릭 장점
- 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다.
- 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없다. 즉, 관리하기가 편하다.
- 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다.
통상적으로 많이 사용되는 형식은 아래와 같다. 일반적으로 그렇다는 거지 굳이 <>내부값이 T,E,K,V,N일 필요는 전혀 없다. <Edkve> 여도 상관없다는 뜻이다.
타입 | 뜻 |
<T> | Type |
<E> | Element |
<K> | Key |
<V> | Value |
제네릭 타입과 다형성
- 참조변수와 생성자로 대입된 타입은 일치해야 한다.
- 부모 타입으로 지정된 자료구조나 참조변수에는 자식 클래스가 들어갈 수 있다.
- JDK 1.7부터 타입 추론이 가능해졌다.(생성자 타입추론)
- 대입된 타입이 다른 제네릭 타입간의 형 변환은 불가능하다.(와일드카드는 가능)
제네릭 클래스, 인터페이스 사용예시
public class Test <T, K> {
T haha;
K hoho;
T testMethod(T a) {}
}
public Interface Interface1 <T, K> {
T haha;
K hoho;
}
public class Test2 { ... }
public class Main {
public static void main(String[] args) {
Test<Test2, Integer> a = new ClassName<Test2, Integer>();
}
}
클래스나 인터페이스는 위와 같은 느낌으로 만든다. T와 K는 클래스, 인터페이스 내부에서까지 사용이 가능하다. 예시의 main에서는 Test 객체를 만들어봤는데, 위 경우 T는 Test2가 되고, K는 Integer가 된다.
제네릭은 일반적인 원시 타입(primitive type)은 올 수 없고, 참조타입이 올 수 있다. 원시타입을 써야한다면 Integer 처럼 Wrapper Class를 사용한다.
하지만 위와 같이 제네릭이 사용된 메소드는 static을 붙이면 타입을 특정할 수 없기에 에러가 난다.
제네릭 메소드
static을 붙인 메소드를 두고 싶은 경우를 위해 메소드만 따로 제네릭을 적용시킨 것이 제네릭 메소드이다.
제네릭 메소드는 클래스와는 달리 반환타입 이전에 <> 제네릭 타입을 선언한다.
[접근 제어자] <제네릭타입> [반환타입] [메소드명]([제네릭타입] [파라미터])
static <T> T testGenericMethod(T o) { // 제네릭 메소드
return o;
}
제한된 제네릭
제네릭 타입에 extends 키워드를 사용하면 부모클래스와 그 부모를 상속받는 자식클래스만 대입할 수 있도록 제한할 수 있다.
예를들어, <T extends Animal>이면 Animal과 Animal을 상속받은 자식클래스만 들어올 수 있다. <T extends Number> 이면 숫자만 들어올 수 있는 것이다.
그리고 인터페이스를 구현한 클래스로 제약이 필요하다면 implements가 아닌 extend를 사용해야 한다.
반대로 super 키워드를 사용하면 부모 클래스와 그 부모의 조상클래스만 들어올 수 있도록 제한하는 것이다.
그래서 extends를 상한 경계, super를 하한 경계라고 부른다.
<K extends T> | T와 T의 자손 타입만 가능 (T가 Number, K가 Double이면 가능) (T가 Number, K가 String이면 불가능) |
<K super T> | T와 T의 조상 타입만 가능 (T가 Double, K가 Number이면 가능) (T가 Double, K가 String이면 불가능) |
와일드 카드(<?> - wild card)
와일드카드는 ?로 나타내며 메소드의 매개변수로 제네릭 클래스의 객체를 받을 때 타입을 제한하기 위해 사용한다.
어떤 타입이든 들어올 수 있기 때문에 이를 사용하는데는 상한제한, 하한제한을 걸어 사용하게 된다.
상한제한은 <? extends T>로 표현되며 T와 그 자손들을 구현한 객체들만 매개변수로 할 수 있다.
<? super T>는 하한제한으로 T와 그 부모들을 구현한 객체들만 매개변수의 데이터 타입으로 사용할 수 있다.
<?>는 어떤 객체 타입도 매개변수로 가능하다는 뜻이다.
와일드 카드에는 &을 사용해 여러개의 클래스를 나열할 수 없다.
<?>는 뭔 타입이 오든 상관없는 것이다.
public class ClassName <E extends Comparable<? super E>> { ... }
PriorityQueue(우선순위 큐), TreeSet, TreeMap 같이 값을 정렬하는 클래스 만약 여러분이 특정 제네릭에 대한 자기 참조 비교를 하고싶을 경우 대부분 공통적으로 위와 같은 형식을 취한다.
E 객체의 상위 타입, 즉 <? super E> 을 해줌으로써 위와같은 불상사를 방지할 수가 있는 것이다.
즉, <E extends Comparable<? super E>> 는 쉽게 말하자면 E 타입 또는 E 타입의 슈퍼클래스가 Comparable을 의무적으로 구현해야한다는 뜻으로 슈퍼클래스타입으로 Up Casting이 발생하더라도 안정성을 보장받을 수 있다.
이 부분은 중요한 것이 이후 필자가 PriorityQueue와 TreeSet 자료구조를 구현할 것인데, 이 부분을 이해하고 있어야 가능하기 때문에 조금은 어렵더라도 미리 알아두는 것이 좋다.
<E extends Comparable<? super E>> 에 대해 설명이 조금 길었다. 위 내용을 한 마디로 정의하자면 이렇다.
"E 자기 자신 및 조상 타입과 비교할 수 있는 E"