JAVA 지네릭스 (Generics)란?
컴파일시 타입을 체킹하자 - 지네릭스
목차
- 지네릭스란?
- 타입변수
- 지네릭스 용어
- Iterator, HashMap 과 지네릭스
- 제한된 지네릭 클래스
- 와일드 카드 <?>
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());
이것이 가능한 이유는 지네릭 타입 __
지정전
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판