프로그래밍 언어/JAVA

제네릭 만들기

· 코딩마이데이

제네릭 클래스

제네릭 클래스를 작성하는 방법은 기존의 클래스 작성 방법과 유사한데, 클래스이름 다음에 일반화된 타입(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