[이것이 자바다] 13. 제네릭(Generic)

목차
1. 제네릭이란?
제네릭이란 Java 5부터 도입한 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 말한다.
자세히 말하자면, '데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 방법'이다.
우리는 이미 제네릭을 자주 사용하고 있다. 흔히 사용하는 ArrayList, Stack, LinkedList 등에 제네릭이 사용된다.
객체<타입> 객체명 = new 객체<타입>();
의 형식으로 사용할 때, 여러가지 데이터 타입들이 <>
괄호 안에 들어가는 것이다.
만약 우리가 어떤 자료구조를 만들어 배포한다고 가정해보자. 이때, Integer에 대한 클래스, String에 대한 클래스 등 타입을 따로 만들 것인가? 그것은 너무 비효율적이다. 이러한 문제를 해결하기 위해 사용하는 것이 제네릭(Generic)이다.
이렇듯 제네릭은 클래스 내부에서 지정하는 것이 아니라 외부에서 사용자에 의해 지정되는 것을 뜻한다. 한마디로 특정(Specific) 타입을 미리 지정해주는 것이 아닌 필요에 의해 지정할 수 있도록 하는 일반(Generic) 타입이라는 것이다.
그렇다면 왜 제네릭을 사용해야 하는가?
제네릭을 사용하면 다음과 같은 장점이 수반된다.
1. 컴파일 시 강한 타입 체크를 할 수 있다.
→ 컴파일 시에 미리 타입을 강하게 체크하여 에러를 사전에 방지할 수 있다.
2. 타입 변환(casting)을 제거한다.
→ 비제네릭 코드는 불필요한 타입 변환을 하기 때문에 프로그램 성능에 저하시킨다.
예를 들어, 다음 코드는 다음 코드는 List에 문자열 요소를 저장했지만, 요소를 찾아올 때는 반드시 String으로 타입 변환을 해야 한다.
List list = new ArrayList();
list.add("hello");
String str = (String) list.get(0); // String으로 타입 변환이 필요하다.
아래와 같이 제네릭을 사용한다면
List<String> list = new ArrayList<String>();
list.add("hello");
String str = list.get(0); // 타입 변환을 하지 않는다.
List에 저장되는 요소를 String 타입으로 국한하기 때문에 요소를 찾아올 때 타입 변환을 할 필요가 없다.
2. 제네릭 타입(class<T>
, interface<T>
)
제네릭 타입은 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다.
1. 클래스 및 인터페이스 선언
blic class ClassName <T> { ... }
public Interface InterfaceName <T> { ... }
기본적으로 제네릭 타입의 클래스나 인터페이스의 경우 위와같이 선언한다.
T 타입은 해당 블럭 { ... }
안에서까지 유효하다.
제네릭 타입을 실제 코드에서 사용하려면 타입 파라미터에 구체적인 타입을 지정해야 한다. 그렇다면 왜 이런 타입 파라미터를 사용해야 할까?
2. 타입 파라미터를 사용하는 이유
예컨데, set() 과 get()을 갖는 다음과 같은 Box라는 클래스가 있다고 생각해보자.
public class Box {
private Object obj;
public void set(Object obj) {
this.obj = obj;
}
public Object get() {
return obj;
}
}
Box
클래스의 필드 타입은 Object
이다. 이는 필드에 모든 종류의 객체를 저장하기 위해서이다.
이때, Object
클래스는 모든 자바 클래스의 최상위 조상(부모) 클래이기 때문에, 모든 자바 객체는 Object
타입으로 자동 타입 변환되어 저장이 된다.
set() 메소드의 경우에는 Object
를 사용함으로써 매개값으로 모든 객체를 받을 수 있다.(자동 타입 변환이 가능하다.) 반대로 get() 메소드는 Object
필드에 저장된 객체를 Object
타입으로 리턴한다. 그렇기 때문에 만약 필드에 저장된 원래 타입의 객체를 얻고 싶다면, 강제 타입 변환이 필수이다.
Box box = new Box();
box.set("hello"); // String 타입을 Object 타입으로 자동 타입 변환해서 저장
String str = (String) box.get(); // Object 타입을 String 타입으로 강제 타입 변환해서 얻음
위와 같이 Object
타입을 사용하여 모든 종류의 자바 객체를 저장할 수는 있지만, 빈번한 타입 변환으로 전체적인 프로그램 성능을 저하시키기 때문에 위의 해결책으로 "제네릭(Generic)"을 사용한다.
위의 Box
클래스를 다음과 같이 수정할 수 있다.
public class Box<T> {
public T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
Box<T>
클래스의 제네릭 타입 파라미터를 이용하면 어떤 값이든 삽입하고 반환할 수 있게 된다.
제네릭을 사용하게 되면 불필요한 형 변환을 안 해도 되기 때문에 코드가 더 깔끔하고, 타입 안정성 또한 가질 수 있게 된다.
3. 멀티 타입 파라미터(class<K,V,...>
, interface<k,v,...>
)
더 나아가 제네릭 타입은 두 개 이상의 멀티 타입 파라미터를 사용할 수 있다. 이 경우 각 타입 파라미터를 콤마로 구분한다.
public class ClassName <T, K> { ... }
public Interface InterfaceName <T, K> { ... }
이 때 주의해야 할 점은 타입 파라미터로 명시할 수 있는 것은 참조 타입(Reference Type)밖에 올 수 없다. 즉, int
, double
, char
같은 primitive type은 올 수 없다는 것이다. 그래서 int
형 double
형 등 primitive Type의 경우 Integer
, Double
같은 Wrapper Type으로 쓰는 이유가 바로 위와같은 이유다.
4. 제네릭 메소드(<T, R> R method(T t)
)
1. 제네릭 메소드 정의 및 선언
제네릭 메소드는 매개 타입과 리턴 타입으로 타입 파라미터를 갖는 메소드를 말한다.
선언은 다음과 같이 진행한다.
public <T> T genericMethod(T o) {
...
}
클래스와 다르게 반환타입 이전에 <>
제네릭 타입을 선언한다.
2. 제네릭 메소드를 사용하는 이유
그렇다면 제네릭 메소드는 언제 사용하는가?
// stasic 변수는 사용불가
public class Student<T> {
static T name;
}
먼저 static 변수는 제네릭을 사용할 수 없다. 왜냐하면 Student 클래스가 인스턴스가 되기 전에 static은 메모리에 올라는데, 이 때 name의 타입인 T
가 결정되지 않기 때문에 위와 같이 사용할 수 없는 것이다.
static 메소드도 마찬가지로 Student 클래스가 인스턴스화 되기 전에 메모리에 올라가는데, 아직 T
의 타입이 정해지지 않았기 때문에 오류가 발생한다.
이때, 사용하는 것이 제네릭 메소드이다.
제네릭 메소드는 호출 시에 매개 타입을 지정하기 때문에 static이 가능하다.
이처럼 타입캐스팅 에러의 경우를 제외시킬 수 있기 때문에 훨씬 안전하게 사용할 수 있어서 사용하는 것이다.
3. 예제
// 제네릭 메소드로 boxing() 정의
public class Util {
public static <T> Box<T> boxing(T t) {
Box<T> box = new Box<T>();
box.set(t);
return box;
}
}
// boxing() 을 BoxingMethodExample 클래스에서 호출
public class BoxingMethodExample {
public static void main(String[] args) {
Box<Integer< box1 = Util.<Integer>boxing(100);
int intValue = box1.get();
Box<String> box2 = Util.boxing("규투리");
String strValue = box2.get();
}
}
5. 제한된 타입 파라미터(<T extends 최상위타입>
) 와 와일드카드 타입(<?>
, <? extends
...>
, <? super ..>
)
지금까지는 제네릭의 가장 일반적인 예시들을 보여주었다. 타입을 T
라고 하고 외부클래스에 Integer
을 파라미터로 보내면 T
는 Integer
가 되고, String
을 보내면 T
는 String
이 된다. 즉, 제네릭은 참조 타입 모두가 될 수 있다.
근데, 만약 특정 범위 내로 좁혀서 타입을 제한하고 싶다면 어떻게 해야할까?
1. <K extends T>
extends
키워드를 사용해서 타입을 제한할 수 있다.
다음 예제는 Number 타입의 하위 클래스 타입의 인스턴스만 가질 수 있도록 제한한 것이다.
public class Util {
public static <T extends Number> int compare(T t1, T t2) {
double v1 = t1.doublieValue(); // Number의 doubleValue() 메소드 사용
double v2 = t2.doublieValue(); // Number의 doubleValue() 메소드 사용
return Double.compare(v1, v2);
}
}
public class BoundedTypeParameterExample {
public static void main(String[] args) {
int result1 = Util.compare(10, 20);
int result2 = Util.compare(2.4, 4);
// String str = Util.compare("a", "b")
// Util은 Number 타입으로 제한되어 있기 때문에 호출할 수 없다.
}
}
2. <?>
: 와일드카드
타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있다.
와일드 카드 <?>
은 <? extends Object>
와 마찬가지이다. Object는 자바에서 모든 API 및 사용자 클래스의 최상위 타입이다.
3. <? extends T>
: 상한 경계
타입 파라미터를 대치하는 구체적인 타입으로 상위 타입이나 하위 타입만 올 수 있다.
이 때 <K extends T>
과 <? extends T>
는 비슷한 구조지만 차이점이 존재한다.
유형 경계를 지정하는 것은 동일하나 경계가 지정되고 K는 특정타입으로 지정이 되지만, ?는 타입이 지정되지 않는다는 의미이다.
/*
* Number와 이를 상속하는 Integer, Short, Double, Long 등의
* 타입이 지정될 수 있으며, 객체 혹은 메소드를 호출 할 경우 K는
* 지정된 타입으로 변환이 된다.
*/
<K extends Number>
/*
* Number와 이를 상속하는 Integer, Short, Double, Long 등의
* 타입이 지정될 수 있으며, 객체 혹은 메소드를 호출 할 경우 지정 되는 타입이 없어
* 타입 참조를 할 수는 없다.
*/
<? extends T> // T와 T의 자손 타입만 가능
위와 같은 차이가 있다. 그렇기 때문에 특정 타입의 데이터를 조작하고자 할 경우에는 K 같이 특정 제네릭 인수로 지정을 해주어야 한다.
4. <? super T>
: 하한 경계
타입 파라미터를 대치하는 구체적인 타입으로 하위 타입이나 상위 타입이 올 수 있다.
다음과 같이 서로 다른 클래스들이 상속관계를 갖고 있다고 가정해보자.
// extends
// T 타입을 포함한 자식 타입만 가능
// extends 뒤에 오는 타입이 최상위 타입으로 한계가 정해짐
<T extends B> // B와 C타입만 올 수 있음
<T extends E> // E타입만 올 수 있음
<T extends A> // A, B, C, D, E 타입이 올 수 있음
<? extends B> // B와 C타입만 올 수 있음
<? extends E> // E타입만 올 수 있음
<? extends A> // A, B, C, D, E 타입이 올 수 있음
// super
// T 타입의 부모 타입만 가능
// super 뒤에 오는 타입이 최하위 타입으로 한계가 정해짐
<K super B> // B와 A타입만 올 수 있음
<K super E> // E, D, A타입만 올 수 있음
<K super A> // A타입만 올 수 있음
<? super B> // B와 A타입만 올 수 있음
<? super E> // E, D, A타입만 올 수 있음
<? super A> // A타입만 올 수 있음
6. 제네릭 타입의 상속과 구현
제네릭 타입도 다른 타입처럼 부모 클래스가 될 수 있다.
public class ChildProduct<M, P, C> extends Product<M, P> {
private C company;
public C getCompany() {
return company;
}
public void setCompany(C company) {
this.company = company;
}
}
이전에 생성했던 Product<M, P>
클래스에서 제네릭 타입의 회사 정보가 필요하다면, 위와 같이 C
타입을 추가해서 company
라는 멤버를 받으면 된다.
[참고 블로그]