제네릭, 제네릭 레퍼런스, 제네릭 파라미터
제네릭
public static Object echo(Object obj) {
return obj;
}
echo() 리턴 타입이 Object이기 때문에 String 레퍼런스로 바로 받을 수 없다. 해결책? 형변환 해야 한다.
String obj2 = (String) echo(new String("Hello"));
형변환의 불편함을 해소하려면 에코를 파라미터별로 오버로딩 해야한다.
public static String echo(String obj) {
return obj;
}
public static Date echo(Date obj) {
return obj;
}
public static Calendar echo(Calendar obj) {
return obj;
}
형변환을 할 필요는 없지만 중복코드를 여러개 만들어야하는 불편함이 있다.
형변환을 하지 않는 이점을 모두 취할 수 있는 문법이 "제네릭(generic)"이다.
제네릭을 이용하면 여러 타입을 모두 수용할 수 있는 메서드를 만들 수 있다.
<다루는 타입의 별명> 리턴타입 메서드명(다루는_타입_별명 파라미터, ...) {...}
<What> What echo(What obj) {
return obj;
}
What 은 클래스나 인터페이스 이름이 아니다.
메서드가 호출될 때 What 타입이 결정된다는 의미이다. => "타입(을 받는) 파라미터"
형변환의 불편함, 코드 중복의 불편함을 한번에 없애주는 문법이 제네릭인 것이다!
제네릭으로 지정한 타입이 아닌 것을 넣으려 하면 컴파일 오류가 발생한다. 이것이 제네릭을 사용하는 이유이기도 하다.
즉 특정 클래스만 다루도록 제한할 수 있다.
Box<Date> b4 = new Box<>();
b4.set("Hello!"); // 컴파일 오류
여러 개의 타입 파라미터를 지정하기
class A<X,Y,Z> {
X v1;
Y v2;
Z v3;
}
A<String,Integer,Member> obj = new A<>();
obj.v1 = new String("Hello");
obj.v2 = Integer.valueOf(100);
obj.v3 = new Member("홍길동", 20);
제네릭 배열 생성
제네릭의 타입 파라미터로 레퍼런스 배열을 생성할 수 없다.
T[] arr;
arr = new T[10];
--불가능--
- 견본 배열을 받아서 복제하는 방법을 사용한다.
static T[] create2(T[] arr) {
return Arrays.copyOf(arr, 10);
} - 배열의 타입 정보를 받아 생성하기
static T[] create3(Class<?> type) {
return (T[]) Array.newInstance(type, 10);
} - 견본 배열에서 타입 정보를 추출하여 배열을 생성하기
static T[] create4(T[] arr) {
Class<?> arrayTypeInfo = arr.getClass(); // 예) String[] Class<?> arrayItemTypeInfo = arrayTypeInfo.getComponentType(); // 예) String
return (T[]) Array.newInstance(arrayItemTypeInfo, 10);
}
제네릭 레퍼런스
ArrayList<?> list2;
list2 = new ArrayList(); // 이렇게 사용하지 말고, 명확히 제네릭의 타입을 지정하라.
list2 = new ArrayList<>();
list2 = new ArrayList<Object>();
list2 = new ArrayList<String>();
list2 = new ArrayList<Member>();
// list2.add(new String()); // 컴파일 오류!
// list2.add(new Integer(100)); // 컴파일 오류!
// list2.add(new java.util.Date()); // 컴파일 오류!
// list2.add(new Member("홍길동", 20)); // 컴파일 오류!
ArrayList가 다루는 타입에 상관없이 ArrayList 레퍼런스를 선언하고 싶다면,
다음과 같이 명확하게 <?> 를 붙여라!
단 이 경우에는 제네릭의 타입이 명확하게 선언되어 있지 않기 때문에 제네릭 검사가 필요한 코드를 컴파일 할 수 없다.
제네릭 파라미터
static void m1(ArrayList list) {
list.add(new Object());
list.add(new A());
list.add(new B1());
list.add(new B2());
list.add(new C());
}
제네릭으로 선언된 클래스를 사용할 때는 반드시 타입 파라미터 값을 지정하라!
제네릭 문법의 목적은 코드 안정성을 추구하는 것이다.
원하는 타입이 아닌 다른 타입의 값을 지정하는 오류(타입 오류)를 줄이기 위해 만든 문법이다.
제네릭 문법의 대상은 컴파일러다. 즉 컴파일 단계에서 최대한으로 타입 오류를 잡아 내는 것이 목적이다.
static class A {}
static class B1 extends A {}
static class B2 extends A {}
static class C extends B1 {}
m1(new ArrayList());
m1(new ArrayList<Object>());
// m1(new ArrayList<A>()); // 컴파일 오류!
// m1(new ArrayList<B1>()); // 컴파일 오류!
// m1(new ArrayList<B2>()); // 컴파일 오류!
// m1(new ArrayList<C>()); // 컴파일 오류!
m1(ArrayList
m1(new ArrayList<B1>());
m1(new ArrayList<C>()); // 이거 안된다 타입지정이니까
static void m1(ArrayList<B1> list){
list.add(new B1());
list.add(new C());
}
제네릭 자체는 다형적 변수가 적용이 되지 않는다. == 타입을 지정할 때!
메서드 호출 시 다형적 변수 적용은 가능하다.
m1(ArrayList<?>) // => 모든 타입에 대해 ArrayList 객체를 파라미터로 넘길 수 있다. 다만 메서드 내부에서는 타입 검사를 할 수 없기 때문에 add() 메서드 호출 같은 타입 검사가 필요한 코드를 사용한 경우에는 컴파일 오류가 발생한다.
ArrayList<B1> my1 = new ArrayList<>();
my1.add(new B1());
my1.add(new B1());
my1.add(new B1());
my1.add(new C());
m1(my1); // OK
static void m1(ArrayList<?> list) {
list.add(new Object()); // 컴파일 오류
list.add(new A()); // 컴파일 오류
list.add(new B1()); // 컴파일 오류
list.add(new B2()); // 컴파일 오류
list.add(new C()); // 컴파일 오류
}
컴파일러는 파라미터로 받은 ArrayList가 어떤 타입의 값을 다루는 지 알 수 없기 때문에 메서드를 사용할 때, 옳은 타입의 값을 전달하는지 컴파일러는 알 수 없다.
그 타입인지 검사해야 하는 메서드를 사용할 때는 컴파일을 명확하게 해줄 수 없다. 따라서 컴파일 오류를 발생시킨다.
즉 제네릭에 대한 타입 검사가 필요한 메서드를 호출할 때는 타입 파라미터의 값이 지정되어 있지 않기 때문에 add() 메서드의 파라미터 타입을 알 수 없다. 그래서 add() 를 호출할 때 어떤 타입이 유효한 문법인지 알 수 없어서 컴파일 할 수 없다.