본 글은 2021년 12월 13일 에 기록되었다.
순서상으로는 Effective Java 중 첫 번째의 포스트 에 해당한다.
이주 전 Generics 를 배우고 Java : Generics 라는 포스트를 작성하였다.
정확히 모르는 개념에 대해서 두리뭉술하게 표현 점들과 앞으로 많은 부분에서 지네릭이 보이는 점들로, 첫 번째 공부 순서를 지네릭으로 하는 것이 맞다라는 생각을 하게 되었다.
본 내용은 지네릭을 알고 있는 사람을 전제로 하고 있는 것 같다.
따라서 본인이 지네릭에 대해서 잘 모른다면 Java : Generics 이 포스트를 보고 오는 것을 추천한다.
Java : Generics 을 처음 작성할 때는 지네릭의 목적이 다음과 같다고 생각했다.
ArrayList <String> 는 배열원소로 String 타입받게 받지 못한다.
따라서 나는 이를 매개변수화 타입 <E>로 선언한 값만 들어올 수 있게 제한하는 기능 이라고 생각했다.
물론 기능적으로는 그런 결과를 만들지는 몰라도 지네릭의 목적성은 클래스나 메서드의 매개변수에 따라 일일히 오버라이딩 하지 않고도 여러 타입에 대응되게 만드는 것 인 것 같다.
로타입 ArrayList
비한정적 와일드 카드 ArrayList < ? >
한정적 와일드 카드 ArrayList <E extends String>
다음의 아이템들로 실 사용 사례를 조금 더 보도록 하겠다.
이름 | 예시 | 설명 | |
---|---|---|---|
1 | 일반 지네릭 | ArrayList<String> | String 타입만을 받아들이는 ArrayList 가 만들어진다. |
2 | 로 타입 | ArrayList | 최초 대입한 데이터의 타입만 받아들이는 ArrayList 가 만들어진다. |
3 | 비한정적 와일드 카드 | ArrayList< ? > | null 이외의 값을 받아들이지 않는 ArrayList 가 만들어진다. [Java] 와일드카드 (wildcards) |
4 | 한정적 와일드 카드 | ArrayList< ? extends String /> ArrayList< ? super String /> | 여기서부터 이해하지 못한 내용이다. |
지네릭을 사용하는 방법은 크게 두 가지가 있다.
// 어떤 타입이든 받아들일 수 있는 로 Collection
private final Collection bigIntegers= ...;
// BigInteger 을 받아들이는 지네릭 Collection
private final Collection<BigInteger> bigInstgers= ...;
로타입은 말 그대로 어떤 타입이든 받아들일 수 있다.
그렇기 때문에, 로타입 변수를 사용하면 다음과같은 문제가 발생할 수 있다.
위에 대한 자세한 예시는 Collection 클래스를 사용하는 경우로,
구글링을 하거나 본 도서 154페이지를 체크해보는 것을 추천한다.
가장 대표적인 경우는 지네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경쓰고 싶지 않을 때이다. 이럴 때 어떤 타입이든 받을 수 있게 로타입 매개변수를 사용하게 된다.
그러나 이러한 경우에는 비한정적 와일드 카드 연산자 ( ? ) 를 사용하도록 하자. 관련된 내용을 조금 더 풀어 써놓은 포스트 를 체크업해보자.
핵심정리
로타입을 사용하면 런타임에 에러가 생길 수 있으니 사용하면 안된다. (이후 생략)
해당 부분은 약간의 @Annotation 과 관련된 부분도 일부 있다.
사례 1 | 잘못된 지네릭 문법
Set<String> stringSet=new HashSet(); // 에러
Set<String> stringSet=new HashSet<String>(); // 정상
Set<String> stringSet=new HashSet<>(); // jdk 1.7 의 다이아몬드 연산자 사용
사례 2 | 잘못된 @Annotation 부터 올바른 @Annotation 까지
// 잘못된 사용 | 사용할 수는 있으나 너무 넓은 범위에 사용해서는 안된다.
@SuppressWarnings("resource")
String methodName() {
Scanner scan=new Scanner(System.in);
return scan.next();
}
// 잘못된 사용 | return 앞에는 Annotation 을 붙일 수 없다.
String methodName() {
Scanner scan=new Scanner(System.in);
@SuppressWarnings("resource") return scan.next();
}
// 올바른 사용
String methodName() {
@SuppressWarnings("resource") Scanner scan=new Scanner(System.in);
return scan.next();
}
// 최선의 사용 | 경고문구를 무시해도 되는 이유까지 명시해야 한다.
String methodName() {
// 시스템 전체에서 Scanner 나 Stream 은 여기서만 쓰이므로 무시해도 괜찮다.
@SuppressWarnings("resource") Scanner scan=new Scanner(System.in);
return scan.next();
}
핵심 정리
모든 비검사 경고는 런타임에 ClassCastException 을 일으킬 수 있는 잠재적 가능성을 뜻하니 최선을 다해서 제거하자.
경고를 없앨 수 없다면 그 코드가 타입 안전함을 증명하고 가능한 범위를 좁혀서 @SuppressWarnings("unchecked") Annotation 으로 경고를 숨겨라.
그런 다음 그러한 판단의 근거를 주석으로 남겨라.public <T> T[] toArray(T[] a){ if(a.length<size){ // 생성한 배열과 매개변수로 받은 배열의 타입이 모두 T[] 므로 올바른 형변환이다. @SuppressWarnings("unchecked") T[] result= (T[]) Arrays.copyOf(elements,size, a.getClass(); return result; } System.arraycopy(elements, 0, a, 0, size); if (a.length > size) a[size]=null; return a; }
배열보다 리스트를 사용해야 하는 이유는 다음과 같이 정리된다.
또한 배열과 리스트는 공존될 수 없는 특징이 있는데 이는 타입 불안정성 을 가지게 되기 때문이다.
A 가 B 의 하위 타입이라면 배열 A[] 는 B[]의 하위 타입이 된다. 즉, 공변 관계이다.
그러나 List<A> 와 List<B> 는 서로 상위타입도 하위 타입도 아니다. 즉 불공변 관계이다.
// 런타임 에러 발생 : 실행이 되며 ArrayStoreException 발생
// **배열의 실체화** 를 언급한 이유가 이것 때문이다.
Object[] objectArray=new Long[1];
objectArray[0]="타입이 달라 넣을 수 없다.";
// 컴파일 에러 발생 : 실행 조차 되지 않는다.
// 제네릭은 컴파일 이후에는 **매개변수 타입** 을 드랍하고 신경쓰지 않는다.
// 그러한 특징 때문에 E, List<E> List<String> 과 같은 타입을 실체화 불가 타입이라고도 부른다.
List<Object> objectList=new ArrayList<Long>();
objectArray.add("타입이 달라 넣을 수 없다.");
// 에러 발생 : ClassCastException 발생
List<String>[] stringLists=new List<String>[];
public class Chooser {
private final Ojbect[] choiceArray;
public Chooesr(Collection choices){
choiceArray=choices.toArray();
}
public Object choose(){
Random rnd=ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
public class Choocer {
private final T[] choiceArray;
public Chooesr(Collection<T> choices){
choiceArray=choices.toArray(); // Object[] cannot be convereted to T[] - 타입 불일치
choiceArray=(T[]) choices.toArray(); // [unchecked] unchecked cast - 비검사 경고
@SuppressWarnings("unchecked") choiceARray=(T[]) choices.toArray();
} // 하지만 최선의 선택이 아니므로 3번을 확인하자
public Object choose(){
Random rnd=ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
public class Chooser {
private final List<T> choiceArray;
public Chooser(Collection<T> choices){
choiceArray=new ArrayList<T>(choices); // ok : 매개변수화 타입 연산자 <T>
choiceArray=new ArrayList<>(choices); // ok : 다이아몬드 연산자 <>
}
public T choose() {
Random rnd=ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
전술한 ### 아이템 28 의 초반부에서 언급했듯이,
배열과 리스트는 매우 다른 타입 규칙이 적용되고 있다.
그렇기 때문에 둘을 섞어쓰면 높은 확률로 에러 및 경고를 만나게 되고
이러한 경우를 해결하기 어렵다면 하나만 사용하는 것(기왕이면 제네릭)이 좋다.
public class Stack {
private Object[] elements;
private int size=0;
private static final int DEFAULT_INITIAL_CAPACITY]=16;
public Stack() {
elements=new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++]=e;
}
public Object pop() {
if(size==0)
throw new EmptyStackException();
Object result=elements[--size];
return result;
}
public boolean isEmpty() {
return size==0;
}
public void ensureCapacity() {
if(elements.length==size)
elements=Arrays.copyOf(elements, 2*size+1);
}
}
public class Stack<E> {
private E[] elements;
private int size=0;
private static final int DEFAULT_INITIAL_CAPACITY=16;
public Stack() {
elements=new E[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++]=e;
}
public E pop() {
if(size==0)
throw new EmptyStackException();
E result=elements(--size);
return result;
}
}
// 배열 elemtns 는 push(E) 로 넘어온 E 인스턴스만 담기에 타입 안정성이 확보된다.
// 이 배열의 런타임 타입은 E[] 가 아닌 Object[] 이다.
@SuppressWarnings("unchecked")
public Stack() {
elements=(E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
// 비검사 경고를 적절히 숨기고
// 다 쓴 참조를 해제한다.
public E pop() {
if(size==0)
throw new EmptyStackException();
@SuppressWarnings("unchecked") E result=(E) elements(--size);
elements[size]=null;
return result;
}
선택지 1은 Object 배열을 만들어 매개변수화 타입 으로 캐스팅하는 방법 이다.
선택지 2는 비검사 형변환을 수행하는 할당문에 @Annotation 을 적용하는 방법 이다.
선택지 1은 코드도 더 짧고 직관적이다.
또한 형변환을 배열 생성 시에만 한 번 하면 된다.
그렇기 때문에 현업에서 해당 코드를 많이 사용한다.
하지만, 이 코드는 힙 오염 | Heap Pollution 을 일으킬 수 있다.
선택지 2는 코드가 더 길지만 비검사 경고가 더 좁은 범위에 사용되어 있다.
하지만, 형변환을 매 메서드 실행 시마다 해야 하기도 한다.
그렇기 때문에 자주 사용되지는 않는다.
하지만, 선택지 1의 힙오염 이슈가 해가 되는 경우에는 이 방법이 대안이 될 수 있다.
핵심 정리
클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다.
그러니 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라.
그렇게 하려면 제네릭 타입으로 만들어야 하는 경우가 많다.
기존에 제네릭이었어야 하는 것이 있다면 제네릭 타입으로 변경하자.
기존 클라이언트에는 아무런 영향을 주지 않으면서 새로운 사용자를 훨씬 편하게 해주는 기능이다.