1. 제너릭
아래의 코드에서 s.value
- 컴파일 타임에서는 컴파일러가 미리 체크를하고, 캐스팅해준다.
- s.value = "String" ==(컴파일러)==> s.value = (String) "String";
- 런타임에서 value 의 타입은 Object이다.
- type erasure 가 타입 파라미터를 지운다.
public class TypeToken {
static class Generic<T> {
T value;
void set(T t) {}
T get() {return null;}
}
public static void main(String[] args) throws Exception {
Generic<String> s = new Generic<>();
s.value = "String";
Generic<Integer> i = new Generic<>();
i.value = 1;
}
}
용어
- 타입 파라미터 : Generic<String> 에서 String 를 타입 파라미터라고 부른다.
2. 강제 캐스팅
강제 캐스팅은 컴파일내에는 오류를 발생시키지 않지만, 런타임시에 오류를 만들 수 있으므로 위험하다.
// ClassCastException 발생
public class TypeToken {
public static void main(String[] args) {
Object o = "String";
Integer i = (Integer) o;
System.out.println("i = " + i);
}
}
반면에 제너릭정보와 클래스정보를 사용하면 안전한 방식으로 (type safe) 사용할 수 있다.
public class TypeToken {
<T> T create(Class<T> clazz) throws Exception {
return clazz.newInstance();
}
}
3. 안전한 Map을 만들어보자.
가정
- Map 안에 여러타입을 넣을수 있는 Map을 한번 만들어보자.
public class TypeToken {
static class TypeSafeMap {
Map<String, Object> map = new HashMap<>();
void run() {
map.put("a", "a");
map.put("b", 1);
Integer i = (Integer) map.get("b");
String s = (String) map.get("a");
}
}
}
위의 코드는 강제 타입변환을 하므로 위험하다. 더 안전하게 만드려면 어떻게해야할까?
이번에도 Map을 만들지만 Map의 key 를 클래스타입(Class<?>)으로 만들어서 만들어보자.
- 클래스타입에 대한 키는 하나밖에 못만들겠지만 일단 진행
- key 에는 클래스타입을, value 에는 key 의 타입에 맞는 값을 넣는다는 목표로 만든다.
아래의코드는 컴파일오류로는 체크하지못하는, 런타임시에 에러가 생기는 코드이다.
public class TypeToken {
static class TypeSafeMap {
Map<Class<?>, Object> map = new HashMap<>();
void put(Class<?> clazz, Object value) {
map.put(clazz, value);
}
}
public static void main(String[] args) {
TypeSafeMap m = new TypeSafeMap();
// 조금 안전한가 싶지만 아래의 코드도 컴파일 오류를 일으키지 않는다.
m.put(Integer.class, "Value");
}
}
개선된 코드
- 아래의 코드처럼 put 함수에 제너릭을 사용하고나니, 컴파일오류가 발생한다.
- 컴파일러가 "Value" 를 (Integer) 로 캐스팅하려하니 에러가 나는 모습!
public class TypeToken {
static class TypeSafeMap {
Map<Class<?>, Object> map = new HashMap<>();
<T> void put(Class<T> clazz, T value) {
map.put(clazz, value);
}
}
public static void main(String[] args) {
TypeSafeMap m = new TypeSafeMap();
// 컴파일오류가 발생한다.
m.put(Integer.class, "Value");
}
}
위의 코드정도면 안전하다고 생각하여 get() 메소드를 만들어보니
- map 의 value 타입이 Object 이기때문에 타입캐스팅을 강제해야한다. -> 안전하지 못한 코드
public class TypeToken {
static class TypeSafeMap {
Map<Class<?>, Object> map = new HashMap<>();
<T> void put(Class<T> clazz, T value) {
map.put(clazz, value);
}
<T> T get(Class<T> clazz) {
return (T) map.get(clazz);
}
}
public static void main(String[] args) {
TypeSafeMap m = new TypeSafeMap();
m.put(Integer.class, 1);
m.put(String.class, "String");
m.put(List.class, Arrays.asList(1, 2, 3));
}
}
(T) map.get(clazz) 를 사용하지말고 Class 타입이 제공하는 cast 를 사용하자
- 결과까지 출력되는 모습을 보니 이제는 강제캐스팅이 없는 TypeSafe 한 Map을 만들었다.
public class TypeToken {
static class TypeSafeMap {
Map<Class<?>, Object> map = new HashMap<>();
<T> void put(Class<T> clazz, T value) {
map.put(clazz, value);
}
<T> T get(Class<T> clazz) {
return clazz.cast(map.get(clazz));
}
}
public static void main(String[] args) {
TypeSafeMap m = new TypeSafeMap();
m.put(Integer.class, 1);
m.put(String.class, "String");
m.put(List.class, Arrays.asList(1, 2, 3));
System.out.println(m.get(Integer.class));
System.out.println(m.get(String.class));
System.out.println(m.get(List.class));
}
}
이처럼 특정 타입의 클래스정보를 넘겨서 타입안정성을 꿰하는 기법을
- Type Token 기법이라고 한다.
4. Super Type Token
이후, 위의 TypeSafeMap에 아래와같이 리스트를 추가하려고한다.
- 하지만 List<Integer> / List<String> 은 추가할 수 없다.
public class TypeToken {
public static void main(String[] args) {
TypeSafeMap m = new TypeSafeMap();
m.put(List<Integer>.class, Arrays.asList(1, 2, 3));
m.put(List<String>.class, Arrays.asList("a", "b", "c"));
}
}
아래의 코드를 수행해보자
- Object 가 출력된다.
- 이유는 type erasure 가 타입 파라미터의 정보를 런타임시에는 지워버리기 때문이다.
- 과거의 java에는 List (List<String>, List<Integer>) 등이 아닌 그냥 List 를 사용
- 이에 대한 호환성을 맞추기위해서 자바는 타입파라미터정보를 런타임에는 지우는 type erasure 방식을 사용
public class SuperTypeToken {
static class Sup<T> {
T value;
}
public static void main(String[] args) throws NoSuchFieldException {
// erasure 에 의해서 Object 만 출력됨, 리플렉션으로도 타입의 정보를 알 수 없음 (바이트코드 호환성문제에 의해서 삭제됨)
Sup<String> s = new Sup<>();
System.out.println(s.getClass().getDeclaredField("value").getType());
}
}
하지만 아래의 코드에서는 type erasure 가 동작하지 않는다.
- 출력의 결과는 String 이 반환된다.
- Sup<List<String>> 이면 List<String> 이 반환 (중첩된 정보도 반환된다.)
- 새로운 타입을 정의하면서 슈퍼클래스를 Generic 클래스로 타입을 지정하여 상속하면 런타임에도 정보가 남아있음
public class SuperTypeToken {
static class Sup<T> {
T value;
}
static class Sub extends Sup<String> {
}
public static void main(String[] args) throws NoSuchFieldException {
// erasure에 의해서 없어지지 않음
Sub b = new Sub();
Type t = b.getClass().getGenericSuperclass();
ParameterizedType ptype = (ParameterizedType) t;
System.out.println(ptype.getActualTypeArguments()[0]);
}
}
5. Local / Anonymous Local Class
그렇다면 Local 클래스로 만들어서 타입정보를 가져오면 되겠구나
public class SuperTypeToken {
static class Sup<T> {
T value;
}
public static void main(String[] args) {
class Sub extends Sup<List<String>> {
}
Sub b = new Sub();
Type t = b.getClass().getGenericSuperclass();
ParameterizedType ptype = (ParameterizedType) t;
System.out.println(ptype.getActualTypeArguments()[0]);
}
}
한걸음 더 나아가서, 익명 Local 클래스로 만들어서 타입정보를 가져올 수 있다.
public class SuperTypeToken {
static class Sup<T> {
T value;
}
public static void main(String[] args) {
Sup b = new Sup<List<String>>() {
};
Type t = b.getClass().getGenericSuperclass();
ParameterizedType ptype = (ParameterizedType) t;
System.out.println(ptype.getActualTypeArguments()[0]);
}
}
아래와같이 사용할 수 있다.
public class TypeToken {
static class TypeReference<T> {
Type type;
public TypeReference() {
Type stype = getClass().getGenericSuperclass();
if(stype instanceof ParameterizedType) {
this.type = ((ParameterizedType)stype).getActualTypeArguments()[0];
}
else throw new RuntimeException();
}
}
public static void main(String[] args) {
TypeReference t = new TypeReference<String>(){
};
System.out.println(t.type);
}
}
6. TypeReference<T> 를 상속한 익명클래스를 이용한 Map
TypeReference<T> 를 상속한 익명클래스를 Key 로 저장
- 이를통해 value 를 따로 가져오는 코드이다.
- 자세한 설명은 토비의봄 수퍼타입토큰을..
public class TypeToken {
static class TypeSafeMap {
Map<Type, Object> map = new HashMap<>();
<T> void put(TypeReference<T> tr, T value) {
map.put(tr.type, value);
}
<T> T get(TypeReference<T> tr) {
if(tr.type instanceof Class<?>) {
return ((Class<T>)tr.type).cast(map.get(tr.type));
} else {
return ((Class<T>)((ParameterizedType)tr.type).getRawType()).cast(map.get(tr.type));
}
}
}
static class TypeReference<T> {
Type type;
public TypeReference() {
Type stype = getClass().getGenericSuperclass();
if(stype instanceof ParameterizedType) {
this.type = ((ParameterizedType)stype).getActualTypeArguments()[0];
}
else throw new RuntimeException();
}
}
public static void main(String[] args) {
TypeSafeMap m = new TypeSafeMap();
m.put(new TypeReference<Integer>(){}, 1);
m.put(new TypeReference<String>(){}, "String");
m.put(new TypeReference<List<Integer>>(){}, Arrays.asList(1, 2, 3));
m.put(new TypeReference<List<String>>(){}, Arrays.asList("a", "b", "c"));
System.out.println(m.get(new TypeReference<Integer>(){}));
System.out.println(m.get(new TypeReference<String>(){}));
System.out.println(m.get(new TypeReference<List<Integer>>(){}));
System.out.println(m.get(new TypeReference<List<String>>(){}));
}
}
이처럼 TypeReference 의 제너릭을 상속하여 익명클래스로 만들어 타입의 정보를 가져오는 방식을
- 슈퍼타입토큰 방식이라고 한다.
언제쓰는가?
public class SpringTypeReference {
public static void main(String[] args) {
RestTemplate rt = new RestTemplate();
// List<User> users = rt.getForObject("http://localhost:8080", List.class); // 타입토큰방식
// System.out.println("users.get(0).getName() = " + users.get(0).getName()); // <- 에러
List<User> supertypeUsers = rt.exchange("http://localhost:8080",
HttpMethod.GET, null, new ParameterizedTypeReference<List<User>>(){}).getBody();
supertypeUsers.forEach(System.out::println);
}
}
7. Resolvable Type
- Reflection.Type 을 이용하여 타입 정보를 가져오는 것이 예외처리도 너무 많고 불편함
- 이를 스프링에서 추상화하여 사용할 수 있도록 만들었다.
예시) TypeReference 는 위에서 구현한 클래스를 재사용
public static void main(String[] args) {
// ResolvableType
ResolvableType rt = ResolvableType.forInstance(new TypeReference<List<String>>(){});
System.out.println("rt.Type = " + rt.getSuperType().getGeneric(0).getType());
System.out.println("rt.nestedType = " + rt.getSuperType().getGeneric(0).getNested(1).getType());
// 자바에서 type erasure 을 통해 타입이 지워지므로 unResolved
System.out.println("unResolved = " + ResolvableType.forInstance(new ArrayList<String>()).hasUnresolvableGenerics());
// 가지고오려면 superclass 를 사용해야함
System.out.println("resolved = " + ResolvableType.forInstance(new TypeReference<ArrayList<String>>(){}).hasUnresolvableGenerics());
}
'토비의봄' 카테고리의 다른 글
4.2. Reactive Streams (0) | 2021.02.28 |
---|---|
4.1. Reactive Streams (0) | 2021.02.28 |
3.2. Generics (0) | 2021.02.25 |
3.1. Generics (0) | 2021.02.24 |
1. 재사용성과 다이나믹 디스패치, 더블 디스패치 (0) | 2021.02.20 |