코딩은 마라톤

[이펙티브자바] 아이템 26, 27, 28 요약 정리 본문

Language/Java

[이펙티브자바] 아이템 26, 27, 28 요약 정리

anxi 2024. 6. 7. 23:56

5장 제네릭

자바 5부터 사용 가능

제네릭을 사용하면 컴파일러에게 타입을 알려주어 더 안전하고 명확한 프로그램을 만들어 준다.

 

아이템 26. 로 타입은 사용하지 마라

제네릭 타입

클래스, 인터페이스 선언에 타입 매개변수가 쓰인 것

List 인터페이스는 원소의 타입을 나타내는 타입 매개변수 E를 받는다.

 

제네릭 타입은 일련의 매개변수화 타입(parameterized type)을 정의한다.

클래스(혹은 인터페이스) 이름에, 꺾쇠괄호<> 안에 실제 타입 매개변수 나열

ex) List<String> : String이 정규 타입 매개변수 E에 해당하는 실제 타입 매개변수

 

로 타입(raw type)

제네릭 타입에서 타입 매개변수를 전혀 사용하지 않는 타입

타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작

private final List list = new ArrayList();

list.add(new Bread());
list.add(new Drink());
...
// unchecked call 경고

 

 

상기 예시의 경우 컴파일 때 알아차리지 못하고 런타임에 문제를 알아챌 수 있다. (ClassCastException)

 

매개변수화된 리스트 타입 - 타입 안정성 확보

private final List<Bread> breads = ... ;
private final List<Drink> drinks = ... ;

 

로타입을 왜 막아 놓지 않은거야?

자바가 제네릭을 받아들이기까지 거의 10년이 걸렸다.

제네릭 없는 즉, 로 타입의 코드들이 너무 많은데 마이그레이션 호환성을 위해 로 타입을 지원하고 제네릭 구현에는 소거 방식을 사용하기로 했다.

 

또한 List 의 로 타입은 사용해선 안되지만, List<Object>처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮다.

차이

List<Object> : 모든 타입을 허용한다는 의사를 컴파일러에게 명확히 전달

 

매개변수로 List를 받는 메서드에 List<String>을 넘길 수 있지만, List<Object>를 받는 메서드에는 넘길 수 없다.

-> 제네릭의 하위 타입 규칙

 

즉, List<String>은 로 타입인 List의 하위 타입이지만, List<Object>의 하위 타입은 아니다.
List<Object>같은 매개변수화 타입을 사용할 때와 달리 List 같은 로 타입을 사용하면 타입 안정성을 잃게 된다.
public static void main(String[] args) {
	List<String> strings = new ArrayList<>();
    unsafeAdd(strings, Integer.valueOf(42));
    String s = strings.get(0);
}

private static void unsafeAdd(List list, Object o) {
    list.add(o);
}

private static void unsafeAdd(List<Object> list, Object o) {
    list.add(o);
}

 

List 로 타입의 메서드에서는 컴파일은 되지만 로 타입인 List를 사용하여 strings.get(0); 에서 형변환할 때 ClassCastException을 던진다. (Integer -> String 변환 시도)

 

List<Object> 에서는 컴파일조차 되지 않는다.

 

위 과정을 보면 원소의 타입을 몰라도 되는 로 타입을 사용하고 싶을 수 있다.

 

다음 예시를 보자.

하기 코드는 2개의 집합(Set)을 받아 공통 원소를 반환하는 메서드이다.

