JAVA - Generic
Oracle의 자바 튜토리얼 페이지의 일부를 발췌하여 설명을 더하거나 빼고 쓴 글입니다.
내가 만드는 제네릭 : 제네릭 타입
generic class는 class name<T1, T2, ..., Tn> { /* ... */ }과 같은 형식으로 정의합니다.
class 이름 다음에 <>안에 type parameter(T1, T2…)들이 옵니다. 이 자리에는 어떤 type, class, interface, 심지어는 다른 사용자 정의 type까지 상관 없이 들어올 수 있습니다.
관용적으로 type parameter는 대문자 한글자가 오는데, 아래와 같은 글자가 많이 쓰입니다.
- E - Element (used extensively by the Java Collections Framework)
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
예시로 타입 T를 담을 수 있는 상자 class를 만들어보도록 하겠습니다.
public class Cart<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
이 Cart는 Cart<Bread> breadCart = new Cart<Bread>();와 같은 방식으로 인스턴스로 만들어 사용할 수 있습니다 Java SE 7 이후로는 Cart<Bread> breadCart = new Cart<>();와 같이 써도 됩니다.
내가 만드는 제네릭 : 제네릭 메서드
제네릭 메서드를 만드는 것은 제네릭 타입을 만드는 것과 비슷합니다. 단, type parameter의 범위는 메서드 안으로 제한됩니다. static 함수와 non-static 함수를 모두 만들 수 있으며 generic class의 생성자도 만들 수 있습니다.
public class Util {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
이 예제에서 제네릭 타입 Pair<K, V>의 생성자 public Pair(K key, V value)와 Util클래스의 public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) 메서드는 둘 다 제네릭 메서드입니다. 이 둘을 통해 어떤 타입의 key든 받아 어떤 타입이든 value를 저장할 수 있는 Pair class와 생성된 Pair 인스턴스를 비교할 수 있는 compare 메서드를 만들 수 있습니다. compare 함수의 리턴 타입 앞에 <K, V>가 써져있는 것을 확인하세요.
Type Parameter에 제한 걸기
Type Parameter에 특정 타입을 상속받은 타입들만 올 수 있도록 제한을 할 수 있습니다. 우리의 카트 class가 앞으로 신선 식품만 담을 수 있게 제한해보겠습니다.
public class Cart<T extends FreshProduct> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
Parameter는 여러개의 제한을 가질 수 있습니다. 이 때는 <T extends B1 & B2 & B3>와 같은 형태로 사용합니다. 이 때 B1, B2, B3 의 순서는 class가 먼저, interface가 나중에 와야합니다. 만약 interface가 먼저 오고 나서 class가 온다면 컴파일 에러가 발생합니다.
제네릭 메서드에서도 Type parameter를 한정시킬 수 있습니다. 이는 타입에 관계 없이 사용할 수 있는 generic method를 만들 때 핵심이 됩니다. 배열 anArray에서 특정 값 elem보다 더 큰 요소의 갯수를 세는 함수를 만든다고 가정해보겠습니다. 아래와 같이 loop를 돌면서 각 요소들과 elem을 비교해야하는데, 그냥 제네릭으로만 정의하면 비교연산 >이 가능한지가 보장되지 않기 때문에 컴파일에러가 발생합니다.
public static <T> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e > elem) // compiler error
++count;
return count;
}
비교 연산이 가능한 타입만 T로 들어올 수 있도록 하기 위해 Comparable<T>인터페이스로 제한을 하면 컴파일 에러가 사라집니다.
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e.compareTo(elem) > 0)
++count;
return count;
}
제네릭의 와일드카드
제네릭에서 물음표 ?는 와일드카드로 사용됩니다. 와일드카드를 parameter, field, 지역 변수, 리턴 타입 등 다양한 곳에 사용할 수 있습니다. The wildcard is never used as a type argument for a generic method invocation, a generic class instance creation, or a supertype.
Upper bounded wildcard
위에서 Cart가 담을 수 있는 것을 신선 상품으로 한정했었는데요, Upper Bounded Wildcard는 비슷한 동작을 하게 만듭니다. public static void process(List<? extends Foo> list) { /* ... */ }라는 함수가 있다고 가정해보면, process 함수는 Foo나 Foo를 상속받는 class를 요소로 가지는 리스트를 인자로 받습니다. 그래서 for (Foo elem : list) { /* ... */ }와 같이 사용할 수 있습니다. 예를 들어 public static double sumOfList(List<? extends Number> list)라는 함수는 숫자를 상속받는 타입을 인자로 받아서 모두 더해 double을 리턴하는 함수입니다. 자연수, 정수, 실수 상관 없이 받아서 double로 리턴할 수 있게 됩니다.
Unbounded wildcard
unbounded wildcard는
- Object class에 있는 것 만으로 메서드를 만들 때
- type parameter와 관계 없는 것만으로 메서드를 만들 때
사용합니다.
예를들어 아래와 같은 메서드는 리스트 안에 어떤 타입이 들어있든 메서드가 해당 타입에 관계 없이 도착하기 때문에 unbounded wildcard를 사용하게 됩니다. 비슷한 경우로 List의 size나 clear 메서드가 있습니다.
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}
Lower bounded wildcard
Lower bounded wildcard는 upper bounded wildcard와 반대로 동작합니다. <? super A>와 같은 형태로 특정 class가 상속받은 base class들만 들어오게 한정할 수 있습니다. 여기서 주의 할 것은 upper bounded wildcard와 lower bounded wildcard는 동시에 사용할 수 없다는 것입니다. 생각해보면 당연합니다. 두개가 동시에 적용이 된다면 결국 특정 타입 하나만 쓰는 것과 다르지 않을테니까요.
제한
제네릭을 만들 때의 제한 사항들.
- Primitive type으로 Generic Type을 만들 수 없다. : 예를 들어
Pair<int, char>는 불가.Pair<Integer, Character>는 가능 - Type Parameter로 인스턴스를 생성할 수 없다. 예를들어
public static <E> void apend(List<E> list)라는 함수 안에서E elem = new E();와 같이 사용할 수 없다. - Type parameter로 static field를 정의할 수 없다.
- Parameterized 된 type들을 캐스팅하거나
instanceof를 사용할 수 없다. - Parameterized 된 type들로 Array를 만들 수 없다. : 예를 들어
List<Integer>[] arrayOfLists = new List<Integer>[2];와 같이 사용할 수 없다. 이렇게 쓸 수 있다면stringLists[1] = new ArrayList<Integer>();와 같이 활용할 수 있게 되는데 강력한 type check가 목적인 generic과도 Java와도 어울리지 않는다. - Parameterized 된 type으로 Exception을 만들 수 없다. : 예를 들어
class MathException<T> extends Exception { /* ... */ }와 같이 사용할 수 없다. - Generic 상태에서 Parameter Type이 같은 함수들은 오버로딩이 불가능하다. : 예를들어 같은 클래스 안에
public void print(Set<String> strSet)와public void print(Set<Integer> intSet)를 동시에 정의할 수 없다. type eraser가 일어난 후에 둘의 파라미터가Set<Object>로 동일하기 때문에 구분할 수 없다.
Todo
- 사용 이유 옮기기
- Erasure
- Restrictions 더 길게