본문 바로가기

개발/[스터디] 이펙티브 자바

[이펙티브 자바 스터디] 아이템 31~35

아이템 31. 한정적 와일드 카드를 사용해 API 유연성을 높여라

매개변수화 타입의 문제점

불공변이라 유연하지 않다.

복습

매개변수화 타입은 불공변(invariant)이다.

즉, "Type2가 Type1의 하위타입"은 List<Type2>가 List<Type1>의 하위 타입임을 보장하진 않는다. 

 

왜 하위 타입임을 보장할 수 없을까

예를 들어, List<Object>와 List<String>을 살펴보자. 

List<Object>에는 어떤 객체든지 넣을 수 있으나, List<String>에는 String만 넣을 수 있다, 즉, 리스코프 치환 원칙에 어긋나므로, List<String>은 List<Object>의 하위타입이 될 수 없다. 

 

불공변의 예시

// 예시
public class Stack<E> {
	public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}

위 class에 pushAll을 추가한다고 가정하자. 

public void pushAll(Iterable<E> src) {
	for (E e: src)
    	push(e);
}

Stack<Number>에 Integer 타입을 넣을 수 있을까? 

답: 불가능하다. 

 

불공변에 유연하게 대응할 수 없을까

"한정적 와일드 카드 타입"이라는 매개변수 타입을 사용하자. 

// 생산자(producer) 매개변수에 와일드 타입 적용
public void pushAll(Iterable<? extends E> src) {
	for (E e : src)
    	push(e);
}

// 소비자(consumer) 매개변수에 와일드 타입 적용
public void popAll(Collection<? super E> dst) {
	while(!isEmpty()) 
    	dst.add(pop());
}

** 예제에서 언급된 개념 

- 생산자(producer): 컬렉션을 살펴보고 각 항목으로 작업을 수행

- 소비자(Consumer): 컬렉션에 항목을 추가할 경우

 

PESCS

- 와일드카드 타입을 사용하는 기본 원칙

- 언제 Extends와 Super를 사용할지 알려주는 공식

producer-extends, consumer-super

결론

유연성을 극대화하려면, 원소의 생산자/소비자용 입력 매개변수에 와일드 타입을 사용하자. 

 

질문

입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드 타입을 써도 좋을게 없다.  (183p)

-> 질문: 동시에 한다는 건 어떤 예시가 있을까? 왜 좋을게 없을까?

-> 답변: comparable이 예시로 나옴. 자세히 보기!!

레퍼런스


아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라.

가변인수(Variadic Argument)란?

  • 정의
    • Method의 Argument의 개수를 클라이언트가 조절 가능하다.
  • 코드 예시
    • void mergeAll(List<String>... stringLists) {}
  • 주의사항
    • 반드시 한 개의 가변 인수만을 사용하자. 
    • 맨 마지막 Argument로 사용하자. 

Heap Pollution(힙오염)

매개변수화 타입의 변수가 타입이 다른 객체를 참조하는 경우

-> 타입 안정성이 깨지므로, 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다. 

 

안전하지 않다면, 왜 varargs 매개변수를 받는 메서드를 선언할 수 있게 했을까?

실무에서 유용하게 쓰여서, 자바 언어 설계자는 이를 수용했다. 

 

제네릭과 가변인수, 어떻게 안전하게 쓸까?

- 제네릭 배열에 아무것도 저장하거나 덮어쓰지 말자. 

- 배열의 참조를 밖으로 노출시키지 말것 

 

'제네릭 가변인수 메서드 경고' 숨기기

언제 숨길까?

제네릭 가변인수 메서드를 클라이언트 단에서 사용시, heap pollution이 일어날 수 있다고 IDE가 경고한다. 

'제네릭과 가변인수, 어떻게 안전하게 쓸까?'에서 언급한 2가지 조건을 모두 만족한다면, 경고를 제거 하는 것이 좋다. 

 

어떻게 숨길까?

제네릭이나 매개변수화 타입의 varagrs 매개변수를 받는 모든 메서드에 @SafeVarargs를 달자. 

@SafeVarargs
static <T> List<T> flatten(List<? extends T> ...lists) {
	// 코드 생략
}

 

@SafeVarargs 없이 경고 없애는 방법

varargs의 매개변수를 List 매개변수로 바꿀 수 있다. 

static <T> List<T> flatten(List<List>? extends T>> lists) {
	// 코드 생략
}

단 위 코드는 클라이언트 코드가 조금 지저분해진다는 단점이 있다. 

// 클라이언트 코드에서 List.of를 사용해야한다. 
audience = flatten(List.of(friends, romans, countrymen));

정리

1. 제네릭 배열에 아무것도 저장하거나 덮어쓰지 말고, 배열의 참조를 밖으로 노출시키지 말아야한다. 

2. 제네릭과 가변인수는 궁합이 잘 맞지 않으니, 함께 사용할 경우 조심하자

3. 이중 리스트도 해답이 될 수 있다. 

레퍼런스

힙오염이란?

https://parkadd.tistory.com/130


아이템 33. 타입 안전 이종 컨테이너를 고려하라.


아이템 34. int 상수 대신 열거 타입을 사용하라. 

정수 열거패턴

- 언제 사용했나: 열거타입의 등장 이전 사용. 

- 단점

  • 타입 안전 보장하지 않는다
  • 표현력이 나쁘다 (prefix 붙여야함)

열거타입이란

- 일정 개수의 상수값을 정의, 그 외의 타입은 허용하지 않는 타입. 

- 열거타입은 클래스이며, 상수 하나당 자신의 인스턴스 하나를 만들어 public static final 필드로 공개. 

 

열거타입의 장점

- 컴파일타임 타입 안전성을 제공. 

- 이름이 같은 상수도 공존 가능

- 새로운 상수 추가/순서 변경시, 재컴파일 필요 없다. 

- toString()을 유용하게 사용 가능. 

- 상수 제거시, 컴파일/런타임 오류 클라이언트에서 발생

 

열거타입 내 메소드 추가

방법 1. switch문 사용

- 좋지 않다. 메서드 추가 잊을 경우 runtime 에러 발생

방법 2. 상수별로 메서드 구현

코드 34-5, 코드 34-6

 

열거타입의 메서드 내 중복 코드 어떻게 제거할까

전략 열거 타입을 이용하자 (코드 45-9) : 각 요일 별로 PayType을 선택한다. 

 


아이템 35. ordinal 메서드 대신 인스턴스 필드를 사용하라. 

 

Ordinal 메서드란?

열거타입에서 몇 번째 위치인지 반환하는 메서드

public enum Ensemble {
	Solo, DUET, TRIO
    
    public int numberOfMusicians() { return ordinal() + 1; } 
}

 

Ordinal 사용의 문제점

1. 동일한 정수를 사용하는 상수는 사용할 수 없다. 

: 만약 2명이 연주하는 다른 연주 방식이 있다면? 이를 상수화할 방법이 없다. 

2. 중간 값을 비울 수 없다. 

: TRIO는 3명이 연주한다. 위 코드에서 5명이 연주하는 QUINTET를 추가시 4명에 대한 상수는 빈 값으로 둘 수 없다. 

 

해결책

열거타입 상수에 연결된 값은 ordinal 메서드로 얻지 말고, 인스턴스 필드에 추가하자. 

public enum Ensemble {
	Solo(1), DUET(2), TRIO(3)
    
    private final int numberOfMusicians;
    Ensemble(int size) {this.numberOfMusicians = size;}
    public int numberOfMusicians() { return ordinal() + 1; } 
}