int numElementsInCommon(Set s1, Set s2) {
	int result = 0;
    for (Object o1 : s1) {
    	if(s2.contains(o1) {
        	result++;
        }
    return result;
}

 

동작은 하지만 로 타입을 사용해 안전하지 않다.

위의 두 예시처럼 모든 타입을 받을 수 있게 하기 위해, 그리고 로 타입을 사용하지 않기 위해선 "비한정 와일드카드 타입"을 사용하는게 좋다.

 

비한정 와일드카드 타입<?>

제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경쓰고 싶지 않을 때 물음표(?)를 사용한다.

비한정 와일드카드 타입을 사용하여 상기 코드를 다시 선언하면

int numElementsInCommon(Set<?> s1, Set<?> s2) {
	int result = 0;
    for (Object o1 : s1) {
    	if(s2.contains(o1) {
        	result++;
        }
    return result;
}

 

로 타입 컬렉션에는 아무 원소나 넣을 수 있어 타입 불변식을 훼손하기 쉽다.

<?> 비한정 와일드카드 타입을 씀으로써 null 외에 어떠한 원소든 넣을 수 없다. 

넣을 수 없다면 왜 사용하는 것일까? 

넣을 수 없는 이유는 타입 불변식을 훼손하지 못하게 하기 위해서고, 값을 넣을 순 없지만 조회는 가능하다. (타입 불변을 훼손하지 않는 행위는 가능)

 

로 타입을 써야할 예외

  1. class 리터럴에는 로 타입을 쓰자.
    • 자바 명세는 class 리터럴에 매개변수화 타입을 사용하지 못한다. (배열, 기본 타입만 허용)
      • List.class, String[].class, int.class (O)
      • List<String>.class, List<?>.class (X)
  2. instanceof 연산자
    • 런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입은 적용할 수 없다.
    • 로 타입과 비한정적 와일드카드타입은 동일한 instanceof 기능이 동작한다.
if (o instanceof Set) { // 로 타입
	Set<?> s = (Set<?>) o; // 와일드카드 타입
}

 

즉, o의 타입이 Set인지 확인 후 Set<?> 와일드카드 타입으로 형변환한다.

 

결론

  • 로 타입 사용시 런타임 예외 발생 가능
  • 로 타입은 제네릭 도입 전 코드와의 호환성을 위한 것뿐
  • Set<Object>는 어떤 타입의 객체든 저장할 수 있는 매개변수화 타입
  • Set<?>는 모종의 타입 객체만 저장할 수 있는 와일드카드 타입

아이템 27. 비검사 경고를 제거하라

제네릭을 사용하면 수 많은 컴파일러 경고를 보게된다. ex) 비검사 형변환 / 메서드 / 매개변수화 가변인수 타입 / 변환 경고 등

 

비검사 경고(unchecked)

런타임에 ClassCastException을 일으킬 수 있는 잠재적 가능성 (타입 안정성을 보장할 수 있을 만큼의 타입 정보 존재 X)

 

대부분의 비검사 경고는 쉽게 제거할 수 있다. (제네릭 타입을 사용함으로써)

자바 7부터 지원하는 다이아몬드 연산자(<>)만으로도 해결할 수 있다. (컴파일러가 실제 타입 매개변수를 추론해준다.)

 

따라서 할 수 있는 한 모든 비검사 경고를 제거하는게 안전하다.

 

@SuppressWarnings("unchecked")

만약, 경고를 제거할 수 없지만 안전하다고 확신할 수 있다면 @SuppressWarnings("unchecked") 애너테이션을 달아 경고를 없앨 수 있다.

@SuppressWarnings 애너테이션은 개별 지역변수 선언부터 클래스 전체까지 선언할 수 있다. 

하지만 항상 가능한 좁은 범위에 적용해야 한다. 그리고 경고를 무시해도 안전한 이유를 주석으로 남겨야 한다.


아이템 28. 배열보다는 리스트를 사용하라

배열과 제네릭 타입의 중요한 차이 두 가지

  1. 배열은 공변(covariant)이다.
    • Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위타입이 된다.
    • 제네릭은 불공변이다. 즉, 서로 다른 타입 Type1과 Type2가 있을 때 List<Type1>과 List<Type2>는 하위 타입도 아니고 상위 타입도 아니다. 
// 배열 : 공변 가능
// 런타임에 에러 발생 (문법상 허용 가능)
// Object : Super -> Long : Sub
Object[] objectArray = new Long[1];
ObjectArray[0] = "타입이 달라 넣을 수 없다.";

 

// 제네릭 : 불공변
// 호환되지 않는다. (문법에 맞지 않다.)
List<Object> ol = new ArrayList<Long>; 
ol.add("타입이 달라 넣을 수 없다.");

 

둘 다 Long 저장소에 String을 넣을 수 없다.

배열은 런타임에 알 수 있지만, 리스트를 사용하면 컴파일할 때 바로 알 수 있다.

 

2. 배열은 실체화(reify)된다.

  • 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지한다.
  • 제네릭은 타입 정보가 런타임에 소거되기 때문에 컴파일 때만 검사하여 런타임에는 알 수 없다.
Drink(Super) -> Coffee(Sub)

// 배열
Drink[] drinks = new Coffee[10];
// 런타임에 drinks의 원소는 Coffee 타입이 된다.

// 제네릭(리스트)
// 컴파일 시 원소 타입 검사
List<Drink> drinks = new ArrayList<Drink>();

// 런타임시 타입 소거
List drinks = new ArrayList();

 

위 이유로 배열과 제네릭은 잘 어울리지 못한다. (제네릭 배열이 없는 이유 : 타입 안전하지 않아서)

 

실체화 불가 타입 : E, List<E>, List<String> ...

실체화되지 않아서 런타임에는 컴파일 타임보다 타입 정보를 적게 가지는 타입을 말한다.

매개변수화 타입 가운데 실체화될 수 있는 타입은 List<?>와 Map<?, ?> 같은 비한정적 와일드카드 타입뿐이다.

 

제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는 게 보통 불가능하다.

제네릭 타입과 가변인수 메서드를 함께 쓰면 경고 메시지를 받는다.

가변인수 메서드를 호출할 때마다 가변인수 매개변수를 담을 배열(Object 타입_어떤 타입이든 담아야하므로)이 하나 만들어지는데, 이때 그 배열의 원소가 실체화 불가 타입이라면 경고가 발생한다.

 

## 가변인수 메서드

// 가변인수 메서드
// 메서드의 매개변수를 동적으로 처리할 수 있다.
// 참조자료형(래퍼 클래스)이 가변인자로 사용가능하다.
public void varargs(String... strings) { ... }

 

결론

배열과 리스트를 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용해보자