제네릭(Generic)
은 JDK 1.5에서 추가된 데이터 타입을 일반화시키는 기능입니다. 풀어서 이야기하자면 제네릭은 코드를 작성할 때 타입을 파라미터의 형태로 정의해두고 실제 사용할 때 파라미터를 구체적인 타입으로 바꾸어서 사용합니다. 즉, 컴파일 타임에 타입 체크를 하게 만들어 준다라는 것 입니다.
제네릭을 사용했을 때의 장점은 다음과 같습니다.
제네릭에서 사용하는 타입을 타입 파라미터라고 부릅니다.
타입 파라미터을 사용할 땐 꺽쇠< >
안에 T, E, K, V
를 사용합니다. 각각 Type, Element, Key, Value
에서 첫 글자를 따온 것이고 일반적으로는 T
를 가장 많이 사용합니다. 어떤 기호를 사용해도 참조형 타입을 의미하기 때문에 아무거나 사용하여도 됩니다.
제네릭이 등장하기 전에는 Object 타입의 참조 변수를 이용했었습니다. 이 경우에는 Object 타입에서 다른 타입으로 캐스팅이 필수적이었지만 제네릭이 등장하면서 이런 수고를 덜 수 있게되었습니다.
제네릭을 선언하는 코드입니다.
public class G <T>{
public T x;
}
클래스 선언부에 제네릭 타입 파라미터 <T>
를 함께 선언함으로써 해당 클래스에서 타입 선언이 오는 자리에 T
를 사용할 수 있음을 알립니다. 변수 x
는 당장의 타입이 뭔진 모르지만 T를 따르겠다라는 의미로 해석하시면 됩니다.
이렇게 선언된 제네릭은 객체를 생성할 때 파라미터 T
의 타입을 결정해서 전달하게 됩니다. 이때 파라미터 T
에는 클래스, 인터페이스 타입만이 올 수 있습니다.
public class Main {
public static void main(String[] args) {
G<String> g = new G<String>();
g.x = "제네릭 사용해보기";
System.out.println(g.x);
}
}
public class Main {
public static void main(String[] args) {
G<Integer> g = new G<Integer>();
g.x = 1234;
System.out.println(g.x);
}
}
같은 클래스로 부터 같은 코드를 실행했을 뿐인데 파라미터 T로 전달하는 타입에 따라서 변수 x
의 결과가 다르게 나타나죠?
만약 위의 두 코드처럼 변수 선언 타입과 생성자 호출 타입이 동일하다면 생성자 호출에서는 타입을 생략하고 <>
만 적을수도 있습니다
G<Integer> g = new G<>();
제네릭 타입
은 결정되지 않은 타입을 파라미터로 갖는 클래스, 인터페이스를 의미합니다. 당장 위에서 제네릭을 설명하기 위해 만든 클래스 G
가 제네릭 타입 클래스라고 할 수 있습니다.
// <>내부에는 하나 이상의 파라미터가 올 수 있습니다.
public class 클래스명<T, ...> {}
public interface 인터페이스명<T, ...> {}
여러 타입을 이용하기 위해 하나 이상의 파라미터를 이용할 수 있습니다.
public class G <T, E, K>{
public T x;
public E y;
public K z;
}
public class Main {
public static void main(String[] args) {
G<String, Integer, Boolean> g = new G<String, Integer, Boolean>();
g.x = "제네릭 타입 알아보기";
g.y = 1234;
g.z = true;
System.out.println(g.x + "\n" + g.y + "\n" + g.z);
}
}
여러 파라미터가 선언된 제네릭 타입은 순서가 있습니다. 변수
z
는<Boolean>
과 매칭되므로 다른 값을 넣으면 오류가 발생합니다.
제네릭 메소드
는 타입 파라미터를 가지고 있는 메소드를 의미합니다. 타입 파라미터는 리턴타입의 바로 앞에 선언해줍니다. 이렇게 선언한 타입 파라미터를 리턴타입과 매개변수에 사용할 수 있습니다.
public <T, ...> 리턴타입 메소드명(매개변수, ...) {}
다음 코드는 제네릭 메소드 예제 코드입니다. 어렵게 가지않고 가장 쉽게 만들 수 있는 getter/setter를 예시로 사용했습니다.
public class G <T>{
private T x;
public T getX() {
return x;
}
public void setX(T x) { //반환할 내용이 없으므로 리턴타입은 T가 아닌 void
this.x = x;
}
}
public class Main {
public static void main(String[] args) {
G<String> g1 = new G<>();
g1.setX("제네릭 메소드");
System.out.println(g1.getX());
G<Integer> g2 = new G<>();
g2.setX(1234);
System.out.println(g2.getX());
}
}
어떤 경우에서는 타입 파라미터로 오는 타입을 제한할 필요가 있습니다. 예를들면 사칙 연산에 사용되는 제네릭 클래스의 같은 경우에는 정수, 실수형 타입만 오도록 제한해야하죠.
제네릭에선 이러한 경우를 위해 제한된 타입 파라미터(Bounded type parameter)
라는 기능을 지원하고 있습니다. 물론 아무 타입이나 제한할 수 있는 것은 아니고 상위의 클래스나 인터페이스만을 지정할 수 있습니다.
다음과 같이 파라미터에 extends
키워드를 이용해서 정의합니다.
<T extends 상위타입>
제한된 타입 파라미터를 사용한 예제입니다.
public class G <T>{
public static <T extends Integer> void genericMethod(T x) {
System.out.println(x);
}
}
public class Main {
public static void main(String[] args) {
G.genericMethod(1234); //정상 출력 1234
G.genericMethod("제한된 타입 파라미터"); //오류!!! String 타입은 extends 되지 않음
}
}
extends에 명시된 상위 타입이 아닌 다른 타입을 넣으니 오류가 발생하는 모습입니다.
와일드카드 타입 파라미터
는 매개변수나 반환값 타입으로 제네릭 타입을 사용할 때 타입 파라미터로 사용할 수 있습니다. 와일드카드 타입 파라미터를 사용하면 선택한 범위에 따라 포함되는 모든 타입을 이용할 수 있습니다. 와일드카드 타입 파라미터는 ?
기호를 이용해 명시합니다.
범위를 지정할 때 super
와 extends
키워드를 선택할 수 있는데 super
는 자기 자신과 조상을 포함하고, extends
는 자기 자신과 자손들을 포함합니다. 만약 지정없이 와일드카드 기호 ?
만 사용하면 모든 타입을 의미하게 됩니다.
제네릭타입<? super 타입> //자기 자신과 조상타입
제네릭타입<? extends 타입> //자기 자신과 자손 타입
제네릭타입<?> //모든 타입 가능