Java Generic (1)

wannabeking·2023년 4월 16일
0

Java

목록 보기
9/13
post-thumbnail

Java의 Generic API는 Java 5부터 사용 가능합니다.

제네릭 이전에는 컬렉션에서 객체를 꺼낼 때마다 형변환을 해야 했고, 따라서 런타임 시점에 ClassCastException이 발생할 수 있는 위험성이 존재했습니다.

우리는 Generic 덕분에 잘못된 객체를 넣으려는 시도를 컴파일 시점에서 막을 수 있는 타입 안정성을 얻었습니다.

또한 List<String>과 같이 parameter type이 명시되어 코드 가독성이 증가하고 generic type은 다양한 타입들을 처리할 수 있어 확장성과 유연성이 증가합니다.

다만, 클래스의 리터럴, instanceof 연산자와 같이 제네릭이 아닌 raw 타입을 사용해야되는 경우도 있다.


이번 시간에는 제네릭의 type parameter, wildcard type를 unbounded에 대해서만 알아보고, 제네릭이 어떻게 타입 안정성을 제공하는지 type erasure를 통해 살펴보겠습니다.



Type Parameter

Java Generic에서 type parameter는 generic type에서 'E', 'T' 등과 같은 알파벳으로 표현되며, 객체의 타입을 지정할때 구체적인 타입으로 대체됩니다.

예를 들어

public class Box<T> {
    private T value;
    
    public Box(T value) {
        this.value = value;
    }
    
    public T getValue() {
        return value;
    }
    
    public void setValue(T value) {
        this.value = value;
    }
}

위와 같은 클래스가 존재한다면


Box<String> stringBox = new Box<>("java");
String s = stringBox.getValue();

Box<String> integerBox = new Box<>(1);
int i = integerBox.getValue();

와 같이 String, Integer라는 actual type parameter를 통해 T가 구체화 됩니다.

따라서 우리는 하나의 클래스를 타입 안정성을 가진 상태에서 다양한 타입의 필드로 사용할 수 있습니다.


그렇다면 제네릭은 covariant(공변) 일까요?

covariant란 Java의 배열과 같이 Sub가 Super의 하위 타입일때 Sub[]도 Super[]의 하위 타입인 것처럼 함께 변하는 성질

아닙니다, 제네릭은 불공변합니다.

그렇다면 제네릭은 왜 불공변으로 설계했을까요?


// 공변이라면...?
List<Object> list = new ArrayList<String>();
list.add("java");
list.add(1); // runtime error

만약 공변으로 설계했다면 위와 같이 런타임 에러가 발생하여, 제네릭의 목적인 타입 안정성을 얻을 수 없습니다.

불공변하게 된다면 다형성의 이점을 챙길 수 없다는 단점이 있으므로 제네릭은 아래에서 설명드릴 wildcard type을 사용하여 다형성을 챙깁니다.



wildcard type

제네릭에서 wildcard type은 ?를 사용하여 선언합니다.

wildcard type을 사용하면 여러 타입의 값을 다룰 수 있습니다.


Box<?> box = new Box<>("java");
Object o = box.getValue();

위 코드에서 Box<String>는 wildcard type인 Box<?>로 추상화할 수 있습니다.

컴파일러는 unbounded wildcard type을 Object로 대체합니다.


그렇다면 wildcard type을 사용하면 Object로 변환하는데, List<?>List<Obejct>는 무엇이 다른걸까요?

정답은, 앞서 말씀드린 것처럼 제네릭은 불공변하기에 하위 타입이라는 개념이 존재하지 않아 List<Obejct> 타입 파라미터는 List<String>를 받을 수 없지만,

wildcard를 사용한 List<?> 파라미터는 List<String>을 받을 수 있습니다.


코드로 살펴보면,

private void printList(List<Object> list) {
	list.foreach(System.out::println);
}

	...
	List<String> list = new ArrayList<>();
	list.add("java");
	printList(list); // 불가능!

위 코드에서는 컴파일 에러가 발생한다는 것을 확인할 수 있습니다.


그렇다면 wildcard를 사용하면 어떻게 될까요?

private void printList(List<?> list) {
	list.foreach(System.out::println);
}

	...
	List<String> list = new ArrayList<>();
	list.add("java");
	printList(list); // 가능!

위와 같이 유연성을 위해 wildcard를 사용하여 여러가지 parameter type을 수용 가능하게 할 수 있습니다.


다만, 주의할 점은 다음과 같습니다.

비한정적 와일드카드 타입 사용 시 read-only로만 사용 가능하다!
만약 write를 시도하면, java: incompatible types: java.lang.String cannot be converted to capture#1 of ? 에러를 뱉는다.

이유는 간단합니다. 이 타입이 포함하는 요소 타입들이 무엇인지 알 수 없기 때문입니다.

이를 허용한다면, 런타임시 엄청난 ClassCastException이 발생할 것입니다.

  • 예를 들어 List<String>을 인자로 넣었는데, Integer를 add하는 경우

// 인자로 List<String>이 올수도, List<Integer>가 올수도 있음!
private void a(List<?> list) {
	list.add("java");
}

따라서 위 코드를 시도한다면,

컴파일이 불가능하다는 것을 확인할 수 있습니다.



Type Erasure

소개에서 설명드린 것처럼, 제네릭은 컴파일에 타입 변환이 완료됩니다.

이는 컴파일 시점에서 자바 코드를 바이트 코드로 변환할 때 type parameter에 관련한 코드를 삭제하고 generic type을 일반적인 클래스, 인터페이스로 바꾸어 진행됩니다.

이 과정을 type erasure라 하는데, 직접 generic type을 만들어 type erasure의 결과를 확인해보겠습니다.


public class GenericTest<T> {

    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

우선 unbounded type parameter로 살펴보겠습니다.

위의 코드를 컴파일하여 바이트 코드를 생성하면...


private Ljava/lang/Object; value
...

public getValue()Ljava/lang/Object;
...

public setValue(Ljava/lang/Object;)V
...

위와 같이 paremeter와 return 모두 Obejct로 변환된 것을 확인할 수 있습니다.


public static void main(String[] args) {
    GenericTest<String> genericTest = new GenericTest();
    genericTest.setValue("java");
    System.out.println((String)genericTest.getValue());
}

해당 generic type을 사용하는 곳에서는


CHECKCAST java/lang/String

와 같이 사용한 actual type parameter에 일치하는 객체로의 형변환 과정이 추가됩니다.


즉, type erasure는 런타임시 발생할 수 있는 type cast error를 잡아주기 위해 사용했던 type parameter을 삭제하는 과정이라할 수 있습니다.

저희는 unbounded type을 사용했기 때문에 field와 method parameter, return의 경우 최상위 객체인 Obejct로 변환되었고, 해당 필드를 사용하는 코드에서는 바이트 코드에 type cast 코드가 삽입된 것입니다.

또한, unbounded wildcard도 동일하게 Obejct로 치환됩니다.


여기서도 주의할 사항이 있는데, 컴파일 후 제네릭에 관련된 코드들이 삭제되고 generic type은 일반 클래스로 변환되기 때문에 런타임 시점에는 형 안정성을 보장할 수 없습니다!


이상으로, 다음 시간에는 Generic의 bounded type에 대해 알아보겠습니다!



profile
내일은 개발왕 😎

0개의 댓글