자바에서 제네릭(generic)이란 데이터의 타입(data type)을 일반화한다(generalize)는 것을 의미한다. 한마디로 특정(Specific) 타입을 미리 지정해주는 것이 아닌 필요에 의해 지정할 수 있도록 하는 일반(Generic) 타입이라는 것이다.
어떤 자료구조를 만들어 배포하려고 할 때 String 타입도 지원하고싶고 Integer타입도 지원하고 싶고 많은 타입을 지원하고 싶다. 그러면 String에 대한 클래스, Integer에 대한 클래스 등 하나하나 타입에 따라 만들 것인가? 이러한 문제를 해결하기 위해 우리는 제네릭이라는 것을 사용한다.
ArrayList<Integer> list1 = new ArrayList<Integer>();
ArrayList<String> list2 = new ArrayList<Integer>();
LinkedList<Double> list3 = new LinkedList<Double>():
LinkedList<Character> list4 = new LinkedList<Character>();
List<T> fruits = new ArrayList<>();
제네릭의 선언 및 생성
class MyArray<T> {
T element;
void setElement(T element) { this.element = element; }
T getElement() { return element; }
}
위의 예제에서 사용된 'T'를 타입 변수(type variable)라고 하며, 임의의 참조형 타입을 의미한다. 이러한 타입 변수를 원래 타입이 들어갈 자리에 삽입하여 제네릭을 선언한다.
꼭 'T'뿐만 아니라 어떠한 문자를 사용해도 상관없으며, 여러 개의 타입 변수는 쉼표(,)로 구분하여 명시할 수 있다. 이러한 특징으로 인하여 동시에 여러 타입을 선언하여 사용이 가능하다.
public <P, R> R test(P p, Function<P, R> function){
return function.apply(p);
}
class AnonyMap<K, V> implements Map<K, V>{
....
}
또한 제네릭은 와일드카드를 이용하여 타입에 대해 유연한 처리를 가능하게 한다. method2처럼 제네릭 타입을 와일드 카드로 모든 타입에 대하여 허용하게 될 경우 param의 제네릭은 최상위 클래스인 Object로 정의된다.
public void method1(List<Object> param){ // List의 제네릭타입으로 Object만 허용한다.
// ...
}
public void method2(List<?> param){ // List의 제네릭타입으로 모든 타입을 허용한다.
// ...
위와 같이 선언된 제네릭 클래스(generic class)를 생성할 때에는 타입 변수 자리에 사용할 실제 타입을 명시해야 한다.
MyArray<Integer> myArr = new MyArray<Integer>();
제네릭의 장점
- 제네릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다. 개발자가 의도하지 않은 타입의 객체가 저장되는 것을 방지할 수 있고 저장한 객체를 다시 가져올 때 기존 타입과 다른 타입으로 캐스팅되어 발생하는 오류(ClassCastException)를 줄일 수 있다.
- 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없다. 즉, 관리하기가 편하다.
- 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다.
- 형 변환(Type Casting)의 번거로움을 줄일 수 있다.
class MadPlay {
private Object obj;
public MadPlay(Object obj) { this.obj = obj; }
public Object getObj() { return obj; }
}
class GenericTester {
public void executeMethod() {
MadPlay instance1 = new MadPlay(new String("Hello"));
MadPlay instance2 = new MadPlay(new Integer(123));
MadPlay instance3 = new MadPlay(new Character('a'));
String obj1 = (String) instance1.getObj();
Integer obj2 = (Integer) instance2.getObj();
Character obj3 = (Character) instance3.getObj();
}
}
class GenericMadPlay<T> {
private T obj;
public GenericMadPlay(T obj) { this.obj = obj; }
public T getObj() { return obj; }
}
class GenericTester {
public void executeMethod() {
GenericMadPlay<String> genericInstance1 = new GenericMadPlay<>("Hello");
GenericMadPlay<Integer> genericInstance2 = new GenericMadPlay<>(123);
GenericMadPlay<Character> genericInstance3 = new GenericMadPlay<>('a');
String genericObj1 = genericInstance1.getObj();
Integer genericObj2 = genericInstance2.getObj();
Character genericObj3 = genericInstance3.getObj();
}
}
타입을 지정하지 않으면 최상위 Object 객체 타입으로 정의되므로 다양한 종류의 타입을 다뤄야하는 메서드의 매개변수에 Object 타입을 사용하고(첫 번째 예제 코드에서의 생성자) 그로 인한 타입 캐스팅이 불가피했지만 두 번째 코드를 보면 알 수 있듯이 제네릭을 사용하면 원하는 타입을 사전에 지정하기만 하면 된다.
제네릭 사용 시 주의할 점
- 제네릭은 클래스와 인터페이스만 적용되기 때문에 자바 기본 타입(Primitive Type)은 사용할 수 없다.
- 제네릭 타입의 객체는 생성이 불가능하다.
- 제네릭 클래스 또는 인터페이스 타입의 배열을 선언할 수 없습니다
public void someMethod() {
List<int> intList = new List<>(); // 기본 타입 int는 사용 불가
List<Integer> integerList = new List<>(); // Okay!
}
--
public void someMethod() {
// Type parameter 'T' cannot be instantiated directly
T t = new T();
return t;
}
--
public void someMethod() {
// generic array creation
// (자바 8이전) Cannot create a generic array of MadPlay<Integer>
MadPlay<Integer>[] arr1 = new MadPlay<>[10];
MadPlay<Integer>[] arr2 = new MadPlay[10]; // Okay!
}
출처