[Java] 지네릭스

최지수·2022년 5월 1일
0

Java

목록 보기
24/27
post-thumbnail

지네릭스(Generics)

일반화Generalized된 클래스나 인터페이스를 만들 때 사용되는 기능이에요. 대표적인 예를 들면, 우리가 정렬할 때 사용하는 sort 메서드는 정수나 문자열 같은 기본 타입형 변수부터 시작해, 우리가 만든 클래스 간에 정렬이 가능하게 설계되어야 다양한 조건 하에 사용할 수 있어요. 클래스를 새로 정의할 때마다 sort 메서드를 새로 만들어야 한다면 중복되는 코드가 무수히 많아지겠죠?

지네릭스Generics는 이런 문제를 해결시켜줘요. 특정 메서드나 변수를 캄파일 시점에서 타입 체크type check를 해줘 이에 맞게끔 기능들을 제공해줘요. 이를 통해 의도하지 않은 타입 처리를 막아 안정성을 높이고, 형변환을 생략할 수 있기 때문에 코드가 간결해지게 할 수 있어요.

지네릭스 선언

클래스

class 키워드 앞에 <문자>를 적어주면 되요. 이 때 문자는 일반적으로 Type을 의미하는 T를 넣어줘요. 즉, 해당 키워드를 명시하는 '1개의 문자'로 명시하는게 관례에요. Value를 의미하는 VElement를 의미하는 E를 보통 많이 쓰여요.

Generics는 클래스 내 변수나 메서드에서만 선언이 가능하니 참고하셔요.

public class <T> Utils {
	
}

변수 & 메서드

변수 타입대신 클래스에 선언된 generics 키워드를 명시해주시면 되요. 여기서 주의할 점은 클래스 변수는 지네릭스 선언이 불가능해요. 왜냐하면 generics는 컴파일 시점에 타입이 정해져야 하는데, 프로세스 실행 직후에 클래스 변수의 타입을 알 수 없기 때문이에요.

그리고 배열형 멤버 변수는 선언이 가능하나, 참조 할당 초기화를 할 수 없어요. 왜냐하면 new 키워드 수행 시 컴파일 시점에 타입을 확실히 알고 있어야 하기 때문이에요. 굳이 지네릭 배열을 초기화하고 싶다면 Object[] 변수를 초기화하고 T[] 변수에 복사하는 방식으로 할 순 있어요. 다만, 이럴 경우 unchecked type 경고가 표시되요.

클래스 메서드는 지네릭 메서드라 해서 다수 특수하게 사용이 가능하긴 해요. 이 부분은 아래에 다룰게요.

class TestGeneric<T> {
    T item;
    // static T sItem;              // Error!
    T[] items;

    // 이렇게 사용하면 안되고 지네릭 메서드 선언을 하셔야 해요
    // public static T staticMethod(T item) {
    //     System.out.println(item);
    //     return item;
    // }

    public TestGeneric(T item) {
        this.item = item;
        //items = new T[10];        // Error!
        //items = new Object[10];   // Error!

        Object[] objects = new Object[10];
        items = (T[])objects;       // Unchecked type!
    }
}

지네릭 클래스 객체 생성

사용하실 때 {클래스 명}<타입>으로 선언하셔서 사용하시면 되요. 이때 형변환을 생각해서 구현 쪽 <타입>에 자식 클래스를 넣을 걸 생각하실 수 있는데, 지네릭스 클래스 변수는 타입 명시가 둘이 완전히 동일해야 가능해요. 다만, 선언만 그럴 뿐이지, 내부적으로 자식 클래스 상호작용은 가능해요.

List<Integer> list1 = new ArrayList<IntegerEx /* Integer 상속 */>();	// Errror!
List<Integer> list2 = new ArrayList<Integer>();						// OK!
list2.add(new Integer());	// OK
list2.add(new IntegerEx());	// OK

제한된 지네릭 클래스

하나의 객체에선 한 종류의 타입만 저장할 수 있도록 제한했지만, 모든 종류의 타입을 지정할 수 있다는 사실을 알았어요. 그런데, '모든 종류'가 아닌 특정 종류로 제한을 할 수 있는 기능도 존재해요. generics 타입에 extends {클래스}를 통해 해당 클래스 또는 자식 클래스만 지정할 수 있도록 할 수 있어요.

class ClothePocket <T> { 
	// 옷 주머니는 작은 물건만 넣을 수 있어
	public void pickSmallObject(T object) {
    	// 형변환된 것은 SmallObject인지 확신할 수 없으니 
        // 형변환은 피할 수 없어
    	SmallObject small = (SmallObject)object;
        
    }
}
ClothePocket<SmallObject> pocket = new ClothePocket<SmallObject>();
// 옷 주머니에 큰 물건을 넣는다는건 말이 안돼! 근데 되네?
ClothePocket<BigObject> pocket = new ClothePocket<BigObject>();	

// 자, 옷주머니는 작은 물건과 이를 상속하는 객체만 관리할 수 있어!
class ClothePocket <T extends SmallObject> { 
	public void pickSmallObject(T object) {
    	// 인자가 SmallObject인건 확실하니 
        // 이 클래스를 기반으로 로직 전개도 되요
    	SmallObject small = object;
    }
}
// 네 놈은 어림도 없지
//ClothePocket<BigObject> pocket = new ClothePocket<BigObject>();	
// 그래 진작에 이랬어야지
ClothePocket<SmallObject> pocket = new ClothePocket<SmallObject>();

