제네릭은 '클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법'이다
제네릭을 사용하는 코드는 제네릭을 사용하지 않은 코드에 비해 많은 이점이 있다.
ex)
List list = new ArrayList();
list.add("안녕하세요");
//타입 캐스팅
String s = (String) list.get(0);
List<String> list = new ArrayList<String>();
list.add("안녕하세요");
// 캐스팅 없음
String s = list.get(0);
제네릭 타입의 이름 정하기
public class Box {
private Object object;
public void set(Object object) {this.object = object;}
public Object get() {return object;}
}
메서드가 Object를 받거나 반환하게 되면 primitive 타입이 아니라면 원하는대로 자유롭게 전달할 수 있다. 그러나 컴파일 타임에 클래스가 어떻게 사용되는지 확인할 방법이 없다.
사용하는 곳에서 잘못된 타입 캐스팅이 발생할 가능성이 있어 컴파일 단계에서 에러를 찾을 수 없다.
public class Box<T> {
// T는 "타입"을 나타냄
private T t;
public void set(T t) {this.t = t;}
public T get() {return t;}
}
이 클래스로 생성되는 Box 객체는 특정한 타입을 매개변수로 받아 클래스 내부에서 사용하게 된다.
코드 내에서 제네릭 클래스를 참조하려면 T 를 Integer 와 같은 구체적인 값으로 대체하는 제네릭 타입 호출을 수행해야한다.
GenericClass<Type args> var;
인스턴스 생성 부분에서도 선언처럼 <Type args>
를 기입해야 한다.
GenericClass<Type args> var = new GenericClass<Type args>();
Java SE 7 부터 컴파일러가 선언을 살펴본 후 타입을 추론 할 수 있다면 일반 클래스의 생성자를 호출하는 데 필요한 타입 인자를 빈 타입 인자 <>로 바꿀 수 있다.
public class WitchPot<T> {
private T meterial;
public static void main(String[] args) {
//선언부에 Integer로 명시되어 있기 때문에 타입 추론을 통해 다이아몬드로도 객체 생성 가능.
WitchPot<Integer> pot; = new WitchPot<>();
}
}
raw 타입은 제네릭을 사용하지 않았던 과거 자바 버전과의 호환성을 위해서 존재하는 타입 매개변수가 없는 제네릭 타입이다.
raw 타입에 매개변수화된 제네릭 타입을 할당할 수 있다.
public class Box<T> {
private T t;
public void set(T t) {this.t = t;}
public T get() {return t;}
public static void main(String[] args) {
//raw 타입 생성
Box rawBox = new Box();
Box<String> pBox = new Box<>();
//raw type에 parameterized type 대입
rawBox = pBox;
}
}
또는
public class Box<T> {
private T t;
public void set(T t) {this.t = t;}
public T get() {return t;}
public static void main(String[] args) {
Box rawBox = new Box();
//parameterized type에 raw type 대입
Box<String> pBox = rawBox;
System.out.println("OK");
rawBox.set(3);
System.out.println(rawBox.get());
System.out.println("OK");
}
}
위 경우에는 컴파일 단계에서 경고메시지를 받는다. 다만 경고일 뿐 컴파일은 진행된다.
제네릭 메소드는 타입 매개변수를 사용하는 메소드이다. 제네릭 타입을 선언하는 것과 비슷하지만 제네릭 메소드에서 타입 매개변수의 scope는 선언 된 메소드로 제한된다.
제네릭 메소드의 구문에는 메소드의 리턴 타입 전에 나타나는 괄호 안에 타입 매개변수 목록이 포함된다. static 제네릭 메소드의 경우 타입 매개변수 섹션이 메소드의 리턴 타입 전에 나타나야한다.
public <타입 파라미터 . . . > 리턴타입 메소드명 (매개변수, . . . ) { . . . }
public static <타입 파라미터 . . . > 리턴타입 메소드명 (매개변수, . . . ) { . . . }
public class Util {
public static <T> WitchPot<T> put(T t) {
return new WitchPot<>(t);
}
}
public class WitchPot<T> {
private T meterial;
public WitchPot(T meterial) {
this.meterial = meterial;
}
public static void main(String[] args) {
String frog = "개구리";
WitchPot<String> pot = Util.<String>put(frog);
System.out.println(pot.meterial); //개구리
}
컴파일러가 제네릭 메소드의 반환 대상의 타입을 미리 검사하는 타입 추론 기능에 의해서 타입 파라미터는 생략이 가능하다.
public class WitchPot<T> {
private T meterial;
public WitchPot(T meterial) {
this.meterial = meterial;
}
}
Java SE 8부터는 컴파일러의 타입 추론 개념이 확장되어 메소드 인자에 포함된 매개변수화된 타입까지 검사한다.
public class WitchPot<T> {
private T meterial;
public WitchPot(T meterial) {
this.meterial = meterial;
}
public static void main(String[] args) {
String frog = "개구리";
//반환 대상이 WitchPot<String> 인 것을 확인하고 String 으로 추론한다.
WitchPot<String> pot = Util.put(frog);
System.out.println(pot.meterial); //개구리
}
}
제네릭 타입 코드 에서 와일드 카드 라고하는 물음표 ( ? ) 는 알 수 없는 유형을 나타낸다. 와일드 카드는 파라미터 변수, 필드 또는 지역변수의 타입 등 다양한 상황(때때로 리턴 타입에도 사용할 수 있음.)에서 사용할 수 있다. 와일드 카드는 제네릭 메서드 호출, 제네릭 클래스 인스턴스 생성 또는 수퍼 타입의 타입 인자로는 사용될 수 없다.
public class Exam_008 {
public static <T> List<Integer> printTokenSizeList(List<T> list) {
List<Integer> result = new ArrayList<>();
for(T t : list) {
result.add(((String)t).split(" ").length);
}
return result;
}
public static void main(String[] args) {
List<String> myList = new ArrayList<>();
myList.add("테스트");
List<Integer> result = printTokenSizeList(myList);
for(int elem : result) {
System.out.println(elem);
}
}
}
위와 같은 예제에서 printTokenSizeList
안을 보면 String 타입으로 캐스팅 하는 코드가 있다.
만일 매개변수로 List<Integer>
가 전달된다면 런타임 에러가 발생하게 된다
바운디드 타입은 타입 파라미터의 타입을 제한할 수 있다.
바운디드 타입 파라미터를 선언하려면 타입 파라미터의 이름, extends키워드, 상위 바운드를 나열한다.
사용하는 방법은 extends 키워드를 사용해서 다음과 같이 작성해주면 된다.
public class Exam_008 {
public static <T extends String> List<Integer> printTokenSizeList(List<T> list) {
List<Integer> result = new ArrayList<>();
for(T t : list) {
result.add(t.split(" ").length);
}
return result;
}
public static void main(String[] args) {
List<String> myList = new ArrayList<>();
myList.add("테스트");
List<Integer> result = printTokenSizeList(myList);
for(int elem : result) {
System.out.println(elem);
}
}
}
타입 파라미터의 타입을 String으로 제한한 부분과 T 타입의 변수 t 에 대해 String 타입이라는 것을 알고 있기 때문에 더 이상 타입 캐스팅이 의미가 없어 삭제한 부분이다.
이렇게 타입을 제한하는 방법중에 와일드 카드를 사용하는 방법이 있다.
와일드 카드는 보통 '모든 것' 을 뜻하는데 * (별표, asterisk) 또는 ? (물음표) 를 사용하는데, 자바 제네릭 에서는 ? 를 사용한다.
크게 세가지 형태가 존재한다.
<?>
<? extends 상위타입>
<? super 하위타입>
ex)
위와 같을 때, 다음과 같이 사용 가능하다.
public class Exam_011 {
public static void main(String[] args) {
// List 의 요소 타입으로 제한을 두지 않음
List<?> wildcard_test = Arrays.asList(
new Root(),
new Sub_01(),
new Sub_02(),
new Sub_02_Sub(),
new Exam_011()
);
// List 의 요소 타입으로 Sub_02 또는 Sub_02 하위 타입으로 제한
List<? extends Sub_02> wildcard_extends_test = Arrays.asList(
new Sub_02(),
new Sub_02_Sub()
);
// List 의 요소 타입으로 Sub_01 또는 Sub_01 상위 타입으로 제한
List<? super Sub_01> wildcard_super_test = Arrays.asList(
new Root(),
new Sub_01()
);
wildcard_test.forEach(System.out::println);
System.out.println();
wildcard_extends_test.forEach(System.out::println);
System.out.println();
wildcard_super_test.forEach(System.out::println);
}
}
class Root {}
class Sub_01 extends Root {}
class Sub_02 extends Root {}
class Sub_02_Sub extends Sub_02 {}
제네릭의 타입 소거(Generics Type Erasure)
erasure란 원소 타입을 컴파일 타임에서만 검사를하고 런타임에는 해당 타입 정보를 알기 힘들다. 컴파일 상태에만 제약 조건을 적용하고, 런타임에는 타입에 대한 정보를 소거하는 프로세스이다.
List<Object> list = new ArrayList<Integer>(); //compile error
list.add("thewing"); // type 이 일치하지 않아 add가 안된다
이와 같은 상황에서 컴파일 오류를 확인이 가능하다
Java 컴파일러는 타입 소거를 아래와 같이 적용을 한다.
<E extends Comparable>
와 같이 bound를 해주지 않은 경우를 의미한다. 이 소거 규칙에 대한 바이트 코드는 제네릭을 적용할 수 있는 일반 클래스, 인터페이스, 메서드에 적용이 가능하다.public static <E> boolean containsElement(E [] elements, E element){
for (E e : elements){
if(e.equals(element)){
return true;
}
}
return false;
}
실제로 이렇게 선언되어 있는 제네릭 메서드의 경우 선언 방식에 따라 컴파일러가 타입 파라미터 E를 실제 유형의 Object로 변경한다
public static boolean containsElement(Object [] elements, Object element){
for (Object e : elements){
if(e.equals(element)){
return true;
}
}
return false;
}
따라서 컴파일러는 코드의 형식 안정성을 보장하고 런타임 오류를 방지한다.
클래스 수준에서 컴파일러는 클래스의 Type Parameter를 버리고 첫 번째 바인딩으로 대체하거나 Type Parameter가 바인딩 되지 않은 경우 Object로 변환한다
Stack 구현의 예시를 보자
public class Stack<E> {
private E[] stackContent;
public Stack(int capacity) {
this.stackContent = (E[]) new Object[capacity];
}
public void push(E data) {
// ..
}
public E pop() {
// ..
}
}
컴파일시 컴파일러는 바인딩되지 않은 형식 매개변수 E를 Object로 바꾸게된다
public class Stack {
private Object[] stackContent;
public Stack(int capacity) {
this.stackContent = (Object[]) new Object[capacity];
}
public void pushpublic class BoundStack<E extends Comparable<E>> {
private E[] stackContent;
public BoundStack(int capacity) {
this.stackContent = (E[]) new Object[capacity];
}
public void push(E data) {
// ..
}
public E pop() {
// ..
}
}
}
}
Type Parameter E가 바인딩 된 경우
public class BoundStack<E extends Comparable<E>> {
private E[] stackContent;
public BoundStack(int capacity) {
this.stackContent = (E[]) new Object[capacity];
}
public void push(E data) {
// ..
}
public E pop() {
// ..
}
}
컴파일러는 바인딩 된 형식 매개 변수 E를 첫 번째 바인딩 된 클래스인 Comparable로 대체한다
public class BoundStack {
private Comparable [] stackContent;
public BoundStack(int capacity) {
this.stackContent = (Comparable[]) new Object[capacity];
}
public void push(Comparable data) {
// ..
}
public Comparable pop() {
// ..
}
}
Method Type Erasure의 경우 method-level type erasure 가 저장되지 않고 바인딩되지 않은 경우 부모 형식 Object로 변환되거나 바인딩 될 때 첫 번째 바인딩 된 클래스로 변환된다
public static <E> void printArray(E[] array) {
for (E element : array) {
System.out.printf("%s ", element);
}
}
컴파일시 컴파일러는 Type parameter E를 Object로 바꾼다
public static void printArray(Object[] array) {
for (Object element : array) {
System.out.printf("%s ", element);
}
}
참조