제네릭 만들기
제네릭 클래스
제네릭 클래스를 작성하는 방법은 기존의 클래스 작성 방법과 유사한데, 클래스이름 다음에 일반화된 타입(generic type)의 매개변수를 <와 > 사이에 추가한다는 차이가 있습니다.
제네릭 클래스 작성
타입 매개변수를 T를 가진 제네릭 클래스 MyClass는 다음과 같이 작성합니다.
public class MyClass<T> { // 제네릭 클래스 Myclass, 타입 매개변수 T
T val; // 변수 val의 타입은 T
void set(T a) {
val = a; // T 타입의 값 a를 val에 저장
}
T get() {
return val; // T 타입의 값 val 리턴
}
}
제네릭 클래스에 대한 레퍼런스 변수 선언
제네릭 클래스의 레퍼런스 변수를 선언할 때 다음과 같이 타입 매개변수에 구체적인 타입을 적습니다.
MyClass<String> s; // <T>를 String으로 구체화
List<Integer> li; // <E>를 Integer로 구체화
Vector<String> vs; // <E>를 String으로 구체화
제네릭 객체 생성 - 구체화(specialization)
제네릭 클래스에 구체적인 타입을 대입하여 구체적인 객체를 생성하는 과정을 구체화라고 부르며, 이 과정은 지비 컴파일러에 의해 이루어집니다. MyClass<T>에서 T에 구체적인 타입을 지정하여 객체를 생성하는 예는 다음과 같습니다.
MyClass<String> s = new MyClass<String>(); // 제네릭 타입 T를 String으로 구체화
s.set("hello");
System.out.println(s.get()); // "hello"출력
MyClass<Integer> n = new MyClass<Integer>(); // 제네릭 타입 T를 Integer로 구체화
n.set(5);
System.out.println(n.get()); // 숫자 5 출력
MyClass<String>만 다루는 구체적인 클래스가 되며, MyClass<Integer>는 정수만 다루는 구체적인 클래스가 됩니다.
컴파일러에 의해 String으로 구체화된 MyClass<String>
public class MyClass<String> {
String val; // 변수 val의 타입은 String
void set(String a) {
val = a; // String 타입의 문자열 a를 val에 저장
}
String get() {
return val; // String 타입의 문자열 val 리턴
}
}
제네릭의 구체화에는 기본 타입은 사용할 수 없음에 유의해야 합니다.
Vector<int> vi = new Vector<int>(); // 컴파일 오류. int는 사용 불가
Vector<Integer> vi = new Vector<Integer>(); // 정상 코드
타입 매개변수
제네릭 클래스 내에서 제네릭 타입을 가진 객체의 생성은 허용되지 않습니다.
public class MyVector<E> {
E create() {
E a = new E(); // 컴파일 오류. 제네릭 타입의 객체 생성 불가
return a;
}
}
컴파일러가 MyVector<E> 클래스의 new E() 라인을 컴파일할 때, E에 대한 구체적인 타입을 알 수 없어, 호출될 생성자를 결정할 수 없고, 또한 객체 생성 시 어떤 크기의 메모리를 할당해야 할 지 전혀 알 수 없기 때문입니다.
제네릭 스택 만들기
class GStack<T> { // 제네릭 스택 선언. 제네릭 타입 T
int tos;
Object[] stck; // 스택에 요소를 저장할 공간 배열
public GStack() {
tos = 0;
stck = new Object[10];
}
public void push(T item) {
if (tos == 10) // 스택이 꽉 차서 더 이상 요소를 삽입할 수 없음
return;
stck[tos] = item;
tos++;
}
public T pop() {
if (tos == 0) // 스택이 비어 있어 꺼낼 요소거 없음
return null;
tos--;
return (T) stck[tos]; // 타입 매개변수 타입으로 캐스팅
}
}
public class MyStack {
public static void main(String[] args) {
GStack<String> stringGStack = new GStack<String>(); // String 타입의 GStack 생성
stringGStack.push("seoul");
stringGStack.push("busan");
stringGStack.push("LA");
for (int n = 0; n < 3; n++)
System.out.println(stringGStack.pop()); // stringStack 스택에 있는 3개의 문자열 팝
GStack<Integer> intStack = new GStack<Integer>(); // Integer 타입의 GStack 생성
intStack.push(1);
intStack.push(3);
intStack.push(5);
for (int n = 0; n < 3; n++)
System.out.println(intStack.pop()); // intStack 스택에 있는 3개의 정수 팝
}
}
실행 결과
LA
busan
seoul
5
3
1
제네릭 클래스 내에서 제네릭 타입의 객체를 생성할 수 없는 것과 같은 이유로 배열도 생성할 수 없으므로 Object 배열로 생성하였습니다.
stck = new T[10]; // 컴파일 오류. 제네릭에서는 T 타입의 배열을 생성할 수 없다.
데이터를 저장해둘 배열을 Object[] 배열로 선언하였으므로 아래와 같이 타입 매개변수의 타입으로 강제 캐스팅하여 리턴해야 합니다.
return (T)stck[tos]; // 타입 매개변수 T 타입으로 캐스팅
제네릭과 배열
제네릭에서는 배열에 대한 제한을 두고 있습니다. 제네릭 클래스 또는 인터페이스 타입의 배열은 선언할 수 없습니다.
GStack<Integer>[] gs = new GStack<Integer>[10]; // 컴파일 오류
그러나 제네릭 타입의 배열 선언은 허용된다.
public void myArray(T[] a) { ..... } // 정상
제네릭 메소드
클래스의 일부 메소드인 제네릭으로 구현할 수도 있습니다. toStack() 메서드를 제네릭으로 구현한 예는 다음과 같습니다.
class GenericMethodEx {
static <T> void toStack(T[] a, GStack<T> gs) {
for (int i = 0; i < a.length; i++) {
gs.push(a[i]);
}
}
}
타입 매개변수는 메소드의 리턴 타입 앞에 선언됩니다. 위의 toStack()에서 <T>가 타입 매개변수의 선언이다. 제네릭 메소드를 호출할 때는 컴파일러가 메소드의 인자를 통해 타입을 유추할 수 있어 제네릭 클래스나 인터페이스와는 달리 타입을 명시하지 않아도 됩니다.
다음 코드는 컴파일러가 toStack()의 호출문으로부터 타입 매개변수 T를 Object로 유추하는 경우이다.
Object[] oArray = new Object[100];
GStack<Object> objectStack = new GStack<Object>();
Generic<MethodEx.toStack(oArray, objectStack); // 타입 매개변수 T를 Object로 유추함
또 다른 경우로, 아래의 코드는 컴파일러가 toStack()의 호출문으로부터 타입 매개 변수 T를 String으로 유추하는 경우이다.
String[] sArray = new String[100];
GStack<String> stringStack = new GStack<String>();
Generic<MethodEx.toStack(oArray, stringStack); // 타입 매개변수 T를 String으로 유추함
다음의 경우는 타입 매개변수 T를 Object로 유추합니다.
Generic<MethodEx.toStack(sArray, objectStack);
여기서 sArray는 String[] 타입이며, objectStack은 GStack<Object>이빈다. Object가 String의 슈퍼 클래스이므로 컴파일러는 Object 타입으로 유추합니다.
스택의 내용을 반대로 만드는 제네릭 메소드 만들기
public class GenericMethodEx {
public static <T> GStack<T> reverse(GStack<T> a) { // T가 타입 매개변수인 제네릭 메소드
GStack<T> s = new GStack<T>(); // 스택 a를 반대로 저장할 목적 GStack 생성
while (true) {
T tmp;
tmp = a.pop(); // 원래 스택에서 요소 하나를 꺼냄
if (tmp == null) // 스택이 비었음
break; // 거꾸로 만드는 작업 종료
else
s.push(tmp); // 새 스택에 요소를 삽입
}
return s; // 새 스택을 리턴
}
public static void main(String[] args) {
GStack<Double> gs = new GStack<Double>(); // Double 타입의 GStack 생성
for (int i = 0; i < 5; i++) { // 5개의 요소를 스택에 push
gs.push(new Double(i));
}
gs = reverse(gs);
for (int i = 0; i < 5; i++) {
System.out.println(gs.pop());
}
}
}
실행 결과
0.0
1.0
2.0
3.0
4.0
제네릭의 장점
- 동적으로 타입이 결정되지 않고 컴파일 시에 타입이 결정되므로 보다 안전한 프로그래밍 가능
- 런타임 타입 충돌 문제 방지
- 개발 시 타입 케스팅 절차 불필요
- ClassCastException 방지
'프로그래밍 언어 > JAVA' 카테고리의 다른 글
| FileReader (1) | 2025.04.17 |
|---|---|
| 자바의 입출력 스트림 (1) | 2025.04.14 |
| LinkedList<E> & Collections 클래스 활용 (0) | 2025.04.08 |
| HashMap<K, V> (0) | 2025.04.05 |
| 컬렉션의 순차 검색을 위한 Iterator (0) | 2025.04.02 |