[Effective Java] Day 25 - Item 27, 28 :: 비검사 경고를 제외하라, 배열보다는 리스트를 사용하라
Day25에서는 item 27,28에 대한 내용을 다룬다.
Item 27 :: 비검사 경고를 제외하라
제너릭을 활용하면 아래와 같은 컴파일러의 비검사 경고를 볼 수 있다.
- 비검사 형변환 경고(unchecked cast warning)
- 비검사 메서드 호출 경고(unchecked method invocation warning)
- 비검사 매개변수화 가변인수 타입 경고(unchecked parameterized vararg type warning)
- 비검사 변환경고(unchecked conversion warning) 등
비검사 경고 예
javac 명령줄 인수에 -Xlint:unchecked
를 추가하면 뭐가 잘못됐는지 위와 같이 설명해준다.
인텔리제이에서 명령줄 인수를 추가하기 위해서는 Settings - Build, Execution, Deployment - Compiler - Java Compiler에서 설정할 수 있다.
위 소스는 아래와 같이 바꾸면 경고가 사라진다. 자바 7부터는 다이아몬드 연산자인 <>
로 타입 매개변수인 <String>
를 대체할 수 있다. 컴파일러는 타입 매개변수를 추론한다.
Set<String> exaltation = new HashSet<String>();
할 수 있는 한 모든 비검사 경고를 제거하라.
경고를 제거할 수는 없지만 타입이 안전하다고 확신할 수 있다면 @SuppressWarnings(“unchecked”) 애너테이션을 달아 경고를 숨기자.
@SuppressWarnings
/*
* The string {@code "unchecked"} is used to suppress
* unchecked warnings. Compiler vendors should document the
* additional warning names they support in conjunction with this
* annotation type. They are encouraged to cooperate to ensure
* that the same names work across multiple compilers.
* @return the set of warnings to be suppressed
*/
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
- 개별 지역변수 ~ 클래스 전체까지 선언할 수 있다.
- 가능한 한 좁은 범위에 적용하자.
- ex) 변수 선언, 아주 짧은 메서드, 생성자
- 절대로 클래스 전체에 적용해서는 안된다.
- 한 줄이 넘는 메서드나 생성자에 달린 @SuppressWarnings 애너테이션을 발견하면 지역변수 선언쪽으로 옮기자.
- 이를 위해 지역변수를 새로 선언하는 수고를 해야 할 수도 있지만, 그만한 값어치가 있을 것이다!
- ex) ArrayList의 toArray 메서드
@SuppressWarnings("unchecked") //unchecked: 미확인 오퍼레이션과 관련된 경고 억제 public <T> T[] toArray(T[] a) { if (a.length < size) // Make a new array of a's runtime type, but my contents: return (T[]) Arrays.copyOf(elementData, size, a.getClass()); System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; }
실제로는 메서드에 @suppressWarnings가 달려있지만 더 좋은 방법이 있다.
반환값을 담을 지역변수를 하나 선언하고 그 변수에 애너테이션을 달아주는 방법이다.public <T> T[] toArray(T[] a) { if (a.length < size) // 생성한 배열인 result와 매개변수로 받은 배열의 타입도 T[]로 형변환 하여 같으므로 // 올바른 형변환이다. @SuppressWarnings("unchecked") T[] result = (T[]) Arrays.copyOf(elementData, size, a.getClass()); return result; System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; }
- 장점
- 컴파일이 깔끔하게 된다.
- 비검사 경고를 숨기는 범위를 최소로 좁힐 수 있다.
- @SuppressWarnings(“unchecked”) 애너테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남기자.
- 다른 사람이 그 코드를 이해하는데 도움이 된다.
- 다른 사람이 그 코드를 잘못 수정하여 타입 안전성을 잃는 상황을 줄여준다.
결론
- 최선을 다해 모든 비검사 경고를 제거하라
- 모두 제거하면 타입 안정성이 증명된다.
- ClassCastException이 발생할 일이 없다.
- 경고를 제거할 수 없고 타입이 안전하다고 확신할 수 있다면
@SuppressWarnings("unchekced")
를 달아 경고를 숨겨라- 경고를 숨기기로 한 근거를 주석으로 남겨라
참고
Item 28 :: 배열보다는 리스트를 사용하라
배열과 리스트(제너릭 타입)의 차이
1. 배열 (Array)
1) 배열은 함께 변한다. 즉, 공변(convariant)이다.
- Sub extends Super인 경우, Sub[] extends Super[]이다.
class Sub extends Super {
}
Super[] superman = new Sub[1];
2) 배열은 실체화(reify)된다.
- 컴파일타임: 타입 불안전 / 런타임: 타입 안전
Object[] objectArray = new Long[1]; objectArray[0] = "String을 넣어봅시다!";
Object 배열에 Long용 저장소를 할당했다.
그러므로 objectArray에는 Long타입만 올 수 있다. String은 못온다.
그런데 위 코드는 컴파일 에러가 나지 않는다.Exception in thread "main" java.lang.ArrayStoreException: java.lang.String at item28.BetweenArrayAndList.main(BetweenArrayAndList.java:7)
런타임에서 에러가 난다.
→ 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인하기 때문
2. 리스트 (List)
1) 리스트는 함께 변하지 않는다. 즉, 불공변(invariant)이다.
- 상위-하위타입 관계가 아니다.
List<Super> superList = new ArrayList<Sub>();//컴파일 에러!
2) List는 실체화(reify)되지 않는다.
- 컴파일타임: 타입 안전 / 런타임: 타입 불안전
//컴파일 에러 List<Object> objList = new ArrayList<Long>(); objList.add("String을 넣어봅시다!");
리스트는 컴파일할 때 바로 알 수 있다. 단, 런타임 시 타입정보가 소거된다.
원소 타입을 컴파일 타임에만 검사, 런타임에는 알 수 조차 없다.
→ 소거: 제너릭이 지원되기 전의 레거시 코드와 제너릭 타입을 함께 사용할 수 있게 해주는 메커니즘
자바5가 제너릭으로 순조롭게 전환될 수 있도록 해준 일등공신!
실체화(reify)
- 실체화: JVM이 type 정보를 완전히 알게 되거나, 알게 하는 것, 또는 알고 체크하는 것
- 실체화 가능 타입(reifiable type)
- 매개변수화 타입 가운데 실체화 될 수 있는 타입은 비한정적 와일드카드 뿐이다.
- ex)
List<?>
,Map<?,?>
- 실체화 불가 타입(non-reifiable type)
- 실체화되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가진다.
- ex) E,
List<E>
,List<String>
제너릭 배열을 만들지 못하게 막은 이유는?
1. 배열과 제너릭은 잘 어우러지지 못한다.
new List<E>[]
, new List<String>[]
, new E[] → 컴파일 시 오류 발생!
2. 타입 안전하지 않다.
위 두가지 이유 때문인데 자세한건 아래 예시를 보고 이해해보자.
ex) 제너릭 배열 생성이 허용된다 가정
List<String>[] stringList = new List<String>[1]; // (1) 허용된다 가정
List<Integer> initList = List.of(42); // (2)
Object[] objects = stringList; // (3)
objects[0] = initList; // (4)
String s = stringList[0].get(0); // (5)
(1)처럼 제너릭 배열이 생성이 허용된다고 일단 가정해보자.
(2)에서 원소가 42 하나인 List<Integer>
를 생성한다.
(3)은 (1) 에서 생성한 List<String>
배열을 Object 배열에 할당한다. 즉, Object 배열은 List<String>
배열을 참조하게 된다. 배열은 공변이므로 가능하다.
(4)는 Objects 배열의 첫 원소에 원소가 42 하나인 initList를 저장한다.
- 문제점
List<String>
인스턴스만 받겠다고 선언한 stringList 배열에List<Integer>
인스턴스가 들어가있다.
(5)는 이 배열의 첫 리스트에서 원소를 꺼내려 한다. 컴파일러는 꺼낸 원소 42를 자동으로 String으로 형변환한다.
String s = (String) 42; // ClassCastException 발생
이런 일을 방지하기 위해서 제너릭배열이 생성되지 않도록 (1)에서 컴파일 오류를 내야하는 것이다.
배열대신 List처럼 원소를 관리하자
배열로 형변환할 때 제너릭 배열 생성오류 or 비검사 형변환 경고가 뜨는 경우
→ 배열인 E[] 대신 컬렉션인 List<E>
를 사용하자!
ex) 생성자에서 컬렉션을 받는 Chooser 클래스
이 클래스는 컬렉션 안의 원소 중 하나를 무작위로 반환하는 choose 메서드를 제공한다.
제너릭 미사용 버전
public class Chooser {
private final Object[] choiceArray;
public Chooser(Collection choices){
choiceArray = choices.toArray();
}
public Object choose(){
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
이 클래스를 사용하려면 choose 메서드 호출 시 마다 반환된 Object를 원하는 타입으로 형변환 해야한다.
혹시나 원하는 타입과 다른 타입의 원소가 들어있는 경우 런타임 시 형변환 오류가 발생할 것이다.
제너릭 사용 버전 1
public class Chooser2<T> {//제너릭으로 변환
private final T[] choiceArray;
//@SuppressWarnings("unchecked") // 비검사 형변환 경고(unchecked cast경고)
public Chooser2(Collection<T> choices){
choiceArray = (T[]) choices.toArray(); //unchecked cast: 'java.lang.Object[]' to 'T[]'
}
public Object choose(){
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
choiceArray = (T[]) choices.toArray();
에서 unchecked cast: ‘java.lang.Object[]’ to ‘T[]’ 경고가 발생한다.
- T[]가 무슨 타입인지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한지 보장할 수 없다라는 뜻
- 제너릭에는 원소의 타입 정보가 소거되어 런타임에는 무슨 타입인지 알 수 없다.
- 컴파일러가 안전을 보장하지 못할 뿐 소스코드는 동작하며, List에 비해 빠르다.
- 코드작성자가 안전하다고 확신한다면 주석을 남기고 애너테이션을 달아 경고를 숨겨도 된다.
- 단, 경고의 원인을 완전히 제거하는게 훨씬 좋은 방법이다(item 27)
제너릭 사용 버전 2
public class Chooser3<T> {//제너릭으로 변환
private final List<T> choiceList;//리스트로 변경
public Chooser3(Collection<T> choices){
choiceList = new ArrayList<>(choices);
}
public Object choose(){
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceList.size()));
}
}
- 장점: 타입 안전성과 상호운용성이 좋아진다. 런타임이 아닌 컴파일시점에 에러를 잡아준다.
- 단점: 코드가 조금 복잡해진다. 성능이 살짝 느려질 수 있다.
결론
- 배열과 리스트를 섞어쓰기란 쉽지 않다.
- Array: 공변. 실체화된다.
- 컴파일타임: 타입 불안전 / 런타임: 타입 안전
- List: 불공변. 타입정보가 소거된다.
- 컴파일타임: 타입 안전 / 런타임: 타입 불안전
- Array: 공변. 실체화된다.
- 섞어쓰다가 컴파일 오류나 경고를 만나면, 배열을 List로 대체해라.