제네릭스(Generics)

syeon·2022년 1월 28일
0

Java

목록 보기
4/4

Generic?

제네릭(Generic)은 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일할 때 타입 체크(compile-time type check)를 해주는 기능이다.

객체의 타입을 컴파일 시에 확인해주기 때문에 타입 안정성을 높이고 형변환이 번거로움을 줄여준다.

Generic의 필요성

굳이 Generic을 사용하지 않고 모든 타입을 받을 수 있는 Object 타입을 사용해도 되지 않을까?
다음 예제를 보면 Sample 클래스의 변수 obj는 Object타입으로 선언되어있다.

class Sample {
	private Object obj;
   	public Sample(Object obj) { this.obj = obj; }
   	public Object getObj() { return obj; }
}
public class Main {
	public static void main(String[] args) {
    	// 객체 생성[1] > String 타입으로 생성
    	Sample s1 = new Sample("안녕하세요");
        System.out.println(s1.getObj());
        
        // 객체 생성[2] > Integer 타입으로 생성
        Sample s2 = new Sample(100);
        System.out.println(s2.getObj());
    }
}
--------------------------------------------------------
>> 안녕하세요
>> 100

따라서 s1 객체와 s2 객체와 같이 변수 obj를 "안녕하세요"라는 문자열 타입 혹은 100의 값을 갖는 정수형 타입으로 정의하여도 객체 생성이 가능하다.
보기에는 그럴 듯해 보이지만 Object를 사용하면 다음과 같은 문제가 발생한다.

public class Main {
	public static void main(String[] args) {
    		
            ...
            
            //String str = s1.getObj();	  --> Error!
            //int num = s2.getObj();  --> Error!
            
            // 실제로는 Object 타입이기 때문에 강제 형변환이 필요하다
            String str = (String) s1.getObj();
            int num = (Integer) s2.getObj();
    }
}

보기에는 사용되는 타입(String, int)처럼 보이지만 실제로는 Object 타입이기 때문에 자동 형변환이 되지 않는다.
또한 컴파일 단계에서는 에러가 나지 않지만 실행 단계에서 Class Cast 오류가 발생하는 등 타입 안정성에 문제가 생길 수 있기 때문에 이와 같은 방법은 권장되지 않는다.

Generic 선언

제네릭은 클래스와 메서드에 선언할 수 있다.
앞서 살펴본 예제인 Sample 클래스를 활용하여 클래스에 선언해보자.

class Sample<T> {
	private T obj;
   	public Sample(T obj) { this.obj = obj; }
   	public T getObj() { return obj; }
}

Sample 클래스를 제네릭 클래스로 변경하고 싶다면 클래스 이름 옆에 <Type명> 을 붙이면 된다.
보통 Type의 앞글자를 따서 "T"를 많이 사용한다. 이때 T는 타입 변수(type variable)라고 한다.

이렇게 제네릭 클래스로 선언된 Sample 클래스의 객체를 생성하려면 다음과 같이 참조변수와 생성자에 타입 T대신에 실제 사용하게 될 타입을 지정해주면 된다.

public class Main {
	public static void main(String[] args) {
    	// 객체 생성[1] > String
    	Sample<String> s1 = new Sample<String>("안녕하세요");
        String str = s1.getObj();
        System.out.println(str);
        
        // 객체 생성[2] > Integer
        Sample<Integer> s2 = new Sample<Integer>(100);
        int num = s2.getObj();
        System.out.println(num);
    }
}
--------------------------------------------------------
>> 안녕하세요
>> 100

Sample 타입의 객체 s1의 경우 String 타입을 T대신에 지정해주었다.
이는 Sample< T >를 다음과 같이 지정해준 것과 같다.
이제 s1의 제네릭 타입 변수에는 String 타입의 객체만 저장 가능하다.

class Sample<String> {
	private String obj;
   	public Sample(String obj) { this.obj = obj; }
   	public String getObj() { return obj; }
}

만약에 타입을 지정해주지 않고 그냥 사용하게 된다면 Object형으로 간주된다. 이렇게도 사용은 가능하지만 안전하지 않다는 경고가 발생한다.

Generic의 제한

❌ static 멤버에는 적용할 수 없다.

이처럼 제네릭 클래스의 객체를 생성할 때는 객체 별로 다른 타입을 지정하는 것이 가능하다.
그러나 모든 객체에 동일하게 동작해야 하는 static 멤버에는 타입 변수를 적용할 수 없다.
static 멤버는 지정된 타입 변수와 관계 없이 동일한 것이어야 하기 때문이다.

❌ 배열을 생성할 수 없다.

제네릭 배열 타입의 참조 변수를 선언하는 것은 가능하다.
하지만 'new T[ ]'와 같이 배열을 선언하는 것은 불가능하다.

(꼭 제네릭 배열을 생성해야 한다면 'Reflection API'의 newInstance()와 같이 동적으로 생성하는 메서드의 배열을 생성하거나, Object 배열을 생성한 뒤 T[ ]로 형변환하는 방법 등이 있다)

profile
기록하려고 만든 개발블로그, 까먹지 말자!

0개의 댓글