- 배열은 공변(covariant), 제네릭은 불공변
- 배열은 실체화(reify) 되지만, 제네릭은 실체화 되지 않는다. (소거)
- new Generic<타입>[배열]은 컴파일 할 수 없다.
배열은 공변(covariant), 제네릭은 불공변
공변 : 같이 변한다.
불공변 : 같이 변하지 않는다.
배열의 공변이란 상속 관계에 있는 배열이 묵시적 형변환이 가능한가? 그렇다는 것이다.
Sub 클래스가 Super 클래스를 상속하고 있는 하위 클래스라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다.
해당 코드를 보자면 Object는 모든 객체의 최상위 클래스로 String 또한 Object의 하위 클래스이다.
그러므로 String 배열은 Object 배열로 담을 수 있다. Refereces 타입의 형변환 같이 부모가 자식을 품어주는 것처럼 더 큰 개념의 일반화된 클래스를 확장한 구체적인 클래스는 일반화된 클래스에 담길 수 있고 담겨져 있던 구체적인 클래스를 다시 구체적인 타입으로 담는다면 그 때는 자기 자신으로 돌아와 명시적인 형변환이 필요하다.
배열도 똑같은 메커니즘으로 작동을하지만 문제가 있다.
해당 구문은 Object에 담겨진 실제로는 String 배열에 숫자를 넣고 있다.
숫자 1이 오토박싱 되며 Integer가 되어 Object 타입으로 넣고 있으니 코드상 컴파일 시점에서는 문제가 되지않아 에러가 나지 않지만 실제 실행하면 ArrayStoreException을 던진다.
*ArrayStoreException : 선언된 배열의 유형에 다른 유형의 객체를 저장하려고 할때 발생하는 예외
반면, 제네릭은 상위 타입과 하위 타입이 의미없이 상속구조 관계라도 묵시적 및 명시적 형변환을 할 수 없다. 한 번 인스턴스화 된 제네릭 타입은 다른 타입으로 변경하거나 담을 수 없다.
Object가 String의 상위타입임에도 불구하고 둘은 그저 다른 타입이라 담을 수 없다. 그래서 컴파일 시점에서 Type mismatch 에러가 난다.
배열이던 제네릭이던 String용 저장소에 Integer를 넣을 수는 없다.
다만 배열에서는 그 실수를 런타임에야 알게 되지만, 리스트를 사용하면 컴파일할 때 바로 알 수있다.
그래서 배열보다는 제네릭을 사용한 리스트를 쓰는 것을 더 추천한다.
배열은 실체화(reify) 되지만, 제네릭은 실체화되지 않는다.(소거)
실체화가 된다는 것은 2002년 월드컵 4강 진출을 꿈 꿨고 실제로 4강 진출의 꿈이 이루어 졌을 때, 어떠한 개념적인 것들 생각만 했던 것들이 실제로 이루어 질 때 실체화라고 생각하면 되겠다.
프로그래밍에서 실체화란 개발자가 작성한 코드는 단순히 도큐멘트, 문서에 불과하지만 이것을 실행하고 실제 메모리에 올라가며 인스턴스화 되는 순간 실체화가 되는 것이다.
배열은 런타임시 자신이 담기로 한 원소의 타입이 어떤 타입인지 인지하고 확인할 수 있기 때문에 String 배열에 Integer가 들어오려고 하면 ArrayStoreException이 발생한다.
반면, 제네릭은 타입 정보가 런타임에는 소거(erasure)된다. 원소 타입을 컴파일 타입에만 검사하며 런타임에는 알수조차 없다는 뜻이다.
소거되는 이유는 제네릭이 지원되기 전 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘으로 자바 5가 제네릭으로 순조롭게 전환될 수 있도록 한 기술이다.
new Generic<타입>[배열]은 컴파일 할 수 없다.
배열은 제네릭 타입, 매개변수 타입, 타입 매개변수로 사용할 수 없다. 즉 코드를 new List<E>[], new List<String>[], new E[] 식으로 작성하면 컴파일 때 제네릭 배열 생성 오류를 일으킨다. 타입 안전하지 않기 때문.
이를 허용하면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 밸생할 수 있다.
런타임에 ClassCastException 발생을 막아주겠다는 제네릭 타입 시스템의 취지에 어긋나는 것이다.
제네릭 배열을 생성하는 코드를 작성 가능하다고 예상했을 때
원소가 하나인 List<Integer>를 생성하고 List<String>의 배열을 Object 배열에 할당한다.
List는 Object의 하위 클래스로 공변이니 아무 문제가 없다.
List<Integer>의 인스턴스인 intList를 Object 배열의 첫 원소로 저장한다. Object 배열에는 List[]이 담긴 것과 마찬가지니 List를 담는 것이 가능하고 런타임 시점에는 제네릭은 다 소거된다. 따라서 런타임시 ArrayStoreException을 일으키지 않는다.
그 후 꺼내서 사용하려고 할 때 String 객체로 형변환 하여 꺼내려는데 실제로는 Integer이므로 런타임에 ClassCastException이 발생한다. 이러한 일을 방지하기 위해 제네릭 배열이 생성되지 않도록 컴파일 에러를 내야 한다.
배열을 썼을 때 문제점과 배열을 List로 바꾸는 법
배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는경우 대부분은 배열인 E[] 대신 컬렉션인 List<E>를 사용하면 해결된다. 코드가 조금 복잡해지고 성능이 조금 나빠질 수 있지만 그 대신 타입 안전성과 상호운용성은 좋아진다.
생성자에서 컬렉션을 받는 이 클래스는 컬렉션 안의 원소 중 하나를 무작위로 선택해 반환하는 choose 메서드를 제공한다.
생성자에 어떤 컬렉션을 넘기느냐에 따라 이 클래스를 주사위판, 매직 8볼, 몬테카를로 시뮬레이션용 데이터 소스 등으로 사용할 수 있다.
*몬테카를로 시뮬레이션은 불확실한 사건의 가능한 결과를 예측하는 수학적 기법입니다. 컴퓨터 프로그램은 이 방법을 사용하여 과거 데이터를 분석하고 조치 선택에 따라 다양한 미래 결과를 예측합니다.
이 클래스를 사용하려면 choose 메서드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해야 한다.
혹시나 타입이 다른 원소가 들어있었다면 런타임에 형변환 오류가 날 것이다.이 것이 배열 기반 코딩에 발생할 수 있는 문제점 중 하나이다.
물론 배열을 Object[] 이 아닌 Integer[] 로 처음부터 만든다면 숫자형식만 받을 수 있으니 문제가 생기지 않는다.
배열은 소거되는 제네릭과는 다르게 런타임시에도 배열 타입이 실체화되니까
물론 받을 때 Object 로 받으면 모든 타입을 출력할 수 있어서 역시 문제가 안되지만 클래스 자체는 Object 배열로 만들어 어떠한 객체든 컬렉션을 담아서 담긴 배열의 값을 랜덤하게 출력하는 범용적인 역할을 하는 클래스이고
이 범용적인 클래스를 사용하는 클라이언트는 숫자타입만을 받고 사용할 것으로 생각하고 사용시 숫자로 형변환해서 사용하는 기능을 만들었다.
하지만 받아온 리스트가 Integer 타입이 아닐 때 Object 배열이라면 숫자 외의 객체들도 들어올 가능성이 있고,
값을 넣을 때는 문제가 안되는데 사용시 형변환이 안되어 캐스팅 Exception이 나는 것이다.
범용적으로 쓸 수 있는 클래스를 만들 때 타입 형변환의 문제를 해결하고자 만든 것이 제네릭이다.
그럼 이제 이 클래스를 제네릭으로 만들어 보면
13번 라인의 오류는 컬렉션으 toArray 메서드가 Object를 반환해서 나는 오류로 Object 배열을 T 배열로 형변환 하면 된다.
그런데 이번엔 경고가 뜨는데
T가 무슨 타입이지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한지 타입 안정성을 보장할 수 없다는 메세지다. 제네릭에서는 원소의 타입 정보가 컴파일 때 소거되어 실제로 코드가 동작중인 런타임에는 무슨 타입인지 알 수 없기 때문이다.
프로그램을 동작하는데는 이상이 없으니 단지 컴파일러가 안전을 보장하지 못하여 나는 비검사 경고로 코드 작성자가 안전하다고 확신한다면 주석을 남기고 애너테이션을 달아 경고를 숨겨도 된다.
하지만 애초에 경고의 원인을 제거하는 편이 훨씬 나으며 배열 대신 리스트를 쓰면 오류나 경고 없이 컴파일 된다.
제네릭을 사용하면 컴파일 타임과 런타임에 타입 안전성을 확보할 수 있다.
성능에 아주 민감한 코드가 아니라면 배열을 리스트로 고치는 것을 추천한다.
@SafeVarags
생성자와 메서드의 제네릭 가변인자에 사용할 수 있는 애노테이션
- 제네릭 가변인자는 근본적으로 타입 안전하지 않다. (가변인자가 배열이니까, 제네릭 배열과 같은 문제)
- 가변 인자 (배열)의 내부 데이터가 오염될 가능성이 있다.
- @SafeVarargs를 사용하면 가변 인자에 대한 해당 오염에 대한 경고를 숨길 수 있다.
Varargs는 가변인자란 뜻으로 자바 5부터 나온 기법이다. 가변인자라는 것은 필요에 따라 매개변수(인자)를 가변적으로 조정할 수 있는 기술이다. 가변인자가 없던 시절에는 컬렉션이나 배열을 이용해서 가변인자를 대체했다.
컬렉션인 Vector에 데이터를 원하는 만큼 넣어서 내부로 전달할 수 있다.
배열은 생성할 배열값을 지정해 줘야하기 때문에 몇 개의 인자가 들어올지 모를 때는 일단 배열갯수를 넉넉하게 잡아야해서 null값이 나온다.
하지만 자바5 에서는 가변인수 기법을 이용하면 보다 쉽게 가변인수를 적용할 수 있다.
메서드에 받는 인자값대로 가변적으로 받을 수 있다.
가변인자는 특별할 것이 없다. 가변인자로 선언할 때 타입과 ...을 붙여주면 된다. 그 다음은 컴파일러가 처리하는데
...이라는 표시는 컴파일러가 배열 형식으로 바꿔버린다.
그리고 매개변수로 주어지는 가변인자들을 모아서 배열 객체로 만들어 버린다.
가변인자를 컴파일러가 처리하는 법
- 매개변수를 배열로 변환한다.
- 원본 : public static void varArgs(String... strs)
- 컴파일 후 : public static void varArgs(String strs[])
- 메서드 호출 시 인자들을 이용해서 배열로 만들어 준다.
- 원본 : varArgs("Hello", "Hey");
- 컴파일 후 : varArgs(new String[] {"Hello", "Hey"});
제네릭 가변인자가 근본적으로 안전하지 않다는 것은
7번라인을 보면 static void notSafe(List<String>[] strLists) 과 같은 말이다 List 배열이 되는 것.
제네릭과 배열을 같이 쓰면 컴파일이 불가했는데 가변인자를 사용하니 제네릭과 배열을 같이 쓰게 되었다.
하지만 이렇게 사용하면 전달받은 List<String>... strLists 이 배열이 오염될 수 있다.
가변인자 매개변수를 통한 잠재적인 힙메모리 오염될 가능성이 있다는 경고를 알려준다.
경고 메세지가 발생하는 이유는 제네릭 타입의 배열이 생기기 때문이다.
그리고 notSafe 메서드 내부 구현은 이전에 보았던 형변환 관련된 문제를 그대로 보여준다.
safe 메서드도 동일한 경고 메세지가 나온다.
제네릭 타입으로 가변인자를 만들었으니 그런데 메서드 내부에서 하는 작업은 배열이 오염이 된다거나 형변환 관련 문제가 발생할 여지가 없는 안전한 코드이다.
만약 제네릭 리스트를 변수에 할당하거나 리턴하는 행위는 위험하니 그런 코드라면 수정이 필요하다.
안전한 코드 구현이라면 @SafeVarargs 애노테이션을 사용하여 경고메세지를 없앨 수 있다.
notSafe 같은 위험한 내부 구현 메서드에서 단지 경고메세지를 없애기 위해 @SafeVarargs 애노테이션을 붙이는 일은 해서는 안된다.
@SuppressWarnings 기능과 비슷하지만 @SafeVarargs는 가변인자 경고 메세지만을 위해 만들어진 애노테이션이다.
https://gyrfalcon.tistory.com/entry/Java-Varargs
https://www.inflearn.com/course/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-2/dashboard
'개인룸 > 도윤' 카테고리의 다른 글
Item32.제네릭과 가변인수를 함께 쓸 때는 신중하라 (0) | 2023.04.22 |
---|---|
Item30. 이왕이면 제네릭 메서드로 만들라 (0) | 2023.04.09 |
Item27. 비검사 경고를 제거하라 (0) | 2023.03.27 |
Item26. 로타입은 사용하지 말라 (0) | 2023.03.26 |
Item.25 톱레벨 클래스는 한 파일에 하나만 담으라 (0) | 2023.03.21 |