F-LAB JAVA · 3주차 · Phase 5 · 제네릭과 와일드카드
🚀 Phase 5 시작 — 자바 타입 시스템 정복
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
@SuppressWarnings("unchecked") 의 의미는?<>) (Java 7+) 의 효과는?제네릭은 "컴파일 타임에 타입 안전성을 보장" 하는 자바 5+ 의 도구다.
Java 1.4 까지는List에 어떤 객체든 담을 수 있어ClassCastException위험이 컸지만,
제네릭 도입으로List<String>처럼 타입을 명시해 컴파일러가 검증.
단, 하위 호환성 을 위해 타입 소거 (Type Erasure) 라는 타협으로 런타임엔 타입 정보가 사라진다.
raw type 은 제네릭 이전의 호환 모드로 위험하므로 사용 회피.
Java 1.4 까지 (raw type):
상자에 라벨 없음 — 무엇이든 담을 수 있음
꺼낼 때 매번 확인 필요
- "이게 책인지 음식인지?"
- 잘못 꺼내면 사고 (ClassCastException)
Java 5+ 제네릭:
상자에 라벨 — "이 상자는 책 전용 (List<Book>)"
컴파일러가 검증 — 음식 못 넣음
꺼낼 때 자동으로 책 타입
- 안전 + 깔끔
→ 제네릭 = 상자에 타입 라벨.
1. Java 1.4 까지의 컬렉션 문제
2. 제네릭의 등장과 도입
3. 타입 안전성의 보장
4. raw type 의 정의와 위험
5. 타입 소거 (Type Erasure)
6. 하위 호환성과 타협
7. 다이아몬드 연산자 (Java 7+)
8. @SuppressWarnings 와 컴파일러 경고
9. 면접 + 자기 점검
// Java 1.4 까지 (1996 ~ 2004)
import java.util.ArrayList;
public class Old {
public static void main(String[] args) {
ArrayList list = new ArrayList(); // 타입 없음
list.add("Hello"); // String 추가
list.add("World");
list.add(42); // Integer 도 추가됨!
// 꺼낼 때
Object obj = list.get(0);
String s = (String) obj; // 명시적 캐스트 필요
// 위험!
String s2 = (String) list.get(2);
// ClassCastException at runtime
}
}
문제점:
Object 타입ArrayList list = new ArrayList();
list.add("apple");
list.add("banana");
// 다른 개발자가 추가
list.add(42); // 컴파일러 OK — 위험 감지 못 함
// 또 다른 곳
for (int i = 0; i < list.size(); i++) {
String s = (String) list.get(i);
System.out.println(s.length());
// i = 2 에서 ClassCastException!
// 컴파일 시점엔 모름, 런타임에 폭발
}
핵심:
// Java 1.4
ArrayList list = getNames(); // ArrayList of Strings (개발자 약속)
// 모든 사용처에서 캐스트
String first = (String) list.get(0);
String second = (String) list.get(1);
for (int i = 0; i < list.size(); i++) {
String name = (String) list.get(i); // 매번 캐스트
System.out.println(name);
}
// 단점:
// 1. 코드 장황
// 2. 캐스트 실수 가능
// 3. 컴파일 시 안전성 검증 X
/**
* @return ArrayList of Strings ← 주석에만 명시
*/
public ArrayList getNames() {
ArrayList list = new ArrayList();
list.add("Alice");
list.add("Bob");
return list;
}
// 사용
ArrayList names = getNames();
// 다른 개발자가 잘못 사용
names.add(42); // OK (컴파일러는 모름)
// 또는 잘못 캐스트
Integer i = (Integer) names.get(0); // ClassCastException
문제:
// 타입별로 별도 메서드 작성 필요
public static int findString(ArrayList list, String target) {
for (int i = 0; i < list.size(); i++) {
String s = (String) list.get(i);
if (s.equals(target)) return i;
}
return -1;
}
public static int findInteger(ArrayList list, Integer target) {
for (int i = 0; i < list.size(); i++) {
Integer n = (Integer) list.get(i);
if (n.equals(target)) return i;
}
return -1;
}
// 또는 Object 로 일반화
public static int find(ArrayList list, Object target) {
for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals(target)) return i;
}
return -1;
}
// 하지만 타입 안전 X
→ 제네릭이 해결.
Java 1.4 까지:
- 모든 컬렉션 = Object 의 컬렉션
- 타입 정보 손실
- 안전성 ↓
- 가독성 ↓
해결 필요:
- 컴파일 타임에 타입 검증
- 명시적 캐스트 회피
- 코드 재사용 + 안전성
- C++ 의 template 같은 기능
제네릭 도입 전 컬렉션의 가장 큰 문제는?
답:
1. 타입 안전성 부족:
→ 제네릭이 모두 해결.
제네릭 (Generics):
타입을 매개변수화 (Parameterize) 하는 기능.
컴파일 타임에 타입을 명시하고 컴파일러가 검증.
문법:
ClassName<TypeParameter>
예: List<String>, Map<K, V>
// Java 5 부터
import java.util.ArrayList;
import java.util.List;
public class New {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
// ↑ 타입 매개변수 <String>
list.add("Hello");
list.add("World");
// list.add(42); // ❌ 컴파일 에러!
// 꺼낼 때 캐스트 불필요
String s = list.get(0); // 자동으로 String
System.out.println(s.length());
}
}
핵심:
<String> 으로 타입 명시1. 타입 안전성
- 잘못된 타입 추가 시 컴파일 에러
- 런타임 ClassCastException 회피
2. 명시적 캐스트 제거
- 컴파일러가 자동 캐스트 삽입 (실제로는)
- 코드 깔끔
3. 코드 재사용
- 한 클래스로 다양한 타입 지원
- List<E>, Map<K,V>
4. 가독성
- 코드만 봐도 타입 명확
- 문서화 의존 X
// Java 1.4 → Java 5+ 전환
// 컬렉션
List → List<E>
Map → Map<K, V>
Set → Set<E>
Queue → Queue<E>
// Comparable
Comparable → Comparable<T>
// 기타
Class → Class<T>
ThreadLocal → ThreadLocal<T>
Iterator → Iterator<E>
→ 모든 컬렉션 + 비교 + 클래스 등이 제네릭화.
// 전 (Java 1.4)
public Object find(List list, Object target) {
for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals(target)) {
return list.get(i);
}
}
return null;
}
// 호출
List names = new ArrayList();
String found = (String) find(names, "Alice");
// 후 (Java 5+)
public <T> T find(List<T> list, T target) {
for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals(target)) {
return list.get(i);
}
}
return null;
}
// 호출
List<String> names = new ArrayList<>();
String found = find(names, "Alice"); // 캐스트 X
// 1. 변수 선언
List<String> names = new ArrayList<>();
Map<Long, Shipment> cache = new HashMap<>();
Set<Permission> permissions = new HashSet<>();
// 2. 메서드 매개변수
public void process(List<Shipment> shipments) { ... }
// 3. 반환 타입
public List<Shipment> findActive() { ... }
// 4. 필드
public class Repository {
private Map<Long, Shipment> cache;
}
// 5. 함수형 인터페이스
Function<String, Integer> length = s -> s.length();
Predicate<Shipment> isActive = s -> s.getStatus() == ACTIVE;
다양한 언어의 제네릭 (parameterized types):
C++ (1990s):
- template
- 컴파일 시점 코드 생성 (monomorphization)
- 매우 빠름
C# (2005):
- Generics
- 런타임에도 타입 정보 보유
- 자바보다 강력
Java (2004, Java 5):
- Generics + 타입 소거
- 런타임에 타입 정보 X
- 하위 호환성 우선
Kotlin / Scala:
- 자바 호환 + 강화 (reified types 등)
- 런타임 정보 일부 보유
제네릭의 4가지 효과는?
답:
1. 타입 안전성: 컴파일 타임 검증
2. 명시적 캐스트 제거: 코드 깔끔
3. 코드 재사용: 다양한 타입 지원
4. 가독성: 타입 정보 명확
추가:
타입 안전성 (Type Safety):
프로그램이 실행 시 타입 관련 오류를 일으키지 않는 보장.
자바의 타입 안전:
- 컴파일 타임 검증
- 캐스트 시 ClassCastException 가능
- 제네릭이 캐스트 회피 도와줌
List<String> list = new ArrayList<>();
list.add("Hello"); // OK
list.add("World"); // OK
// list.add(42); // ❌ 컴파일 에러
String s = list.get(0); // OK (자동 캐스트)
// Integer i = list.get(0); // ❌ 컴파일 에러 (타입 불일치)
// 메서드도 검증
public void process(List<Integer> nums) { ... }
process(list); // ❌ 컴파일 에러
// List<String> 을 List<Integer> 매개변수에 전달 불가
// Java 컴파일러의 타입 검증 흐름
List<String> list = new ArrayList<>();
// list.add("Hello") 호출 시:
// 1. list 의 타입: List<String>
// 2. add 메서드: boolean add(E e) where E = String
// 3. "Hello" 의 타입: String
// 4. 매개변수 String 과 매칭 ✓
// list.add(42) 호출 시:
// 1. list 의 타입: List<String>
// 2. add 메서드: boolean add(String e)
// 3. 42 의 타입: Integer (autoboxed)
// 4. String 과 Integer 매칭 ✗ → 컴파일 에러
// IDE 가 자동완성으로 도와줌
List<Shipment> shipments = ...;
// IDE 가 표시:
shipments.add(...); // add(Shipment) 추천
shipments.get(0).getStatus(); // Shipment 의 메서드 자동완성
// 잘못된 타입 빨간 줄
shipments.add(new Cargo()); // 빨간 줄 — Cargo 아님
→ 제네릭 + IDE = 강력한 안전망.
// 소스 코드
List<String> list = new ArrayList<>();
String s = list.get(0);
// 컴파일러가 실제로 생성하는 바이트코드 (개념적):
List list = new ArrayList();
String s = (String) list.get(0); // ★ 자동 캐스트 삽입
// 즉, 컴파일러가 명시적 캐스트를 자동으로 삽입
// 사용자는 의식할 필요 X
→ 컴파일러의 마법.
// 안전한 사용
List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0); // 안전
// 위험한 사용 — raw type 혼용
List<String> safeList = new ArrayList<>();
List rawList = safeList; // raw type 으로 캐스트
rawList.add(42); // 경고만, 컴파일 OK
String s = safeList.get(0); // ClassCastException!
// 실제로는 Integer 가 들어있음
→ 제네릭은 컴파일 타임 도구, raw type 으로 우회 가능.
// 안전한 컬렉션 사용
// 1. 명확한 타입
private final Map<Long, Shipment> cache = new HashMap<>();
// ↑ ↑
// 키, 값 타입 명시 구현체
// 2. 메서드 시그니처
public List<Shipment> findByStatus(ShipmentStatus status) {
return repository.findAll().stream()
.filter(s -> s.getStatus() == status)
.toList();
}
// 3. 함수형 인터페이스
Predicate<Shipment> isActive = s -> s.getStatus() == ShipmentStatus.ACTIVE;
Function<Shipment, BigDecimal> toFare = Shipment::getFare;
// 4. 불변 컬렉션
List<String> ports = List.of("BUSAN", "INCHEON", "GWANGYANG");
Set<ShipmentStatus> finals = Set.of(DELIVERED, CANCELLED);
제네릭이 보장하는 타입 안전성의 정도는?
답:
컴파일 타임 안전성:
한계:
(List<String>)) 는 여전히 위험실용적 안전:
raw type (원시 타입):
제네릭 클래스를 타입 매개변수 없이 사용한 것.
예:
List ← raw type (List<E> 의 raw)
Map ← raw type
ArrayList ← raw type
Java 5 의 도전:
제네릭 도입하면서 기존 코드 호환?
기존 코드:
List list = new ArrayList();
list.add("Hello");
→ 새 List<E> 와 호환 안 되면 모든 코드 깨짐
→ raw type 으로 하위 호환성 유지
결과:
- List<E> 의 raw 가 List
- 기존 코드 그대로 사용 가능
- 단, 경고 발생
// raw type 사용 (권장 X)
List list = new ArrayList(); // raw — 타입 없음
list.add("Hello");
list.add(42); // 다양한 타입 추가 가능
list.add(true);
// 꺼낼 때
Object obj = list.get(0); // Object 타입
// 캐스트 필요
String s = (String) list.get(0); // 명시적 캐스트
1. 타입 안전성 손실
- 어떤 타입이든 추가 가능
- 컴파일 타임 검증 X
2. ClassCastException 위험
- 런타임 에러로 폭발
- 디버깅 어려움
3. 다른 제네릭과 혼용 시 위험
List<String> safe = new ArrayList<>();
List raw = safe; // 경고
raw.add(42); // safe 도 오염
String s = safe.get(0); // ClassCastException
4. 코드 의도 불명확
- "어떤 타입의 List?"
- 가독성 ↓
List list = new ArrayList();
list.add("Hello");
// 컴파일러 경고:
// warning: [rawtypes] found raw type: List
// warning: [unchecked] unchecked call to add(E) as a member of the raw type List
자바 컴파일러:
// 정상 제네릭
List<String> safe = new ArrayList<>();
safe.add("Hello");
// safe.add(42); // 컴파일 에러
// raw type
List raw = new ArrayList();
raw.add("Hello");
raw.add(42); // 경고만, 컴파일 OK
// 둘 사이의 변환
List<String> back = safe; // OK
List rawCopy = safe; // OK (경고)
List<String> dangerous = (List<String>) raw; // 경고 + 위험
dangerous.add(42); // 실제로는 raw 라 추가됨
List<Object> 차이// raw type
List raw = new ArrayList();
raw.add("Hello");
raw.add(42);
raw.add(true);
// List<Object>
List<Object> objects = new ArrayList<>();
objects.add("Hello");
objects.add(42);
objects.add(true);
// 둘 다 다양한 타입 추가 가능
// 하지만 다름:
// 1. 다른 제네릭과의 변환
List<String> strings = new ArrayList<>();
List raw2 = strings; // OK (경고)
List<Object> objs = strings; // ❌ 컴파일 에러!
// 2. 타입 안전성
raw.add(true); // 경고만
objects.add(true); // 컴파일 OK + 안전 (Object 가 부모)
List<Object>:
List:
권장:
✗ 새 코드에선 절대 사용 X
허용:
- Class 리터럴: List.class (raw type 표현)
- instanceof 검사: obj instanceof List
- Java 5 이전 코드와 호환
회피:
- 새 변수 선언
- 매개변수
- 반환 타입
대신:
- List<E> 사용
- List<?> 사용 (와일드카드, 다음 Unit)
raw type 이 위험한 이유와 사용 회피 방법은?
답:
위험:
회피 방법:
List<String> 명시List<E> 또는 List<?>예외:
List.class 같은 리터럴→ raw type 은 Java 5 이전과의 호환성을 위한 유산.
타입 소거 (Type Erasure):
컴파일 시점에 제네릭 타입 정보가 제거되는 메커니즘.
컴파일 후 바이트코드에는 raw type 만 남음.
영향:
- 런타임에 타입 정보 X
- List<String> 과 List<Integer> 는 같은 클래스
- .class 비교: List<String>.class == List<Integer>.class
// 소스 코드 (컴파일 전)
public class Box<T> {
private T value;
public T get() {
return value;
}
public void set(T value) {
this.value = value;
}
}
// 바이트코드 (컴파일 후, 타입 소거)
public class Box {
private Object value; // T → Object
public Object get() { // T → Object
return value;
}
public void set(Object value) { // T → Object
this.value = value;
}
}
핵심:
T → Object 로 소거// bound 가 있는 경우
public class NumberBox<T extends Number> {
private T value;
public T get() { return value; }
public void set(T value) { this.value = value; }
}
// 소거 후
public class NumberBox {
private Number value; // T → Number (bound)
public Number get() { return value; }
public void set(Number value) { this.value = value; }
}
→ T extends Number 면 T → Number 로 소거.
// 소스
Box<String> box = new Box<>();
box.set("Hello");
String s = box.get();
// 컴파일 후
Box box = new Box();
box.set("Hello");
String s = (String) box.get(); // ★ 자동 캐스트
→ 제거된 타입 정보를 캐스트로 보완.
List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
// 같은 클래스
System.out.println(strings.getClass() == ints.getClass()); // true
System.out.println(strings.getClass().getName()); // java.util.ArrayList
// 둘 다 List<E> 의 raw 인 ArrayList
// 타입 매개변수는 런타임에 없음
// 1. 같은 시그니처 메서드 정의 불가
public class Box<T> {
public void process(List<String> list) { }
public void process(List<Integer> list) { } // ❌ 컴파일 에러
// 이유: 소거 후 둘 다
// public void process(List list) { }
// 동일 시그니처 → 오버로딩 불가
}
// 2. 제네릭 배열 생성 불가
T[] arr = new T[10]; // ❌ 컴파일 에러
List<String>[] arr = new List<String>[10]; // ❌ 컴파일 에러
// 이유: 런타임에 T 가 무엇인지 모름
// 3. instanceof 검사 제약
if (obj instanceof List<String>) { } // ❌ 컴파일 에러
if (obj instanceof List<?>) { } // ✓ (와일드카드)
if (obj instanceof List) { } // ✓ (raw)
// 이유: 런타임에 List<String> 과 List<Integer> 가 같음
// 4. 제네릭 타입의 static 멤버 제약
public class Box<T> {
static T staticField; // ❌ 컴파일 에러
static void process(T t) { } // ❌ 컴파일 에러
}
// 이유: T 가 인스턴스마다 다름, static 은 클래스 레벨
왜 타입 소거?
이유 1: 하위 호환성
- Java 1.4 코드와 Java 5 코드 혼용
- raw type 과 제네릭의 공존
- .class 파일 호환
이유 2: 단순한 구현
- JVM 변경 최소화
- 기존 명령어 (invokevirtual 등) 그대로
- 추가 메타데이터 X
이유 3: 메모리 효율
- List<String> 과 List<Integer> 가 같은 클래스
- 클래스 정보 중복 회피
- C++ template 의 코드 폭증 회피
이유 4: 점진적 도입
- 기존 라이브러리 그대로 사용
- 새 코드는 제네릭 활용
- 마이그레이션 용이
1. 런타임 타입 정보 X
- 리플렉션 어려움
- JSON 직렬화 트릭 필요
2. 제네릭 배열 불가
- new T[] 못 함
- Array 와 제네릭의 충돌
3. instanceof 검사 제약
- List<String> 검사 X
4. 같은 시그니처 메서드 충돌
- 오버로딩 제약
5. 캐스트 비용
- 컴파일러가 자동 삽입
- 약간의 오버헤드
C# Generics:
- 런타임에도 타입 정보 보유
- List<string> 과 List<int> 가 다른 클래스
- 더 강력 + 더 안전
- 하지만 호환성 깨지는 큰 변화
자바:
- 호환성 우선
- 타입 소거 채택
- 트레이드오프
타입 소거의 정의와 의도는?
답:
정의:
T → Object (또는 bound)의도:
단점:
→ 호환성을 위한 의도적 타협.
소스 호환성 (Source Compatibility):
- 기존 .java 파일 재컴파일 가능
- 새 컴파일러로도 컴파일
바이너리 호환성 (Binary Compatibility):
- 기존 .class 파일이 새 JVM 에서 실행
- 링크 에러 없음
Java 5 의 목표:
1. 제네릭 도입 (타입 안전성)
2. 기존 코드와 호환 (소스 + 바이너리)
3. 점진적 마이그레이션
도전:
- 새 문법 (List<E>) 도입
- 기존 List 와 공존
- 기존 라이브러리 (.class) 그대로
해결:
- raw type 허용
- 타입 소거
- 컴파일러 경고만 (에러 X)
// Java 1.4 라이브러리 (변경 없이)
public class OldLibrary {
public List getNames() {
ArrayList list = new ArrayList();
list.add("Alice");
list.add("Bob");
return list;
}
}
// Java 5+ 코드
public class NewCode {
public void process() {
OldLibrary lib = new OldLibrary();
// 1. raw type 으로 받기 (호환)
List names = lib.getNames();
// 2. 제네릭으로 받기 (캐스트 + 경고)
List<String> names2 = (List<String>) lib.getNames();
// unchecked cast 경고
}
}
// 정상 제네릭 → raw
List<String> safe = new ArrayList<>();
List raw = safe; // ✓ OK (경고)
// raw → 정상 제네릭
List raw2 = new ArrayList();
List<String> safe2 = raw2; // ✓ OK (unchecked 경고)
// 둘 다 컴파일 가능, 단 경고
→ Java 5 의 의도적 유연성.
import java.util.ArrayList;
import java.util.List;
public class Compat {
public static void main(String[] args) {
// raw type
List list = new ArrayList();
list.add("Hello");
// warning: [unchecked] unchecked call to add(E) as a member of the raw type List
// 변환
List<String> typed = list;
// warning: [unchecked] unchecked conversion
// 위험한 사용
String s = typed.get(0); // 동작은 OK
list.add(42); // 잘못된 타입 추가
String s2 = typed.get(1); // ClassCastException!
}
}
타협:
1. raw type 허용
- 기존 코드 그대로
- 단, 경고 발생
2. 제네릭 + raw 변환 허용
- 양방향 가능
- 단, 경고
3. 타입 소거
- 런타임에 타입 정보 X
- 단순한 구현 + 호환
4. unchecked 경고
- 에러 X
- 단, 권장 X
결과:
- 점진적 마이그레이션 가능
- 기존 코드 깨짐 X
- 새 코드는 안전한 제네릭
// Step 1: Java 1.4 코드 (변경 없이)
public class V1 {
public ArrayList getItems() {
ArrayList items = new ArrayList();
items.add("a");
items.add("b");
return items;
}
}
// Step 2: 시그니처만 제네릭화
public class V2 {
public ArrayList<String> getItems() { // 반환 타입만
ArrayList items = new ArrayList(); // 내부는 raw
items.add("a");
items.add("b");
return items; // 자동 변환 (경고)
}
}
// Step 3: 완전 제네릭화
public class V3 {
public List<String> getItems() {
List<String> items = new ArrayList<>();
items.add("a");
items.add("b");
return items;
}
}
// Step 4: 사용 코드도 제네릭화
List<String> items = new V3().getItems();
for (String s : items) {
System.out.println(s);
}
→ 점진적 개선 가능.
Java 5 (2004) 부터 20년 후 (2024):
- 기존 Java 1.4 코드 여전히 작동
- 라이브러리 점진적 제네릭화
- 사용자 코드 점진적 마이그레이션
- 깨지는 코드 없음
이런 호환성은:
- C# 보다 약한 제네릭의 대가
- 하지만 자바 생태계의 안정성
- 큰 시스템 운영 가능
Java 5 의 제네릭 도입에서 호환성 타협 3가지는?
답:
1. raw type 허용:
타입 소거:
양방향 변환 허용:
→ 호환성과 안전성의 균형.
// Java 5, 6 (제네릭 도입 후 ~ 7 이전)
List<String> list = new ArrayList<String>();
// ↑
// 반복적인 <String>
Map<Long, Shipment> cache = new HashMap<Long, Shipment>();
// ↑
// 반복적인 <Long, Shipment>
// Java 7+
List<String> list = new ArrayList<>(); // <> 다이아몬드
Map<Long, Shipment> cache = new HashMap<>();
// 컴파일러가 좌변에서 타입 추론
1. 가독성 ↑
- 중복 제거
- 핵심 정보 (좌변) 강조
2. 작성 편의
- 타이핑 ↓
- IDE 자동완성과 잘 어울림
3. 리팩토링 안전
- 좌변만 변경하면 됨
- 우변 자동 따라옴
// ✓ 권장: 명확한 좌변
List<String> list = new ArrayList<>();
Map<String, List<Integer>> map = new HashMap<>();
// ✓ 메서드 호출 시
public void process() {
List<String> tmp = new ArrayList<>();
}
// △ 어색한 경우
ArrayList<String> list = new ArrayList<>(); // OK
List list = new ArrayList<>(); // △ raw + 다이아몬드 (이상)
// ❌ 제약
// 익명 클래스에선 Java 8 까지 못 씀 (Java 9+ 가능)
List<String> list = new ArrayList<>() { // Java 9+
// 익명 클래스 본체
};
// 컴파일러가 좌변에서 타입 추론
List<String> list = new ArrayList<>();
// ↑
// <> 가 String 으로 추론됨
// 매개변수 추론
public <T> List<T> single(T value) {
List<T> result = new ArrayList<>();
result.add(value);
return result;
}
// 호출
List<String> single = single("hello");
// 컴파일러가 T 를 String 으로 추론
// var 키워드 (Java 10+)
var list = new ArrayList<String>();
// 컴파일러가 list 의 타입을 ArrayList<String> 으로 추론
// 한계 1: 좌변이 불명확하면 추론 안 됨
List<? extends Number> list = new ArrayList<>();
// 와일드카드 + 다이아몬드 → 추론 가능하지만 제약
// 한계 2: 메서드 인자 추론 어려움
process(new ArrayList<>()); // 컨텍스트에 따라 다름
// 명시 권장: process(new ArrayList<String>());
// Java 7+
List<String> list = new ArrayList<>(); // 좌변 명시, 우변 다이아몬드
// Java 10+
var list = new ArrayList<String>(); // 좌변 var, 우변 명시
var map = new HashMap<String, Integer>();
// 결론:
// - 다이아몬드: 좌변 타입을 명시
// - var: 우변 타입을 명시
// - 둘 다 타입 정보 한 곳만 명시
권장:
// ILIC 예시
public class ShipmentService {
// 필드
private final Map<Long, Shipment> cache = new HashMap<>();
private final List<EventListener> listeners = new ArrayList<>();
// 메서드 내
public List<Shipment> findByStatus(ShipmentStatus status) {
List<Shipment> result = new ArrayList<>();
for (Shipment s : findAll()) {
if (s.getStatus() == status) {
result.add(s);
}
}
return result;
}
// Stream 결과
public Map<String, List<Shipment>> groupByRoute(List<Shipment> shipments) {
return shipments.stream()
.collect(Collectors.groupingBy(Shipment::getRoute));
// 좌변에서 타입 추론, 우변 명시 불필요
}
}
다이아몬드 연산자가 좋은 이유는?
답:
1. 중복 제거:
List<String> list = new ArrayList<>();<String> 생략가독성 ↑:
리팩토링 안전:
타이핑 ↓:
→ Java 7+ 의 작은 개선이지만 큰 효과.
// 1. unchecked 경고
List rawList = new ArrayList();
List<String> typed = rawList; // unchecked conversion
// 2. rawtypes 경고
List raw = ...; // raw type
// 3. deprecation 경고
Date.UTC(...); // deprecated 메서드
// 4. removal 경고
sun.misc.Unsafe.getUnsafe(); // 제거 예정 API
@SuppressWarnings:
특정 컴파일러 경고를 무시하도록 지정하는 어노테이션.
문법:
@SuppressWarnings("unchecked")
@SuppressWarnings({"unchecked", "rawtypes"})
// 메서드 레벨
@SuppressWarnings("unchecked")
public <T> T cast(Object o) {
return (T) o; // unchecked cast 경고 무시
}
// 클래스 레벨
@SuppressWarnings("rawtypes")
public class LegacyAdapter {
private List list; // raw type
// ...
}
// 변수 레벨 (지역 변수)
public void process() {
@SuppressWarnings("unchecked")
List<String> list = (List<String>) getList();
}
// 필드 레벨
public class MyClass {
@SuppressWarnings("unchecked")
private List<String> list = (List<String>) raw;
}
@SuppressWarnings 값:
"unchecked"
- unchecked cast
- unchecked conversion
- unchecked call
"rawtypes"
- raw type 사용
"deprecation"
- deprecated API 사용
"removal"
- 제거 예정 API
"serial"
- Serializable 의 serialVersionUID 없음
"unused"
- 사용 안 하는 변수/매개변수
"all"
- 모든 경고 (권장 X)
// ✓ 좋은 사용: 안전성 확인 후 무시
public class GenericCache<T> {
private final Map<Class<?>, Object> cache = new HashMap<>();
public <T> T get(Class<T> type) {
@SuppressWarnings("unchecked")
T value = (T) cache.get(type); // 타입 안전 보장됨
return value;
}
public <T> void put(Class<T> type, T value) {
cache.put(type, value);
}
}
// ✗ 나쁜 사용: 경고 그냥 무시
@SuppressWarnings("all")
public void doSomething() {
List list = new ArrayList(); // 위험
list.add(42);
// ...
}
권장:
1. 좁은 범위에 적용
- 메서드 < 클래스 < 패키지 순
- 가능한 한 작은 범위
2. 이유 주석
- 왜 경고 무시하는지 명시
- 안전성 검증 결과
3. 정기 검토
- 코드 변경 시 재검토
- 경고가 여전히 유효한지
회피:
1. @SuppressWarnings("all")
- 모든 경고 무시
- 위험 신호 놓침
2. 무분별한 사용
- 경고가 알려주는 위험 무시
- raw type 등 더 큰 문제
3. 잘못된 캐스트 정당화
- 실제로 안전하지 않은데 무시
// 제네릭 배열 생성 불가 우회
public class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(int capacity) {
array = (T[]) new Object[capacity]; // unchecked cast
// 안전: 외부에선 T[] 로 보임
}
public T get(int i) {
return array[i];
}
public void set(int i, T value) {
array[i] = value;
}
}
이런 패턴이 ArrayList, HashMap 등의 내부 구현에 활용.
IDE 가 경고 + 추천:
- 빨간 줄 (에러)
- 노란 줄 (경고)
- 회색 (사용 안 함)
Quick Fix:
- @SuppressWarnings 자동 추가
- 타입 추가
- 캐스트 추가
설정:
- 경고 레벨 조정
- 특정 경고 무시
@SuppressWarnings("unchecked") 를 적절히 사용하는 방법은?
답:
1. 좁은 범위: 메서드 또는 지역 변수에만
2. 안전성 검증 후: 실제로 안전한지 확인
3. 이유 주석: 왜 무시하는지 명시
4. 정기 검토: 코드 변경 시 재검토
회피:
@SuppressWarnings("all")→ "경고는 친구, 무시는 신중하게".
| Q | 핵심 답변 |
|---|---|
| 제네릭의 정의? | 타입을 매개변수화하는 기능 |
| 제네릭 등장 이유? | 타입 안전성 + 캐스트 제거 |
| Java 5 도입 시기? | 2004년 |
| raw type 의 정의? | 타입 매개변수 없는 제네릭 사용 |
| raw type 위험? | 타입 안전성 손실, CCE |
| 타입 소거의 정의? | 컴파일 시 제네릭 타입 제거 |
| 타입 소거 이유? | 하위 호환성, JVM 최소 변경 |
| List.class? | List.class (소거로 같음) |
| 제네릭 배열 불가 이유? | 런타임 타입 정보 X |
| 다이아몬드 연산자? | Java 7+, 우변 타입 추론 |
| @SuppressWarnings 사용? | 검증 후 좁은 범위에 |
| C# Generics 차이? | C# 은 런타임 타입 보유 |
답:
List<String> 은 List<Object> 의 자식 XList<String> strings = new ArrayList<>();
List<Object> objects = strings; // ❌ 컴파일 에러
답:
// 제네릭 클래스
public class Box<T> {
private T value;
// ...
}
// 제네릭 메서드 (일반 클래스에 추가)
public class Utils {
public static <T> T first(List<T> list) {
return list.get(0);
}
}
// 차이:
// - 클래스: 인스턴스마다 타입 결정
// - 메서드: 호출마다 타입 결정
// - 자세한 건 Unit 5.4
답:
답:
// Java 7+
List<String> list = new ArrayList<>(); // 다이아몬드
list.add("hello"); // 타입 안전
String s = list.get(0); // 자동 캐스트
// 효과:
// - 좌변에서 타입 추론 (다이아몬드)
// - 컴파일 시 타입 검증 (제네릭)
// - 런타임 안전 (캐스트 자동)
답:
1. 런타임 타입 정보 X (소거)
2. 제네릭 배열 불가 (new T[] 못 함)
3. static 멤버에 T 불가 (인스턴스마다 다름)
추가:
1. 제네릭 = 타입 매개변수화
2. raw type = 하위 호환의 유산
3. 타입 소거 = 호환성의 대가
이번 Unit에서 제네릭의 기본을 봤다면, 다음은 타입 매개변수의 활용.
🚀 Phase 5 — 제네릭과 와일드카드
✅ Unit 5.1 제네릭의 등장과 raw type ← 여기
⏭ Unit 5.2 타입 매개변수와 타입 인자
⏭ Unit 5.3 와일드카드 ? extends, ? super
⏭ Unit 5.4 제네릭 메서드 + 제네릭 클래스
⏭ Unit 5.5 PECS 원칙 (마스터 깊이)
✅ Phase 1 — Pass by Value (1.1 ~ 1.3 완주)
✅ Phase 2 — 컬렉션 프레임워크 (2.1 ~ 2.6 완주)
✅ Phase 3 — 해시의 원리 (3.1 ~ 3.4 완주)
✅ Phase 4 — 추상화의 두 도구 (4.1 ~ 4.4 완주)
🚀 Phase 5 — 제네릭과 와일드카드 (1/5 진행)
총: 18/43 Unit 작성 (약 42%)