
제네릭은 클래스, 인터페이스 및 메서드를 정의할 떄 유형 (클래스 및 인터페이스)이 매개 변수가 되도록 한다. 메서드 선언에 사용되는 보다 친숙한 형식 매개 변수와 마찬가지로 형식 매개 변수는 다른 입력으로 동일한 코드를 재사용 할 수 있는 방법을 제공한다.
차이점은
매개 변수에 대한 입력은 : 값
유형 매개 변수에 대한 입력은 : 유형
이라는 것.
제네릭을 사용하는 코드는 제네릭을 사용하지 않는 코드에 비해 많은 이점을 가진다.
컴파일 타임에 더 강력한 타입 검사가 가능하다.
Java 컴파일러는 제네릭을 사용하지 않는 일반 코드에 강력한 타입 검사를 적용하고 코드가 안정성을 위반할 경우 오류를 발생시킨다. 컴파일 오류는 런타임 오류를 수정하는 것 보다 쉽기 때문에 좋은 이점을 가진다.
제네릭을 사용하면 타입 캐스팅을 제거할 수 있다.
List list = new ArrayList();
list.add("hello");
String s = (String)list.get(0);
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); //no cast
타입 캐스팅을 하지 않아도 정상적으로 사용 가능하다.
: 프로그래머가 제네릭을 사용하여 다양한 유형의 컬렉션에서 작동하고 사용자 정의가 가능하며, 유형에 안전하고 읽기 쉬운 제네릭 알고리즘을 구현할 수 있다.
public class Box {
private Object object;
public void set(Object object) {
this.object = object;
}
public Object get() {
return object;
}
}
Box의 필드 object는 타입이 모든 클래스의 상위 타입인 Object이기 때문에 모든 클래스의 정보를 가질 수 있다. 하지만 해당 필드를 get/set으로 잘못 꺼내서 쓰거나 저장할 경우 런타임 시에 타입이 맞지 않는 오류가 발생할 수 있다.
public clas Exam01 {
public static void main(String[] args) {
Box box = new Box();
box.set(10)p; //정수
String s = (String)box.get() //실수로 String 을 get
}
}
ClassCastException이 발생한 것을 알 수 있다.
위의 Box 클래스를 제네릭 클래스로 정의하면
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
제네릭 클래스는 다음과 같은 형식으로 정의할 수 있다.
클래스 이름 <T1, T2, ..., Tn> {/*...*/}
<>로 구분된 매개 변수는 클래스 이름 뒤에 온다. 해당 괄호에는 지정할 타입 피라미터를 넣을 수 있다. 또한 인터페이스에서도 동일하게 적용이 가능하다.
public class Exam02{
public static void main(String[] args){
Box<Integer> box = new Box<>();
box.set(10);
Integer integer = box.get();
}
}
단일 대문자로 작성한다. 해당 규칙을 적용하면 일반 클래스 혹은 인터페이스의 이름의 차이를 구분하여 직관적으로 사용할 수 있다.
제네릭 클래스인 Box에 구체적인 타입을 선언하기 위해서는 <>를 활용해야 한다.
Box<Integer> box;
다른 변수선언과 비슷한 모양을 가지고 있고, 단순히 Integer 타입을 가지는 Box에 대한 참조 변수를 선언한 것이다. 이 클래스를 인스턴스화 하기 위해서는 동일하기 new 키워드를 사용한다.
Box<Integer> box = new Box<Integer>();
Java7 부터는 클래스의 생성자를 호출하는데 필요한 타입 인수를 빈 유형으로 바꿀 수 있다. 해당 <>을 다이아몬드라고 부른다.
Box<Integer> box = new Box<>();
생략하여도 적절히 타입 추론되어 인스턴스화 되는 것을 알 수 있다.
만약 type 매개변수에 제한을 두고 싶다면 다음과 같이 사용 가능
public class Box<T> {
private T t;
public void set(T t) {this.t = t;}
public T get() { return t; }
public<U extends Number> void print(U u){
System.out.println(t.getClass());
SYstem.out.println(u.getClass());
}
}
print메소드의 type 매개변수를 extends 키워드를 사용하여 Number로 제한하였다. 해당 U에는 Integer, Float 등등의 클래스만 허용된다.
와일드 카드를 표현하는 ?는 알 수 없는 타입을 나타낸다. 와일드 카드는 다양한 상황에서 사용할 수 있다. 매개변수, 필드 혹은 지역 변수, 때때로 반환 타입에도 사용된다.
하지만 와일드 카드는 제네릭 메소드 호출, 제네릭 클래스 인스턴스 생성 또는 슈퍼 타입의 타입 인수로는 사용되지 않는다.
public static double sumOfList(List<? extends Number> list){
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}
public static void printList(List<Object> list){
for(Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
하지만 printList는 Object List의 목록한 매개변수 목록으로 받을 수 있기 때문에, Double, Integer 등 타입의 list를 전달하면 컴파일 오류를 발생시킨다.
import java.util.ArrayList;
import java.util.List;
public class Exam06 {
public static void main(String[] args) {
List<Double> list = new ArrayList<>();
list.add(10.0);
list.add(11.0);
list.add(12.0);
printList(list);
}
public static void printList (List<?> list) {
for (Object elem : list)
System.out.print(elem + " ");
System.out.println ();
}
}
printList를 위와 같이 수정하여 작성하면 모든 List에 대응이 가능하다.
import java.util.ArrayList;
import java.util.List;
public class Exam07 {
public static void main(String[] args) {
List<Integer> integers = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
integers.add(10);
numbers.add(10.0);
printList(integers);
printList(numbers);
}
public static void printList(List<? super Integer> list) {
for (Object o : list)
System.out.println(o.getClass());
}
}
Integer 타입의 super type들이 허용되는 것을 알 수 있다.
메소드의 선언부에 제네릭 타입이 선언된 메소드를 제네릭 메소드라고 한다.
위에 만들었던 printList를 제네릭 메소드로 변경하면
import java.util.ArrayList;
import java.util.List;
public class Exam08 {
public static void main(String[] args) {
List<Double> list = new ArrayList<>();
list.add(10.0);
list.add(11.0);
list.add(12.0);
printList(list);
}
public static <T extends Number> void printList(List<T> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
}
기존의 printList와 동일하게 적용된다. 매개변수의 타입이 복잡할 때 직관적으로 확인이 가능하기 때문에 유용하게 사용이 가능하다.
Java에 제네릭이 도입되어 컴파일 타임에 엄격한 타입 검사를 지원한다.
제네릭을 구현하기 위해 Java컴파일러는 다음과 같은 type erasure를 제공한다.
type erasure는 매개 변수화된 타입에 대해 새 클래스가 생성되지 않도록 한다.
결과적으로 제네릭은 런타임 오버 헤드를 발생 시키지 않는다.
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}