와일드 카드

아래와 같은 소스가 있다고 가정해봐요

class Stuff {}
class Food extends Stuff {}
class Phone extends Stuff {}
static void pick(Pocket<Stuff> stuffs) {
	...
}

Pocket 클래스는 지네릭 클래스가 아닌데다, 그렇다 하더라도 static 메서드는 타입 매개변수 T를 매개변수에 사용할 수 없어요. 그럼에도 이를 지네릭스를 적용하고 싶다면 어떻게 해야 할까요? 참고로 각 타입에 대응하는 클래스 메서드를 오버로딩해서 사용할 수 없어요. 타입이 다르다고 해서 자바는 이를 오버로딩이 아닌 중복 메서드 선언으로 인식하기 때문이에요.

// Error! 중복 메서드 선언!
static void pick(Pocket<Food> stuffs) {}
static void pick(Pocket<Phone> stuffs) {}

이를 위해서 나온 것이 와일드 카드에요. 정리하자면, 지네릭 클래스 메서드 내, 지네릭 타입 매개변수를 지네릭스하기 위한 개념이다라고 생각하시면 되요. 사용법은 ? 키워드를 사용해요. 여기서 extends를 통해 타입 또는 상속 클래스를, super를 통해 타입 또는 부모 클래스를 지네릭화 시킬 수 있어요. ?만 존재하면 모든 클래스를, 즉 Object를 상속하는 모든 클래스를 지네릭화 시킬 수 있다고 이해하면 되요.

// Stuff 또는 상속 클래스
static void pick(Pocket<? extends Stuff> stuffs) {}
// Stuff 또는 부모 클래스
static void pick(Pocket<? super Stuff> stuffs) {}
// Object 클래스, 즉 모든 클래스
static void pick(Pocket<?> stuffs) {}

실사례를 통해 보자면, 자바에서 자주 활용되는 sort 함수가 있어요.

static 키워드 앞에 지네릭스 선언이 되어 있는데 이는 지네릭 메서드라고 해요. 이건 아래에 다룰게요.

Comparator를 보면 <? super T>라고 선언되어 있어요. 이는 TT의 부모 클래스를 지네릭스화 시킨다는 의미에요. 이는 결국 T를 확장한 자식 클래스를 사용하지 못하도록 막음으로써 지네릭스의 제한을 둬서 예상할 수 없는 예외를 사전에 방지한다는 의미에요.

이렇게 자바는 조건을 제한함으로써 개발자의 의도를 보다 명시적으로 알려줄 수 있어요.

지네릭 메서드

드디어 나왔네요. 메서드의 선언부에 지네릭 타입이 선언된 메서드에요.

// 바로 요거!
static <T> void sort(T[] items, Comparator<? super T> c);

참고로 지네릭 메서드에 선언된 타입과 지네릭 클래스에 선언된 타입은 서로 별개에요. 설령 같은 타입 문자를 사용해도 둘은 완전히 다른 것이니 헷갈리지 마세요!

그래도 지네릭 클래스랑 다소 비슷한 점이 있다면, 지네릭 메서드의 타입은 인자의 지네릭 타입에 모두 적용해요. 내부 멤버 변수/메서드에 지네릭 타입을 적용하는 지네릭 클래스와, 바로 이런 부분이 동일하다는 사실을 아시면 이해하기 쉬우실 거에요.

따라서 아래와 해당 선언을 아래와 같이 하여 보다 편리하게 작성이 가능해요.

static void doSomething(List<? extend {class}> list1, List<? extend {class}> list2);

// 아래와 같이 적용이 가능해요.
static<T extends {class}> void doSomething(List<T> list1, List<T> list2);

그리고 지네릭 메서드 '사용'하실 때 주의할 점은 호출 시 대입된 타입을 반드시 명시해줘야 해요. 클래스 내부에서 클래스 메서드를 사용할 때 그러지 않아도 되는 부분에 대비가 되요. 이는 기술적인 이유에 의한 규칙이라 그냥 그렇구나~하고 사용하시면 되요.

// <{class}>doSomething(list1, list2); 			// Error!
this.<{class}>doSomething(list1, list2); 		// OK!
Something.<{class}>doSomething(list1, list2); 	// OK!

컴파일(feat. 지네릭 타입의 제거)

지네릭스 타입에 대한 정보는 컴파일 후 바이트 코드인 *.class에 존재하지 않아요. 왜냐하면 컴파일 이후에 타입이 이미 정해버리기 때문이에요.

예를 들면 <T extends String> 지네릭 타입은 String으로 치환되요. <T>는 그냥 Object로 치환되요. 이렇게 생각하시면서 작성하신다면 지네릭스를 지정하실 때 조금 더 편하게 생각하실 수 있으실 거에요.

// 이렇게 작성된게...
class<T extends Stuff> Pocket {
	void add(T item) {
	
    }
}

// 이렇게 변하는 거에요!
class Pocket {
	void add(Stuff item) {
    	Stuff item2 = (Stuff)item;
	}
}
profile
#행복 #도전 #지속성

0개의 댓글