| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 |
- EC2
- 게임개발동아리
- Route53
- 도커
- openAI API
- 프로그래밍
- 체크인미팅
- UNIDEV
- 오블완
- spring ai
- CICD
- 전국대학생게임개발동아리연합회
- 백엔드개발자
- Spring boot
- bastion host
- 생활코딩
- 개발공부
- AWS
- 스프링부트
- NAT gateway
- Redis
- UNICON
- 캡스톤디자인프로젝트
- 42서울
- 프리티어
- 인프라
- 티스토리챌린지
- 프롬프트엔지니어링
- UNICON2023
- 라피신
- Today
- Total
Hyun's Wonderwall
[Java] 이펙티브 자바 - 5장(2/2), 6장(1/2) (아이템 28~36) 본문
5장 제네릭 (2/2)
아이템 28. 배열보다는 리스트를 사용하라
배열와 제네릭 타입의 중요한 차이 2가지: 1. 배열은 공변이다. 2. 배열은 실체화된다.
1. 배열은 공변이다.
- 배열: 공변 (variant, 함께 변한다.) (예로 Sub가 Super의 하위 타입이라면 Sub[]는 Super[]의 하위 타입)
- 제네릭: 불공변 (invariant) (예로 Type1과 Type2가 있을 때, List<Type1>은 List<Type2>의 하위 타입도 상위 타입도 아니다.
- 배열에서는 타입호환이 안 되는 곳에 넣는 실수를 런타임에서 알게 되는데(->ArrayStoreException), 리스트는 컴파일할 때 알게 되어 더 타입 안전한 프로그래밍을 할 수 있다.
2. 배열은 실체화된다.
- 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
- 제네릭은 타입 정보가 런타임에 소거된다. 원소 타입을 컴파일타임에만 검사해 런타임에는 알 수 없다.
- 소거(erasure): 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘
🔗 더 자세한 설명: JAVA 제네릭 배열을 생성하지 못하는 이유 | BLOG
JAVA 제네릭 배열을 생성하지 못하는 이유 | BLOG
JAVA 제네릭 배열을 생성하지 못하는 이유 작성일: 2020-07-18 14:28 자바에서 제네릭 타입은 중요한 두 가지 차이를 가지고, 이 차이로 인해 제네릭 배열은 타입 안전성을 보장할 수 없어 직접 생성이
pompitzz.github.io
제네릭 타입의 배열 생성은 불가능하다 (new List<String>[10]; 같은)
불가능하게 막은 이유: 타입 안전하지 않아서
- 이를 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다. 타입이 다른 원소를 넣을 때는 문제가 발생하지 않고, 꺼내는 시점에.
- 이는 런타임에 이 예외 발생을 막고자 한 제네릭 타입 시스템의 취지에 어긋난다. 따라서 제네릭 배열이 생성되지 못하게 되어있다.
- 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
실체화 불가 타입: 실체화되지 않아 런타임에 컴파일타임보다 타입 정보를 적게 가지는 타입 (E, List<E>, List<STring> 등) (소거 메커니즘)
매개변수화타입 가운데 실체화될 수 있는 타입은 List<?>, Map<?, ?>같은 비한정적 와일드카드 타입뿐
타입 안정성과 상호운용성을 위해 List<E> 사용 추천.
핵심 정리: 배열은 공변이고 실체화된다. 제네릭은 불공변이고 타입 정보가 소거된다.
-> 배열은 런타임에는 타입 안전하지만 컴파일타임에는 그렇지 않다. 제네릭은 컴파일타임에 타입 안전하지만 런타임에는 무슨 타입인지 알 수 없다.
-> 컴파일 오류나 경고를 만나면 배열을 리스트로 대체하자. (조금 더 느리게 될지라도 타입 안전하므로 가치 있다)
아이템 29. 이왕이면 제네릭 타입으로 만들라
일반 클래스를 제네릭 클래스로 바꾸기
1. 클래스 선언에 타입 매개변수 추가 (Stack<E>)
2. 코드에 쓰인 필드 elements의 타입을 기존 Object[]에서 E[]로 변경
-> 에러: "실체화 불가 타입으로는 배열 생성 불가"
해결 방법 1: elements 타입 E[]로 변경하고, 초기화하는 부분에서 elements = (E[]) new Object[...];로 생성.
해결 방법 2: elements 타입 Object[] 그대로 유지하고, pop()에서 E result = (E) elements[--size]와 같이 배열이 반환한 원소를 E타입으로 형변환.
1, 2 다 비검사 형변환이 안전한 경우이므로 @SuppressWarnings("unchecked")를 앞에 붙여 경고 숨길 수 있다.
현업에서는 해결방법 1이 형변환을 단 한번만 해주면 되므로 선호한다. 하지만 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염을 일으킬수 있는 단점이 있다.
정리: 클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다. 그러니 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라. 그렇게 하려면 제네릭 타입으로 만들어야 할 경우가 많다.
아이템 30. 이왕이면 제네릭 메서드로 만들라
메서드도 제네릭으로 만들 수 있다. 매개변수화 타입을 받는 정적 유틸리티 메서드는 보통 제네릭. (ex. Collections의 알고리즘 메서드 - binarySearch, sort 등)
타입 안전하게 만드는 것이 중요하다.
- 메서드 선언에서의 세 집합의 원소 타입을 타입 매개변수로 명시하고, 메서드 안에서도 이 타입 매개변수만 사용하게 수정
- 타입 매개변수들을 선언하는 타입 매개변수 목록(<E>)은 메서드의 제한자와 반환 타입(Set<E>) 사이에 온다.
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
한정적 와일드카드 타입을 사용하면 더 유연하게 개선할 수 있다.
불변 객체를 여러 타입으로 활용할 수 있게 만들어야 할 때가 있다.
제네릭이 런타임에 타입 정보가 소거되므로 하나의 객체를 어떤 타입이로든 매개변수화할 수 있는 점을 이용한다. -> 요청한 타입 매개변수에 맞게 매번 그 객체의 타입을 바꿔주는 정적 팩터리를 만들어야 한다. 이 패턴을 '제네릭 싱글턴 팩터리'라 한다.
- Collections.reverseOrder, Collections.emptySet
항등함수를 담은 클래스를 제네릭 싱글턴으로 만들면
private static UnaryOperator<Object> IDENTITY_FN = (t)->t;
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction(){
return (UnaryOperator<T>) IDENTITY_FN;
}
재귀적 타입 한정: 자기 자신이 들어간 표현식을 사용해 타입 매개변수의 허용 범위를 한정.
- 주로 타입의 자연적 순서를 정하는 Comparator 인터페이스와 함께 쓰인다.
max()를 구현해보자 (실제 구현과 다름)
public static <E extends Comparable<E>> E max (Collection<E> c) {
if(c.isEmpty())
throw new IllegalArgumentException("컬렉션이 비어 있습니다.");
E result = null;
for(E e : c)
if(result==null || e.compareTo(result) > 0)
result = Objects.requireNonNull(e);
return result;
}
정리: 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기도 쉽다. 타입과 마찬가지로 메서드도 형변환 없이 사용할 수 있는 편이 좋다. 형변환을 해줘야 하는 메서드는 제네릭하게 만들자.
아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라
한정적 와일드카드 타입: <? extends E>처럼 한정하는 것
pushAll() 예제
- Stack<Number>에 Iterable<Integer> 넣으려는 상황
- public void pushAll(Iterable<? extends E> src) {...}
- Iterable<E>면 E만 되지만 Iterable<? extends E>면 E의 하위 타입의 Iterable이면 가능
popAll() 예제
- popAll은 입력 매개변수의 타입이 E의 Collection이 아니라, E의 상위 타입의 Collection이어야 한다.
- public void popAll(Collection<? super E> dst) {...}
유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라.
한편, 입력 매개변수가 생산자와 소비자 역할을 동시에 하거나, 타입을 정확히 지정해야 하는 상황에는 쓰지 말아야 한다.
PECS: producer-extends, consumer-super
- 매개변수 타입 T가 생산자라면 <? extends T>, 소비자라면 <? super T>
또한 반환 타입에는 한정적 와일드카드 타입을 사용하먄 안 된다. 클래스 사용자가 와일드카드 타입을 신경쓰지 않도록 해야 한다.
위의 max()의 구현을 다듬어보면
public static <E extends Comparable<? super E>> E max (Collection<E> c) {
- Comparable은 언제나 소비자이므로 Comparable<? super E>를 사용하는 편이 낫다. Comparator도.
메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하라.
정리: 조금 복잡하더라도 와일드카드 타입을 적용하면 API가 훨씬 유연해진다. 널리 쓰일 라이브러리를 작성한다면 와일드카드 타입을 적절히 사용해줘야 한다. PECS 공식. 생산자는 extends, 소비자는 super.
아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라
매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생한다.
안전한 varargs 메서드만을 작성하고, 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs를 달라. 그래야 사용자를 헷갈리게 하는 컴파일러 경고를 없앨 수 있다. 힙 오염 경고가 뜬다면 메서드 안전성 점검해야.
점검방법
1. varargs 매개변수 배열에 아무것도 저장하지 않는다.
2. 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.
또는 varargs대신 List로 사용해라.
정리: 가변인수는 제네릭과 궁합이 좋지 않다. 가변인수는 기능은 배열을 노출하여 추상화가 완벽하지 못하고, 배열과 제네릭의 타입 규칙이 서로 다르기 때문이다. 제네릭 varargs 매개변수는 타입 안전하지는 않지만 허용된다. 메서드에 제네릭 varargs 변수를 사용하고자 한다면, 그 메서드가 타입 안전한지 확인한 다음 애너테이션을 달아 사용하는 데 불편함이 없게끔 하자.
아이템 33. 타입 안전 이종 컨테이너를 고려하라
컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다.
- 그럼, 데이터베이스 모든 열 타입 안전하게 하는 것처럼은 어떻게?
타입 안전 이종 컨테이너 패턴: 컨테이너 자체가 아닌 키를 매개변수로
- 컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공 -> 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장하게 한다.
- Class를 키로 사용 ('타입 토큰': 이러한 Class 객체)
- 직접 구현한 키 타입도 사용 가능
- 예시: 데이터베이스의 행(컨테이너) 표현한 DatabaseRow 타입에는 제네릭 타입인 Column<T>를 키로 사용 가능
6장 열거 타입과 애너테이션 (1/2)
아이템 34. int 상수 대신 열거 타입을 사용하라
열거 타입: 일정 개수의 상수 값을 정의한 다음 그외의 값은 허용하지 않는 타입. 클래스이다.
- 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다.(인스턴스 통제)
- 싱글턴은 원소가 하나뿐인 열거 타입이라 할 수 있고, 열거 타입은 싱글턴을 일반화한 형태라 볼 수 있다.
- 컴파일타임 타입 안정성 제공.
- 각자의 이름공간이 있어서 이름이 같은 상수도 평화롭게 공존.
- 새로운 상수를 추가하거나 순서를 바꿔도 다시 컴파일하지 않아도 된다. 공개되는 것이 필드의 이름뿐이라 상수 값이 클라이언트로 컴파일되어 각인되지 않기 때문에. => 열거 타입에서 상수를 하나 제거해도 참조하지 않는 클라이언트에는 아무 영향이 없다. (참조하고 있다면 컴파일 에러 (or 재컴파일하지 않았다면 런타임 예외))
열거 타입에는 임의의 메서드나 필드를 추가할 수 있고, 임의의 인터페이스를 구현하게 할 수도 있다.
- Object 메서드들, Comparable, Serializable 구현. 그 직렬화 형태도 구현해놓음
어떨 때 열거 타입에 필드, 메서드 추가? 각 상수와 연관된 데이터를 해당 상수 자체에 내재시키고 싶을 때. 상수마다 다르게 동작하게 할 때.
- 열거 타입 각각을 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다. (매개변수로 데이터를 받고 계산한 값을 저장해도 됨)
- 열거 타입은 근본적으로 불변이므로 모든 필드는 final이어야 한다.
- 필드 제한자는 private으로 두고 별도의 pulbic 접근자 메서드를 두는 게 낫다.
열거 타입은 자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메서드인 values를 제공한다. 값들은 선언된 순서로 저장된다.
각 열거 타입 값의 toString 메서드는 상수 이름을 문자열로 반환하므로 출력에 사용하기 좋다.
열거 타입을 선언한 클래스 혹은 그 패키지에서만 유용한 기능은 private이나 package-private 메서드로 구현하라.
널리 쓰이는 열거 타입은 톱레벨 클래스로 만들고, 특정 톱레벨 클래스에서만 쓰인다면 해당 클래스의 멤버 클래스로 만든다.
상수별 메서드 구현: 열거 타입은 추상 메서드를 선언하고 상수별 클래스 몸체로 사용해서 상수별로 다르게 동작하는 코드를 구현할 수 있다.
상수별 메서드 구현을 상수별 데이터와 결합할 수도 있다.
열거 타입에는 상수 이름을 입력받아 그 이름에 해당하는 상수를 반환해주는 valueOf(string) 메서드가 자동 생성된다.
한편, 열거 타입의 toString 메서드를 재정의하려거든, toString이 반환하는 문자열을 해당 열거 타입 상수로 변환해주는 fromString 메서드도 함께 제공하는 걸 고려해보자. 다음 코드는 모든 열거 타입에서 사용할 수 있도록 구현한 fromString이다.
private static final Map<String, Operation> stringToEnum = Stream.of(values().collect(toMap(Object::toString, e->e));
public static Optional<Operation> fromString(String symbol) {
return Optional.ofNullable(stringToEnum.get(symbol));
}
전략 열거 타입 패턴: 내부에 전략 열거 타입을 만드는 것. 열거 타입 상수 일부가 같은 동작을 공유하는 경우. 하나의 메서드가 상수별로 다르게 동작해야 할 때.
기존 열거 타입에 상수별 동작을 혼합해 넣을 때는 switch문도 좋은 선택이다. 추가하려는 메서드가 의미상 열거 타입에 속하지 않는 경우.
열거 타입을 언제 사용해야 할까?
- 필요한 원소를 컴파일타임에 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자.
- 열거 타입에 정의된 상수 개수가 영원히 고정불변일 필요는 없다. 열거 타입은 나중에 상수가 추가돼도 바이너리 수준에서 호환되도록 설계되었다.
=> 특히 배운 점..!
아이템 35. ordinal 메서드 대신 인스턴스 필드를 사용하라
열거 타입 상수에 연결된 값은 ordinal 메서드로 얻지 말고, 인스턴스 필드에 저장하자.
이 메서드는 EnumSet, EnumMap과 같이 열거 타입 기반 범용 자료구조에 쓸 목적으로 사용. 이런 용도가 아니라면 쓰지 말고,
값을 인스턴스 필드에 저장하자.
=> 최근에 진행한 프로젝트에서 팀원이 이렇게 사용했던 사례가 생각났다.
아이템 36. 비트 필드 대신 EnumSet을 사용하라
열거한 값들이 주로 단독이 아니라 집합으로 사용될 경우
- 예전에는 각 상수에 서로 다른 2의 거듭제곱 값을 할당한 정수 열거 패턴을 사용해 왔다.
- 그러나 현재는 명료함과 성능을 갖춘 EnumSet을 쓰는 것이 좋다.
- 유일한 단점: 불변 EnumSet은 만들 수 없다. 불변으로 쓰고 싶다면 Collections.unmodifiableSet으로 EnumSet을 감싸기.
'Study > Java' 카테고리의 다른 글
| [Java] 이펙티브 자바 - 7장(2/2), 8장(1/2) (아이템 46~54) (0) | 2025.08.23 |
|---|---|
| [Java] 이펙티브 자바 - 6장(2/2), 7장(1/2) (아이템 37~45) (2) | 2025.08.16 |
| [Java] 이펙티브 자바 - 4장(2/2), 5장(1/2) (아이템 19~27) (4) | 2025.08.02 |
| [Java] 이펙티브 자바 - 3장, 4장(1/2) (아이템 10~18) (2) | 2025.07.26 |
| [Java] 이펙티브 자바 - 1장, 2장(아이템 1~9) (0) | 2025.07.19 |