[Java] 제네릭(generic) 정리
제네릭이란 클래스 내부에서 사용할 데이터 타입을 외부에서 정할 수 있게 하는 기법으로 잘못된 타입이 사용될 수 있는 문제를 컴파일 과정에서 고칠 수 있게 해준다. 주로 Collections에서 많이 쓰이며 Java 5 버전에 추가되었다.
우선 간단하게 사용법을 보자
List<String> list = new ArrayList<String>();
list.add("S");
list.add(1); // 에러
String s = list.get(0);
String i = list.get(1); // 에러 나지 않음
위 코드에서 <String> <<--- 이게 제네릭이다. List에 String Type만 넣을 수 있다고 선언해 놓은 것이다. 그래서 String 변수에 형변환을 하지 않아도 바로 넣을 수 있다.
만약 위에 있는 코드를 아래 코드와 같이 제네릭을 사용하지 않고 데이터를 저장해보자
List list = new ArrayList();
list.add("S");
list.add(1); // 에러 나지 않음
String s = list.get(0); // 에러
String i = (String)list.get(1); // 에러 나지 않음
데이터를 저장할 때 Object 클래스로 저장된다. 그래서 불러올 때 형변환을 하지 않는다면 에러가 난다.
물론 제네릭을 사용하지 않으면 넣고 싶은 Type을 마음껏 넣을 수 있는 장점이 있지만 사용할 때마다 list에 넣은 타입의 수 만큼 타입을 확인해야 하는 불편함이 있다.
그리고 제네릭을 선언할 때는 클래스로 선언해야 하므로 기본형(primitive) 타입으로 선언할 수 없고 Wrapper Class를 사용해야 한다.
List<int> list1 = new ArrayList<int>(); // 에러
List<Integer> list2 = new ArrayList<Integer>(); // 에러 나지 않음
이제 클래스에 제네릭을 사용해보자
public class GenericTest <T> {
T value;
GenericTest(T value) {
this.value = value;
}
public static void main(String[] args) {
GenericTest<String> g1 = new GenericTest<String>("!");
GenericTest<Integer> g2 = new GenericTest<Integer>(1);
System.out.println(g1.value);
System.out.println(g2.value);
}
}
위 코드는 new GenericTest<String> 과 같이 String을 제네릭으로 지정하면 public class GenericTest <T> 에서 T에 String이 들어가고 T value; 는 String value; 와 같게 되는 것이다. 출력 값은 ! 와 1 이다.
인터페이스도 클래스와 마찬가지로 제네릭을 사용할 수 있다.
public interface GenericTest <T> {
T get();
}
이번에는 제네릭을 두 개 써보자
public class GenericTest <T, S> {
T t;
S s;
GenericTest(T t, S s) {
this.t = t;
this.s = s;
}
public static void main(String[] args) {
GenericTest<String, Integer> g1 = new GenericTest<String, Integer>("!", 1);
System.out.println(g1.t);
System.out.println(g1.s);
}
}
위 코드에는 제네릭을 두 개만 사용했지만 몇 개를 작성해도 상관없다. 출력 값은 ! 와 1 이다.
메소드에 사용하고자 하면 아래와 같이 사용하면 된다.
주의할 점은 메소드에 제네릭을 쓰면 해당 메소드 안에서만 사용할 수 있다는 것이다.
public class GenericTest {
T t; // 에러
<T> T setT(T t) {
return t;
}
public static void main(String[] args) {
GenericTest g1 = new GenericTest();
GenericTest g2 = new GenericTest();
System.out.println(g1.setT(1));
System.out.println(g2.setT("1123"));
}
}
만약 제네릭 타입을 제한하고자 하면 상속관계를 통해 제한할 수 있다.
public class GenericTest {
<T extends Number> T setT(T t){
return t;
}
public static void main(String[] args){
GenericTest g1 = new GenericTest();
GenericTest g2 = new GenericTest();
System.out.println(g1.setT(1));
System.out.println(g2.setT("1123")); // 에러
}
}
위 코드에서 <T extends Number>를 보면 되는데 int형, float형 등등 Number 클래스이거나 Number 클래스를 상속받는 자식 클래스만 제네릭 타입으로 사용할 수 있게 한다. 주의할 점은 인터페이스도 extends를 사용한다는 점이다.
아래 코드는 제네릭을 상속받을 때이다.
class test1 <T> {
T t;
}
public class GenericTest <T, S> extends test1<T> {
S s;
public static void main(String[] args) {
GenericTest<Integer, Float> g1 = new GenericTest<Integer, Float>();
g1.s = 1.1F;
g1.t = 1;
}
}
부모 클래스에서 <T>가 있으므로 자식 클래스에서 <T>를 포함해 줘야 한다.
제네릭은 T나 S대신 어떤 문자를 사용해도 상관없다(ex : <a>, <ge>). 하지만 묵시적 약속이 있는데 아래와 같다.
E - Element
K - Key
N - Number
T - Type
V - Value
S - Second
U - Third
V - Fourth
** 자바 7 버전부터는 GenericTest<String, Integer> g1 = new GenericTest<>("!", 1); 와 같이 객체생성 코드에서는 <>로 생략할 수 있다.