제네릭 플플러스 알파

cutiepazzipozzi·2023년 3월 21일
2

지식스택

목록 보기
10/35
post-thumbnail

이펙티브 자바 3판을 통해 알아보는 제네릭의 특징, 그 2번째 이야기!
(글들을 읽고읽고 내 생각을 조금 덧붙여 써 해석이 틀렸을 수도 ,, 잇어요,,)

지금은 막 와닿진 않아도 성장할수록 한문장씩 더 눈에 들어오지 않을까

알아두면 좋은 제네릭 특징ss

1. 제네릭과 가변인수를 함께 쓸 때는 신중하라

여기서 잠깐! 가변인수가 뭐람?

= 메서드의 인자 개수가 고정적이었으나 동적으로 지정해줄 수 있게 만드는 것
= 선언은 타입... 변수명으로 선언 (ex. Object... args)
= 비 구체화(non-refiable) 타입으로 컴파일 타임에 타입 정보가 사라짐

  • 만약 인자가 매개변수가 더 있다면 가변인자를 가장 마지막에 둘 것!
간단한 코드로 알아보는 활용 방법
String add(String s1, String s2) {}
String add(String s2, String s2, String s3) {} 를

String add(String... ss) 하나로 대체할 수 있음!
//인자가 없어도 무방, 배열 형태여도 가능

//매개변수 타입이 배열인 것과의 차이가 궁금할 수 있는데,
//이는 null이나 길이가 0인 배열을 생성할 수 있다는 데서 차이가 있다
  • 그러나 내부적인 배열을 이용하기 때문에 가변인자 메서드를 호출할 때마다 배열이 새로 생성되기 때문에 신중하게 사용해야 한다.

  • 가능하면 가변인수가 있는 메서드는 오버로딩 하지 않는다.

public String add(String... ss) {
	return add("", ss);
}
public String add(String s1, String... ss) {
	String res = "";
    for(String s : ss) {
    	res += s1+s;
    }
    return res;
}

//여기서
String[] args = {"1", "2", "3"};
System.out.println(add("", args));
System.out.println(add("", new String[]{"1", "2", "3"}));
//은 허용되지만
System.out.println(add("", "1", "2", "3"));
//은 오버로딩된 위의 메서드를 구분하지 못해 에러가 발생한다

아무튼 그래서 제네릭과 가변인수를 함께 쓰는 경우로 돌아가보자.

public class AllTogether {
	public static void use(List<String>... list) {
    	List<Integer> intList = List.of(1);
        Object[] object = list;
        object[0] = intList; 
    }
}

컴파일 오류는 발생하지 않지만 호출 시 classCaseException이 발생한다. (사실은 잘못된 타입을 넣어서 발생한 오류임) 어찌보면 당연하다. 형변환 코드를 통해 타입이 String으로 변화했는데 Integer라는다른 타입의 리스트를 집어넣었으니 타입 안정성이 깨질 수 밖에 없다.

그러나 한줄 한줄 뜯어보면 Object 타입에 List 형태의 가변 인수가 들어갈 수 있고, 이 줄이 컴파일이 된다면 String이라는 타입이 제거되고 Object가 들어가기 때문에 지극히 타당한 코드가 된다는 결과가 나온다. 이 상황을 Heap Pollution이라 일컫는다.
(아래의 Heap Pollution에 대한 설명을 읽고 다시 보면 이해가 갈 것이다)

Heap Pollution

= 말 그대로 메모리 영역의 힙 부분에 문제가 발생한다는 의미에서 쓰인다.

-> 보통 이 원인은 제네릭 타입으로부터 시작하는 경우가 많다.

이전 포스팅에서 썼던 말처럼 호환성의 문제가 있는데, 기존의 ArrayList 클래스는 내부적으로 Object 타입으로 저장되었다. 그러나 제네릭이 도입된 이후(Java 5 ~)는 이 클래스가 제네릭이 되어 두 ArrayList의 호환성이 문제로 대두되었다. 그래서 타입 파라미터를 컴파일이 끝난 시점에 제거하도록 하였다. 만약 제네릭 ArrayList로 타입이 String이 였다면, 컴파일이 끝난 후에는 제거되고 Object로 바꾼다는 의미이다.

해결책은 생각보다 간단하다. Collections 클래스에 있는 checkedList를 활용해 예외 상황을 막아준다.

[예시]
try {
  List<String> list = new ArrayList<String>();  
  List<String> tslst = Collections.checkedList(arlst, String.class);  
} catch (IllegalArgumentException e) {
  System.out.println("Exception occurs!");	  
}
//String이 아닌 타입이 온다면 곧바로 예외처리

실제로 제네릭이나 가변인수(varargs)를 사용하는 메서드가 매우 유용하게 사용되기 때문에, 자바 7부터는 @SafeVarargs 애노테이션을 활용해 제네릭 varargs를 받는 메서드를 선언한다.

@SafeVarargs는 개발자가 타입에 대해 안전하다는 표시를 함으로써 컴파일러 경고를 무시할 수 있도록 해주는 애노테이션이다. 따라서 잘 사용해야 한다.
(당연히! 재정의할 수 없는 메서드에만 달아야 한다)
-> 따라서 varargs 배열이 단순히 인수들을 전달하는 목적만 이루도록 하면 된다!

