Generic과 함께하는 가장 대표적인 warning부터 보고 시작하겠습니다
List <T> 대신 List 형태로 바로 사용하고 있는데요
Warning
unchecked
Generic을 이용해 타입 지정을 했지만, 실제 추론 가능한 타입은 Object일 때 사용됩니다
Object를 Generic에 의해서 제공된 타입의 정보로 캐스팅할 때 발생합니다
list.get(index)를 하는 결과는 Object타입이지만, 이를 (T) 형태로 캐스팅할 수 있는지 여부를 확인하지 못하고 변환했기에 발생한 예외입니다
rawtypes
List를 List <T> 형태로 작성하지 않고, List 형태로 사용했기 때문에 컴파일러가 타입추론을 할 수 없다는 경고를 하고 있습니다
List <T>를 제대로 적어준다면, 발생하지 않는 경고입니다
런타임에 Generic에 대한 정보는 어디로 갈까?
class 파일을 실행하는데, 실제 클래스 파일을 보면 런타임에 generic에 대한 정보가 다 사라지는 모습을 볼 수 있습니다
이를 Type Erase라고 하는데요
Type Erase
Java 컴파일러가 Type 을 지우는 규칙
Java 8을 기준으로 봤을 때, 컴파일된 이후에는 아무것도 Super 나 Extends로 처리하지 않았다면, Object 형태로 컴파일이 되고, 아니라면 최대한 구체적으로 추론한다고 합니다
private static <N extends Number> N cast2(N n) {
return n;
}
같은 메서드가 있다면 Number로 추론될 것으로 기대했는데요
실제로 Java 11에서 컴파일 했을 때는 달랐습니다
이는 실제로 intellij 의 도움을 받아서 java 11 클래스파일을 디컴파일 한 결과입니다
타입이 없어지지 않는 것을 확인할 수 있었습니다
이 부분에 대해서는 추가적으로 확인해 볼 필요가 있어 보입니다
https://docs.oracle.com/javase/tutorial/java/generics/erasure.html
하지만 일단 규칙상으로는 없어지는 게 맞다고 하긴 하네요
규칙이 적용되었다면 Number 타입으로 반환하고, casting을 했을 것 같기도 하네요
장점 아닌 장점으로 런타임에 타입이 소거되기에, parameterized type 별로 하나씩 생성하지 않기에 오버헤드가 줄어든다는 점이 있네요
Generic에 대한 정보를 런타임에 얻어올 방법
클래스의 정의가 Generic과 관계가 있다면, 이는 컴파일타임에 없어지지 않습니다
class Something {
public final List<String> list = new ArrayList<>();
}
System.out.println(something.getClass().getDeclaredField("list").getGenericType());
이런 식으로 직접 출력을 해볼 수 있는데요
다른 방식으로는
ParameterizedType
이라는 인터페이스를 이용하면 됩니다
reflective 메서드를 이용해서 호출되었을 때 생성된다고 합니다.
이 타입을 얻어내는 방법은 getGenericSuperClass() 메서드를 호출하는 것뿐인데요
getGenericSuperClass() 메서드를 뜯어보면
이렇게 되어있습니다
genericInfo 이 실제로 적용되어 있다면 classRepository에서 데이터를 꺼내옵니다
반환 타입은 Type이지만 만약 generic이 적용되어 있다면 Type을 상속받은 ParameterizedType 형태로 반환되게 됩니다.
이를 바깥에서 (ParameterizedType)~~~. getClass(). getParameterizedType 형태로 캐스팅해 보면 실제 generic을 통해서 받은 타입을 알 수 있습니다
System.out.println(((ParameterizedType) sub.getClass().getGenericSuperclass()).getActualTypeArguments()[0]);
같은 형태로 실제로 출력해 볼 수 있습니다
이름에서 알 수 있는 것처럼 generic이 적용된 것이 부모 클래스인 경우에만 타입을 가져올 수 있는데요
과 같은 케이스에서만 사용할 수 있습니다
처음에도 말했지만, 클래스의 정의와 직접 관련되어 있는 경우라면 제네릭 데이터가 남아있을 수 있습니다
PECS
Producer Extends Consumer Super의 약자입니다
Producer Extends
생산하는 쪽에서는 Extends를 통해서 받으면 좋다는 의미입니다
여기서 봤을 때, 처음에는 integer 가 들어갔는데, 이후에 Number를 확장한 모든 클래스면 다 받을 수 있도록 만들어줍니다
이후에 쓸 때는 당연하지만, Integer을 통해 접근할 수 없고, Number에 대해서 접근할 수밖에 없습니다
Number을 확장한 클래스가 Integer만 있는 것은 아니니까요
이때 List <Number>에 대입할 수는 없습니다 이를 해결하기 위해서 Number를 확장하는 모든 클래스는 다 괜찮아라고 하는 extends를 활용해서 처리하는 방식을 사용하는 것을 Producer Extends라고 합니다
Consumer Super
이렇게 쓸 수 있는데요
쓰는 입장에서는 List <Object> 도 괜찮고, List <Number> 도 괜찮고, List <Integer> 도 괜찮습니다
그냥 그대로 출력만 하는 경우에는, 저런 식으로 출력을 할 수 있는데요
당연하지만 실제 사용 시에 어떤 타입인지 확인이 불가능하기에, 모든 경우에 다 사용 가능한 Object 타입으로만 접근할 수 있습니다
이렇게 어떤 타입이든 괜찮아~라는 경우에는 Consumer Super를 사용합니다
Generic을 사용하면 안 되는 경우
1. primitive type 은 사용할 수 없습니다
generic이 없다가 생겼는데, 이때 과거와의 하위호환성을 유지하기 위해서 모든 generic 타입은 Object 타입으로 변환할 수 있어야 합니다. 그렇기에 Object 타입이 아닌 int 같은 primitive type은 사용이 불가능합니다
(X) List <int>
2. new 키워드를 사용할 수 없습니다
T t=new T(); 형태로 사용할 수 없는데요
이는 컴파일 타임으로 가면, Object 형태로 변경된다고 했을 때, new Object(); 가 호출되는 것과 비슷하기에 안 될 것 같아 보이네요
직접 바로 new를 사용하지 않고, reflection의 newInstance를 이용해서 생성하는 것은 가능하다고 합니다.
그 객체를 통해서 얻어온 정보이기에 가능해 보입니다
3. static 변수로 사용할 수 없습니다
private static final T something;
이 부분이 불가능한 것인데요
클래스 전체에서 공유되어야 하는데, 다른 타입으로 클래스를 생성했을 때 에러가 나기 때문 같아 보입니다
4. instanceOf에 사용할 수 없습니다
List instanceOf List <String>
를 했을 때, 컴파일 타임 이후에는 String에 대한 정보가 없기에, 제대로 알 방법이 없기에 사용할 수 없습니다
5. generic과 함께 배열을 사용할 수 없습니다
List <Integer>[] 같은 경우가 불가능한 것인데요 배열은 공변성을 가지고 있기에, 힙 오염이 발생할 수 있다는 이유로 이런 것은 사용할 수 없다고 합니다
6. 오버라이드시에 사용할 수 없습니다
something(List <String> string)
something(List <Integer> integer)
이런 2가지를 구분할 방법이 없어서 막아두었다고 합니다
7. Throwable을 확장하는 클래스가 만들어질 수 없습니다
MyException <T> extends Throwable
같은 경우에 작동하지 않는다고 합니다
MyException <String> vs MyException <Integer>을 catch 할 방법이 없어서 이렇게 만들었다고 합니다
varargs와 사용하는 Generic
금지 사유 5번과 정확하게 일치하는 문제가 발생합니다
이 문제가 발생하면, String 임에도, Integer를 반환할 수 있습니다
그래서 사용이 불가능하지만, 편의성이 너무 압도적이었기에, 이를 추가되었다고 합니다
List.of 메서드를 통해서 생성하는 상황에서 특정 타입의 변수만을 다 받아서 생성하는 과정을 통해서 편의성을 만들 수 있습니다
사실 위의 타입을 보게 되면 E[] 형태가 되게 됩니다. 하지만 E [] 타입은 Generic 이기에, 생성이 안 되어야 합니다
이런 느낌으로 생성이 됩니다
당연하지만, 이러면 힙 오염이 발생하는 문제가 생깁니다
실제로 위의 코드는 컴파일이 됩니다 하지만, 런타임에 ClassCastException이 발생하면서 실행에 실패하게 됩니다
이를 방지하기 위한 해결책은 여러가지가 있지만 2가지를 소개해드리도록 하겠습니다
1. 아무것도 추가로 저장하지 않는다
당연하지만, 아무것도 저장하지 않으면 문제가 생기지 않습니다
2. 매개변수 배열을 바깥으로 노출시키지 않아야 합니다
바깥으로 노출시키는 순간 위에 작성한 코드처럼 너무 쉽게 터뜨릴 수 있으니까요
이런 조건들을 다 만족시켰을 때
이런 어노테이션을 달아주면, 원래는 바깥에서 unchecked 경고가 발생해야 하지만, 발생하지 않도록 바꿀 수 있다고 합니다
SupressWarning 대신 unchecked를 없앨 수 있는 방법 중 하나인 거죠
이종 컨테이너
어지간해서 직접 만들 일은 없을 클래스이긴 하지만, 이 부분도 있긴 하니까 다뤄보도록 하겠습니다
타입을 키로, 값으로 실제 객체를 저장하고 싶을 때 사용하는 방법입니다
객체. getClass() 메서드를 통해서 class를 가져오는 방식이 있는데요
이때 문제점은 class를 통해서 가져왔어도 위에서 설명했던 것처럼 generic에 대한 데이터를 알 수 없습니다
List <String> 문자열
List <Integer> 숫자
이 2가지를 키로 저장하려고 하면, 같은 List라는 클래스를 바라보기에, 구별할 방법이 없을 것입니다
이를 해결하기 위한 방법으로 추상 클래스를 만들고, 이를 상속하게 하는 방식으로 클래스의 정의에 generic 데이터를 심어두는 방식입니다
abstract class Key<T> {
private Type type;
public Key() {
type = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
public Type getType() {
return type;
}
}
Key<List<String>> key = new Key<List<String>>() {
};
System.out.println(key.getType());
이렇게 만들었을 경우에, key의 타입을 찍어보면 java.util.List <java.lang.String>이라는 타입을 정확하게 가져올 수 있습니다
이를 키로 만든다면 정확한 타입을 통해서 Key <List <String>>과 Key <List <Integer>>을 구분할 수 있습니다
이를 이용해서 키, 값을 정의한 컨테이너를 타입 이종 컨테이너라고 합니다
읽어보면 좋은 링크들
https://velog.io/@kasania/Java-Generic%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B4%80%EC%B0%B0-2
https://github.com/Java-Bom/ReadingRecord/issues/88
https://docs.oracle.com/javase/tutorial/java/generics/restrictions.html
'Java' 카테고리의 다른 글
Java Exception 알아보기 (0) | 2023.02.27 |
---|---|
Java 초기화 되지 않음을 표현하는 방법 (0) | 2023.02.27 |
Generics 시작하기 (6) | 2023.02.23 |
java에서 입출력 어디까지 테스트 해야할까요? (2) | 2023.02.19 |
Object 의 toString 부터 hashCode 까지 (0) | 2023.02.19 |