조슈아 블로크 님의 "Effective Java" 책을 정리한 포스팅 입니다.
1. 배열보다는 리스트를 사용하라
배열은 공변
Object[] objArray = new Long[1];
objArray[0] = "hello"; // 런타임에 ArrayStoreException 발생
- 구현 타입으로 변경됨
- 잘못된 타입이 할당될 경우, 런타임에 예외 발생
제네릭은 불공변
List<Object> ol = new ArrayList<Long>(); // 호환 X. 컴파일 타임에 오류 발생!
ol.add("hello");
- 매개변수 타입과 관련없이 제네릭 타입은 서로 다른 타입
- 일치하지 않을 경우, 컴파일 타임에 오류
2. 이왕이면 제네릭 타입으로 만들라
- 클래스 선언에 매개변수 타입을 추가하기
3. 이왕이면 제네릭 메서드로 만들라
타입 매개변수 목록 메서드의 제한자와 반환 타입 사이에 옴
public <E> set<E> union(Set<E> s1, Set<E> s2) {
// ...
}
제네릭 싱글턴
- 제네릭 타입과 싱글턴 패턴을 결합한 개념
- 불변 객체를 제네릭 타입으로 활용하여 여러 타입에 대하여 재사용할 수 있도록 설계된 패턴
항등함수를 담은 클래스
public class GenericSingleton {
// 항등 함수 (불변 객체)
private static final UnaryOperator<Object> IDENTITY_FUNCTION = t -> t;
// 타입 안전한 싱글턴 인스턴스 제공
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
return (UnaryOperator<T>) IDENTITY_FUNCTION;
}
public static void main(String[] args) {
// String 타입 항등 함수
UnaryOperator<String> stringIdentity = GenericSingleton.identityFunction();
System.out.println(stringIdentity.apply("Hello")); // 출력: Hello
// Integer 타입 항등 함수
UnaryOperator<Integer> intIdentity = GenericSingleton.identityFunction();
System.out.println(intIdentity.apply(42)); // 출력: 42
}
}
- 불변 객체를 여러 타입으로 활용
재귀적 타입 한정
public class RecursiveTypeBound {
// 최대 값을 반환하는 제네릭 메서드
public static <T extends Comparable<T>> T max(List<T> list) {
if (list.isEmpty()) throw new IllegalArgumentException("List is empty");
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) max = item;
}
return max;
}
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(1, 3, 2, 5, 4);
System.out.println("Max Integer: " + max(intList)); // 출력: Max Integer: 5
List<String> strList = Arrays.asList("apple", "banana", "cherry");
System.out.println("Max String: " + max(strList)); // 출력: Max String: cherry
}
}
- 타입 매개변수가 자기 사진을 상위 타입으로 제한하는 방식
- Comparable과 함께 쓰임
- 타입 T는 반드시 Comparable<T>를 구현해야 하며, 자기 자신과 비교할 수 있어야 함
4. 한정적 와일드카드를 사용해 API 유연성을 높이라
제네릭 타입은 불공변
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integers); // 컴파일 오류!
- Stack<Number>의 pushAll(integer...)일 경우 컴파일 오류 발생
- Stack<Number>와 Stack<Integer>와는 아무 상관 없음
- 제네릭 타입이 타입 안전성을 보장하기 위함
한정적 와일드카드
- 제네릭 타입의 유연성을 극대화 하는 방법
- 특정 상위 타입이나 하위 타입으로 제한된 타입 매개변수를 지정할 수 있음
<? extends T>
public class Stack<E> {
private List<E> elements = new ArrayList<>();
public void push(E element) {
elements.add(element);
}
// <? extends E> 사용: 하위 타입 허용
public void pushAll(Iterable<? extends E> src) {
for (E e : src) {
push(e);
}
}
}
// 사용
public static void main(String[] args) {
Stack<Number> stack = new Stack<>();
List<Integer> integers = Arrays.asList(1, 2, 3);
stack.pushAll(integers); // 가능: Integer는 Number의 하위 타입
}
- 타입 매개변수의 상위타입 지정할 수 있음
- 타입 매개변수가 생산자일 경우 유용 (읽기)
<? super T>
public class Stack<E> {
private List<E> elements = new ArrayList<>();
public E pop() {
if (elements.isEmpty()) {
throw new EmptyStackException();
}
return elements.remove(elements.size() - 1);
}
// <? super E> 사용: 상위 타입 허용
public void popAll(Collection<? super E> dst) {
while (!elements.isEmpty()) {
dst.add(pop());
}
}
}
// 사용
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
List<Number> numbers = new ArrayList<>();
stack.popAll(numbers); // 가능: Number는 Integer의 상위 타입
}
- 타입 매개변수의 하위타입을 지정할 수 있음
- 타입 매개변수가 소비자일 경우 유용 (쓰기)
'Java' 카테고리의 다른 글
[Effective Java] 6-1. 열거 타입과 애노테이션 (0) | 2024.12.30 |
---|---|
[Effective Java] 5-3. 제네릭: 고려 사항 (0) | 2024.12.30 |
[Effective Java] 5-1. 제네릭: 제한 사항 (0) | 2024.12.27 |
[Effective Java] 4-4. 클래스와 인터페이스: 클래스 (0) | 2024.12.27 |
[Effective Java] 4-3. 클래스와 인터페이스: 인터페이스 (0) | 2024.12.27 |