@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
	return new ArrayList<>(a);  
}

2. 타입 안전 이종 컨테이너를 고려해라

이 내용에 대해서 알아보기 전에 나는 컨테이너 부터 찾아봐야 했다.
컨테이너크기가 처음 정해지면 바꿀 수 없는 배열의 단점을 보완하기 위해 등장한 클래스이다.

컨테이너에는 두 가지 종류가 있는데, 우리가 흔히 아는 컬렉션과 맵이다.

  • 컬렉션(Collection)
    = 하나 이상의 규칙이 적용되는 개별적인 요소들을 모은것
    = ex. ArrayList, LinkedList
  • 맵(Map)
    = 연관배열이라고 불리는, key-value 쌍으로 묶인 것

그러나 이 단일 컨테이너에서는 매개변수화 할 수 있는 타입의 수가 제한된다. 그러나 좀 더 유연한 수단이 필요할 때, 컨테이너 대신 키를 매개변수화 하고 컨테이너의 값을 넣고 뺄 때 매개변수화한 키를 함께 제공해주는 설계 방식을 타입 안전 이종 컨테이너 패턴이라고 부른다.

타입 안전 이종 컨테이너인 Favorite 클래스를 예로 들어보자.

public class Favorite {
  	private Map<Class<?>, Object> favorites = new HashMap<>();
	public <T> void putFavorite(Class<T> type, T instance) {
  		favorites.put(Objects.requireNonNull(type), instance);
  	}
  	public <T> T getFavorite(Class<T> type) {
  		return type.cast(favorites.get(type)); //동적형변환
  	}
  	//주어진 인수가 Class 객체가 알려주는 타입의 인스턴스인지를 보고, 	맞다면 그대로 반환하고 아니면 ClassCastException을 던짐!
}

requireNonNull()메서드는 무엇?

objects 클래스에서 제공하는 null체크를 위한 메서드!
1. 같은 NullPointerException을 반환하더라도 더 빠름!
2. 가독성이 더 좋음!

String nono = null;
Objects.requireNonNull(nono);
  
if(nono.equals(null)) {
	throw new NullPointerException("it's null");
} //심지어 얘는 수동으로 작성해줘야 함

굳이 cast 메서드를 사용하는 이유

cast 메서드의 시그니처가 Class 클래스가 제네릭이라는 이점을 활용하고 있기 때문!

public class Class<T> {
	T cast(Object obj);
}

본론으로!!!

Favorites 클래스에서는 키가 와일드카드 타입이기 때문에, 모두 다른 키가 서로 다른 매개변수화 타입이 될 수 있다.
(ex. Class<Integer>, Class<String>)

문제점

  1. 악의적인 client가 class 객체를 로 타입으로 넘기면 타입 안정성이 깨진다.
    => 해결 가능! putFavorite 코드에서 동적 형변환을 사용하면 됨
public <T> void putFavorite(Class<T> type, T instance) {
  		favorites.put(Objects.requireNonNull(type), type.cast(instance));
  	}
// checkedSet, checkedList, checkedMap 메서드가 그 예시!!!
  1. 실체화 불가 타입에는 사용할 수 없다.
    (실체화 = 런타임 시 자신이 담기로 한 원소의 타입을 인지하고 확인)
    = ex. List<String>.class라는 문법 X
    (List<String>용 Class 객체를 얻을 수 없음, 왜냐면 List<Integer>List<String>이나 같은 List.class를 공유하고 있기 때문에)
    => 나름의 해결책이 슈퍼 타입 토큰이지만 완전한 해결책이 아니라는...

슈퍼 타입 토큰

= ParameterizedTypeReference라는 클래스로 미리 구현해놓음

Ex.
favorite.put(new TypeRef<List<String>>(){}, pets);
List<String> listOfStrings = favorite.get(new TypeRef<List<String>>(){});

=> 타입토큰 = 클래스 타입을 나타낼 수 있는 최소한의 정보
void 함수(class<?> param)에서 인자가 타입 토큰

++더 추가 예정...

참고 (감사합니다ㅠㅠㅠ)

https://velog.io/@minseojo/Java-%EA%B0%80%EB%B3%80%EC%9D%B8%EC%9E%90-varargs
https://madplay.github.io/post/effectivejava-chapter5-generics
https://codemasterkimc.tistory.com/402
https://velog.io/@adduci/Java-%ED%9E%99-%ED%8E%84%EB%A3%A8%EC%85%98-Heap-pollution
https://www.geeksforgeeks.org/collections-checkedlist-method-in-java-with-examples/
https://applefarm.tistory.com/153
https://velog.io/@kasania/Varargs%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B4%80%EC%B0%B0
https://hudi.blog/java-requirenonnull/
https://donghyeon.dev/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C%EC%9E%90%EB%B0%94/2021/04/24/%ED%83%80%EC%9E%85-%EC%95%88%EC%A0%84-%EC%9D%B4%EC%A2%85-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88/
https://sungminhong.github.io/spring/superTypeToken/

profile
노션에서 자라는 중 (●'◡'●)

0개의 댓글