일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- springboot
- Spring
- JPA
- 이펙티브자바
- ddd
- RESTClient
- 도메인 주도 개발 시작하기
- 교육기획팀
- scheduling messages with rabbitmq
- 30기
- 큐시즘
- Domain Driven Design
- 객체지향 쿼리 언어
- Spring Batch
- java
- 자동처리
- 최범균
- JPQL
- 한국대학생it경영학회
- kusitms
- cicd
- delayed message plugin
- rabbitmq-delayed-message-exchange
- 밋업프로젝트
- GitHub Actions
- 영속성
- 교육기획팀원
- jdbc
- 자바 ORM 표준 JPA 프로그래밍
- reactive operaton
- Today
- Total
코딩은 마라톤
[이펙티브자바] 아이템 26, 27, 28 요약 정리 본문
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 외에 어떠한 원소든 넣을 수 없다.
넣을 수 없다면 왜 사용하는 것일까?
넣을 수 없는 이유는 타입 불변식을 훼손하지 못하게 하기 위해서고, 값을 넣을 순 없지만 조회는 가능하다. (타입 불변을 훼손하지 않는 행위는 가능)
로 타입을 써야할 예외
- class 리터럴에는 로 타입을 쓰자.
- 자바 명세는 class 리터럴에 매개변수화 타입을 사용하지 못한다. (배열, 기본 타입만 허용)
- List.class, String[].class, int.class (O)
- List<String>.class, List<?>.class (X)
- 자바 명세는 class 리터럴에 매개변수화 타입을 사용하지 못한다. (배열, 기본 타입만 허용)
- 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. 배열보다는 리스트를 사용하라
배열과 제네릭 타입의 중요한 차이 두 가지
- 배열은 공변(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) { ... }
결론
배열과 리스트를 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용해보자
'Language > Java' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 10장. JPQL (2) (0) | 2024.06.19 |
---|---|
[이펙티브자바] 아이템 29, 30, 31 요약 정리 (1) | 2024.06.09 |
[이펙티브자바] 아이템 25 요약 정리 (0) | 2024.06.07 |
[이펙티브자바] 아이템 22, 23, 24 요약 정리 (1) | 2024.06.06 |
[이펙티브자바] 아이템 19, 20, 21 요약 정리 (2) | 2024.06.05 |