F-LAB JAVA · 3주차 · Phase 5 · 제네릭과 와일드카드
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
제네릭 클래스는 "타입이 인스턴스마다 다른" 클래스, 제네릭 메서드는 "타입이 호출마다 다른" 메서드다.
컴파일러의 타입 추론 으로 대부분의 호출에서 명시적 타입 인자가 불필요하며,
다이아몬드 (<>) 와 var 가 이를 더 간결하게 만든다.
자바 표준 라이브러리 (Collections.sort,Stream.collect,Optional.map등) 의 정밀한 시그니처가
모두 이 두 도구의 조합과 PECS 원칙으로 설계되어 있다.
제네릭 클래스 = 공장
Box<T> = "타입 T 의 박스를 만드는 공장"
- 한 공장이 정해진 한 타입의 박스만
- 인스턴스마다 타입 다름
- String 박스 공장, Integer 박스 공장 등
제네릭 메서드 = 작업자
<T> T process(T t) = "한 호출당 한 타입을 처리하는 작업자"
- 호출마다 타입 다름
- 같은 메서드가 String 처리, Integer 처리, ...
- 클래스 인스턴스화 없이 호출 가능 (static)
→ 클래스 = 인스턴스 단위 타입, 메서드 = 호출 단위 타입.
1. 제네릭 클래스 vs 제네릭 메서드의 차이
2. 타입 추론 (Type Inference) 메커니즘
3. 타깃 타입 (Target Typing)
4. 제네릭 생성자
5. 제네릭 클래스 + 제네릭 메서드 결합
6. var 와 제네릭
7. 익명/로컬 제네릭 활용
8. 자바 표준 라이브러리 정밀 분석
9. 면접 + 자기 점검
// 제네릭 클래스 — 클래스 이름 뒤에 <T>
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T get() { return value; }
public void set(T value) { this.value = value; }
}
// 제네릭 메서드 — 반환 타입 앞에 <T>
public class Utils {
public static <T> T identity(T input) {
return input;
}
}
핵심:
class Name<T>public <T> ReturnType method(...)// 제네릭 클래스 사용
Box<String> stringBox = new Box<>("hello");
Box<Integer> intBox = new Box<>(42);
// 각 인스턴스가 정해진 T 보유
stringBox.set("world"); // String 만
intBox.set(99); // Integer 만
// 제네릭 메서드 사용
String s = Utils.identity("hello"); // T = String
Integer i = Utils.identity(42); // T = Integer
Shipment sh = Utils.identity(new Shipment()); // T = Shipment
// 같은 메서드, 호출마다 다른 T
// 제네릭 클래스 — 인스턴스화 필요
Box<String> box = new Box<>("hello"); // ★ new 필요
String value = box.get();
// 제네릭 메서드 — static 이면 인스턴스화 불필요
String result = Utils.identity("hello"); // 호출만
// 인스턴스 메서드라면 인스턴스화 필요
public class MyClass {
public <T> T process(T input) { return input; }
}
MyClass obj = new MyClass();
String s = obj.process("hello");
public class Box<T> { // 클래스의 T
private T value; // 필드 — 클래스 T
public T get() { // 메서드 — 클래스 T
return value;
}
public <U> U transform(Function<T, U> fn) { // 메서드 — 자기 U
return fn.apply(value);
// ↑
// value 의 타입 T (클래스), fn 의 반환 U (메서드)
}
// T 와 U 는 서로 무관
}
// 사용
Box<String> box = new Box<>("hello");
Integer length = box.transform(String::length);
// ↑
// T = String (클래스), U = Integer (메서드)
public class Box<T> {
private T value; // OK — 인스턴스 필드
// ❌ static 필드에 T 불가
private static T staticValue;
// ❌ static 메서드의 매개변수에 클래스 T 불가
public static void process(T input) { }
// ✓ static 메서드는 자기만의 T 가능
public static <T> T identity(T input) { // 메서드 T, 클래스와 무관
return input;
}
}
이유:
// 제네릭 클래스 활용
public class Cache<K, V> {
private Map<K, V> store = new HashMap<>();
public V get(K key) { return store.get(key); }
public void put(K key, V value) { store.put(key, value); }
}
Cache<Long, Shipment> shipmentCache = new Cache<>();
Cache<String, User> userCache = new Cache<>();
// 제네릭 메서드 활용 (보통 유틸리티)
public class CollectionUtils {
public static <T> List<T> repeat(T value, int times) {
List<T> list = new ArrayList<>();
for (int i = 0; i < times; i++) {
list.add(value);
}
return list;
}
public static <T> Optional<T> first(List<T> list) {
return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0));
}
}
// 호출
List<String> repeated = CollectionUtils.repeat("hello", 3);
Optional<Integer> first = CollectionUtils.first(List.of(1, 2, 3));
public class Container<T> { // 클래스의 T
private List<T> items = new ArrayList<>();
public void add(T item) { // 클래스 T 사용
items.add(item);
}
public T get(int i) { // 클래스 T 반환
return items.get(i);
}
// 메서드만의 추가 매개변수
public <R> List<R> map(Function<T, R> fn) { // 클래스 T, 메서드 R
List<R> result = new ArrayList<>();
for (T item : items) {
result.add(fn.apply(item));
}
return result;
}
// 메서드만의 또 다른 T (shadowing — 권장 X)
// public <T> void process(T anotherT) { ... }
// 같은 이름 T 이지만 클래스 T 와 다름 — 혼란 → 회피
}
// 사용
Container<String> container = new Container<>();
container.add("hello");
List<Integer> lengths = container.map(String::length);
// ↑
// T = String, R = Integer
제네릭 클래스와 제네릭 메서드의 결정적 차이는?
답:
1. 타입 결정 시점:
선언 위치:
class Name<T><T> ReturnType method(...)static 제약:
활용 패턴:
타입 추론 (Type Inference):
컴파일러가 컨텍스트를 분석해 타입 매개변수를 자동으로 결정.
사용자가 명시 안 해도 컴파일러가 추론.
// 1. 다이아몬드 연산자 (Java 7+)
List<String> list = new ArrayList<>();
// ↑
// 컴파일러가 String 추론
// 2. 제네릭 메서드 호출 (Java 5+)
String s = Utils.identity("hello");
// ↑
// T = String 추론
// 3. 람다 매개변수 (Java 8+)
List<String> list = ...;
list.stream().filter(s -> s.length() > 5);
// ↑
// s 의 타입 String 추론
// 4. var 키워드 (Java 10+)
var list = new ArrayList<String>();
// ↑
// ArrayList<String> 추론
컴파일러의 추론 과정:
1. 컨텍스트 분석:
- 좌변 타입
- 메서드 시그니처
- 호출 인자
2. 제약 수집:
- "T 는 String 과 호환"
- "T 는 Comparable 의 구현"
- 등등
3. 가장 좁은 타입 결정:
- 제약을 만족하는 가장 정확한 타입
- 보통 인자의 타입
4. 타입 매개변수 대체:
- T 자리에 실제 타입 삽입
public static <T> T identity(T input) {
return input;
}
// 호출 분석:
String s = identity("hello");
// 1. 컨텍스트: 반환을 String 변수에 할당
// 2. 인자 타입: "hello" → String
// 3. 결과: T = String 추론
// 4. 메서드 시그니처가 됨: String identity(String input)
public static <T> List<T> singletonList(T value) {
return Collections.singletonList(value);
}
// 호출 1: 인자만 사용
List<Integer> list = singletonList(42);
// 1. 인자: 42 → Integer
// 2. T = Integer 추론
// 3. 반환: List<Integer>
// 호출 2: 좌변 활용
List<Number> list = singletonList(42);
// 1. 좌변: List<Number>
// 2. 인자: 42 → Integer
// 3. T = Number? Integer?
// 4. Integer 가 더 정확 → T = Integer
// 5. 반환 List<Integer> 를 List<Number> 에 ❌
// 컴파일 에러: incompatible types
// 호출 3: 명시적
List<Number> list = Utils.<Number>singletonList(42);
// 1. T = Number 명시
// 2. 인자: 42 → Integer (Number 의 자식, 자동 변환)
// 3. 반환 List<Number>, 좌변과 일치 ✓
public static <T> List<T> single(T value) {
return List.of(value);
}
// 호출
List<Integer> list = single(42);
// 1. 42 는 int (primitive)
// 2. 자동 박싱: int → Integer
// 3. T = Integer 추론
// 4. 반환 List<Integer>
// 주의: int 의 List 는 불가
// List<int> list = single(42); // ❌
// 제네릭은 객체 타입만 (Integer 만)
public static <T> T pick(T a, T b) {
return a;
}
// 호출 1: 같은 타입
String s = pick("a", "b"); // T = String
// 호출 2: 다른 타입 — LUB (Least Upper Bound)
// Integer 와 Double 의 공통 부모
Number n = pick(1, 1.0);
// T = Number (LUB)
// 또는 더 정확히: Number & Comparable<...>
// Java 의 LUB 알고리즘:
// 두 타입의 공통 부모 중 가장 좁은 것
public class Utils {
public static <T> List<T> emptyList() {
return new ArrayList<>();
}
}
// 추론 (가능한 경우)
List<String> list1 = Utils.emptyList(); // 좌변에서 T = String
// 명시 (필요한 경우)
List<String> list2 = Utils.<String>emptyList(); // T 명시
// 명시 필요한 경우:
// 1. 메서드 인자에 T 없음 (좌변만 추론 단서)
// 2. 다른 코드와 함께 사용
process(Utils.emptyList()); // process(Object) 추론
process(Utils.<String>emptyList()); // process(List<String>)
자바의 타입 추론이 어떻게 동작하나?
답:
1. 컨텍스트 분석:
제약 수집:
추론:
결과:
→ Java 7 다이아몬드, Java 10 var 로 더 강력해짐.
타깃 타입 (Target Typing):
표현식의 컨텍스트로부터 기대되는 타입.
컴파일러가 이를 활용해 타입 추론.
예:
List<String> list = new ArrayList<>();
↑ ↑
이게 표현식 타깃 타입은 List<String>
// Java 7+
List<String> list = new ArrayList<>();
// 1. 좌변 타깃 타입: List<String>
// 2. 우변 다이아몬드: ArrayList<?>
// 3. 추론: ArrayList<String>
// 더 복잡한 케이스
Map<String, List<Integer>> map = new HashMap<>();
// 타깃: Map<String, List<Integer>>
// 추론: HashMap<String, List<Integer>>
public void process(List<String> list) { ... }
// 호출
process(new ArrayList<>());
// 1. process 의 매개변수 타입: List<String>
// 2. 타깃 타입: List<String>
// 3. 추론: new ArrayList<String>()
// 더 복잡
process(Utils.emptyList());
// emptyList 의 반환 타입 추론
// 타깃: List<String>
// 추론: T = String, 반환 List<String>
Comparator<String> comp = (a, b) -> a.length() - b.length();
// 1. 좌변 타깃: Comparator<String>
// 2. 람다의 매개변수: a, b (타입 없음)
// 3. Comparator<String>.compare(String, String) 시그니처
// 4. 추론: a 는 String, b 는 String
// 타깃 없으면 추론 실패
Object x = (a, b) -> a.length() - b.length();
// 컴파일 에러: 타깃이 Comparator 인지 모름
List<String> names = ...;
// 타깃 타입 활용
List<String> upper = names.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
// 컴파일러의 분석:
// 1. names.stream() → Stream<String>
// 2. map(Function<String, R>) — R 추론 필요
// 3. s -> s.toUpperCase() — s 는 String, 반환 String
// 4. R = String 추론
// 5. .map() 반환: Stream<String>
// 6. collect → 타깃이 List<String>
// 7. Collectors.toList() — T = String 추론
public Optional<Integer> findAge(String name) {
return Optional.of(...);
}
// 빈 Optional
public Optional<Integer> notFound() {
return Optional.empty();
// empty() 의 타입은 Optional<T>
// 메서드 반환 타입 Optional<Integer> 가 타깃
// T = Integer 추론
}
// 명시 필요한 경우
public <T> Optional<T> get() {
return Optional.<T>empty();
// 타깃이 Optional<T> 인데 T 는 메서드 자신의 T
// 명시가 더 안전
}
List<Shipment> shipments = ...;
// 체이닝의 각 단계마다 타깃이 다음 단계
Map<String, BigDecimal> totalByRoute = shipments.stream()
.collect(Collectors.groupingBy(
Shipment::getRoute,
Collectors.reducing(
BigDecimal.ZERO,
Shipment::getFare,
BigDecimal::add
)
));
// 컴파일러가 각 Collectors 의 T, K, V, R 추론
// 타깃 타입이 없으면 추론 실패
Utils.emptyList().size();
// 좌변이 size() 의 호출 — int
// emptyList() 의 타입 추론?
// 기본: List<Object>
// 명확하게:
List<String> list = Utils.emptyList(); // 타깃 명시
list.size();
타깃 타입이 무엇이고 왜 중요한가?
답:
정의:
활용:
new ArrayList<>()Comparator<String> c = (a, b) -> ...Collectors.toList()Optional.empty()중요한 이유:
한계:
제네릭 생성자:
자체 타입 매개변수를 가진 생성자.
클래스가 제네릭이든 아니든 가능.
// 일반 클래스의 제네릭 생성자
public class MyClass {
private Object value;
public <T> MyClass(T value) { // ← 생성자만의 T
this.value = value;
System.out.println("Created with: " + value.getClass());
}
}
// 사용
MyClass m1 = new MyClass("hello"); // T = String
MyClass m2 = new MyClass(42); // T = Integer
MyClass m3 = new <Integer>MyClass(42); // 명시
public class Pair<A, B> {
private A first;
private B second;
// 기본 생성자 — 클래스 T 사용
public Pair(A first, B second) {
this.first = first;
this.second = second;
}
// 제네릭 생성자 — 자신만의 T
public <T extends Comparable<T>> Pair(T value, B second) {
this.first = (A) value; // 캐스트 필요
this.second = second;
}
}
public class TypedBox<T> {
private T value;
public TypedBox(T value) {
this.value = value;
}
// 다른 타입의 TypedBox 로부터 생성 + 변환
public <U> TypedBox(TypedBox<U> source, Function<U, T> converter) {
this.value = converter.apply(source.value);
}
}
// 사용
TypedBox<String> stringBox = new TypedBox<>("123");
TypedBox<Integer> intBox = new TypedBox<>(stringBox, Integer::parseInt);
// String "123" → Integer 123
// ArrayList 의 생성자
public class ArrayList<E> {
// 기본 — 클래스 E 사용
public ArrayList() { ... }
public ArrayList(int initialCapacity) { ... }
// 컬렉션 생성자 — 자신의 E + 와일드카드
public ArrayList(Collection<? extends E> c) { ... }
// ↑
// Producer
}
// HashMap 도 비슷
public class HashMap<K, V> {
public HashMap() { ... }
public HashMap(int initialCapacity) { ... }
public HashMap(int initialCapacity, float loadFactor) { ... }
public HashMap(Map<? extends K, ? extends V> m) { ... }
}
public class MyClass {
public <T> MyClass(T value) { ... }
}
// 1. 추론 (대부분)
MyClass m = new MyClass("hello"); // T = String 추론
// 2. 명시 (드물게)
MyClass m = new <String>MyClass("hello");
// ← <Type> 이 new 와 클래스명 사이
// 또는
MyClass m = new MyClass.<String>method(...);
// 인스턴스 메서드 호출 시
// 다이아몬드 + 명시
List<String> list = new ArrayList<String>(); // 명시
List<String> list = new ArrayList<>(); // 다이아몬드
권장:
1. 변환 생성자
- 다른 타입에서 생성
- 예: TypedBox(TypedBox<U>, Function<U, T>)
2. 빌더 패턴
- 다양한 입력에서 동일 객체 생성
- 예: <T> Builder(T input)
3. 팩토리 메서드와 결합
- 정적 팩토리가 제네릭이라면
회피:
1. 단순한 경우
- 클래스 매개변수로 충분
- 복잡도만 ↑
2. 의도 불명확한 경우
- 사용자 혼란
제네릭 생성자의 활용 사례는?
답:
1. 변환 생성자:
<U> TypedBox(TypedBox<U>, Function<U, T>)컬렉션 복사 생성자:
ArrayList(Collection<? extends E>)유틸리티 빌더:
드물게 사용:
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T get() {
return value;
}
// 메서드만의 U
public <U> U transform(Function<T, U> fn) {
return fn.apply(value);
}
// 다중 메서드 매개변수
public <U, V> Pair<U, V> mapToPair(
Function<T, U> first,
Function<T, V> second) {
return new Pair<>(first.apply(value), second.apply(value));
}
}
// 사용
Box<String> box = new Box<>("hello");
Integer length = box.transform(String::length); // T=String, U=Integer
Boolean empty = box.transform(String::isEmpty); // T=String, U=Boolean
Pair<Integer, Character> pair = box.mapToPair(
String::length, // U=Integer
s -> s.charAt(0) // V=Character
);
public class Container<T> {
private List<T> items = new ArrayList<>();
public void add(T item) {
items.add(item);
}
// 메서드 매개변수가 T 의 자식만
public <S extends T> void addAll(Collection<S> source) {
for (S item : source) {
items.add(item); // S 가 T 의 자식이라 가능
}
}
// 와일드카드도 가능
public void addAllWild(Collection<? extends T> source) {
for (T item : source) {
items.add(item);
}
}
}
// 사용
Container<Number> container = new Container<>();
container.addAll(List.of(1, 2, 3)); // S = Integer (Number 의 자식)
container.addAll(List.of(1.0, 2.0)); // S = Double
// 다음도 가능
container.addAllWild(List.<Integer>of(1, 2)); // ? extends Number
public class Pair<A, B> {
private A first;
private B second;
// 정적 팩토리 — 메서드 자체의 T, U
public static <T, U> Pair<T, U> of(T first, U second) {
return new Pair<>(first, second);
}
// 단일 타입 팩토리
public static <T> Pair<T, T> twin(T value) {
return new Pair<>(value, value);
}
}
// 사용
Pair<String, Integer> p1 = Pair.of("age", 25);
Pair<String, String> p2 = Pair.twin("hello");
public abstract class Builder<B extends Builder<B>> {
protected abstract B self();
public B with(String key, Object value) {
// 설정
return self();
}
}
// 자식이 B 명시
public class ShipmentBuilder extends Builder<ShipmentBuilder> {
@Override
protected ShipmentBuilder self() {
return this;
}
// 자식의 메서드 추가
public ShipmentBuilder withBlNo(String blNo) {
return with("blNo", blNo);
}
}
// 사용 — Fluent API
ShipmentBuilder builder = new ShipmentBuilder()
.with("priority", 1) // ShipmentBuilder 반환
.withBlNo("BL-001") // ShipmentBuilder 의 메서드
.with("urgency", "high");
// 1. Optional<T>
public final class Optional<T> {
public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
// 클래스 T, 메서드 U
}
public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper) {
// 클래스 T, 메서드 U
}
public static <T> Optional<T> of(T value) {
// 정적 팩토리, 메서드 T
}
}
// 2. Stream<T>
public interface Stream<T> {
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
<R, A> R collect(Collector<? super T, A, R> collector);
}
// 3. CompletableFuture<T>
public class CompletableFuture<T> {
public <U> CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn) { ... }
public <U, V> CompletableFuture<V> thenCombine(
CompletionStage<? extends U> other,
BiFunction<? super T, ? super U, ? extends V> fn) { ... }
}
public class ShipmentRepository<T extends Shipment> {
private final Map<Long, T> cache = new HashMap<>();
public T save(T shipment) {
cache.put(shipment.getId(), shipment);
return shipment;
}
public Optional<T> findById(Long id) {
return Optional.ofNullable(cache.get(id));
}
// 메서드 자체의 R
public <R> List<R> findAllAndMap(Function<T, R> mapper) {
return cache.values().stream()
.map(mapper)
.toList();
}
// S extends T — T 의 자식만 추가
public <S extends T> void saveAll(Collection<S> shipments) {
for (S s : shipments) {
save(s);
}
}
// 정적 팩토리
public static <T extends Shipment> ShipmentRepository<T> create() {
return new ShipmentRepository<>();
}
}
// 사용
ShipmentRepository<Shipment> repo = ShipmentRepository.create();
repo.save(new SeaShipment());
repo.save(new AirShipment());
repo.saveAll(List.<SeaShipment>of(...)); // S = SeaShipment
List<String> blNos = repo.findAllAndMap(Shipment::getBlNo); // R = String
List<BigDecimal> weights = repo.findAllAndMap(Shipment::getWeight); // R = BigDecimal
public class Box<T> {
private T value;
// ❌ T 와 같은 이름 사용 (shadowing — 권장 X)
public <T> T processAnother(T other) {
// 여기 T 는 메서드의 T, 클래스 T 와 무관
// 혼란!
return other;
}
// ✓ 다른 이름 사용
public <U> U processAnother(U other) {
return other;
}
}
→ 같은 이름의 타입 매개변수는 shadowing 발생, 회피.
제네릭 클래스의 T 와 제네릭 메서드의 U 를 함께 쓰는 경우는?
답:
1. 변환 메서드:
<U> U transform(Function<T, U> fn)추가 매개변수:
<S extends T> void addAll(Collection<S>)정적 팩토리:
static <T> Optional<T> of(T value)자바 표준의 정밀 패턴:
Optional.map, Stream.map, Stream.collect→ 클래스 T 와 메서드 U 를 명확히 구분.
// Java 10 부터
var list = new ArrayList<String>();
// ↑
// ArrayList<String> 으로 추론
var map = new HashMap<Long, Shipment>();
// ↑
// HashMap<Long, Shipment> 으로 추론
// ✓ 가능
var x = 10; // int
var s = "hello"; // String
var list = new ArrayList<String>(); // ArrayList<String>
var stream = list.stream(); // Stream<String>
// ❌ 불가
// 1. 초기화 없이
var x; // 컴파일 에러
// 2. null 만으로
var x = null; // 컴파일 에러
// 3. 람다 직접 (Java 11+ 부터 매개변수에 가능)
var fn = (String s) -> s.length(); // 에러
// 4. 메서드 매개변수
public void process(var x) { } // 에러
// 5. 필드
public class MyClass {
var field; // 에러
}
// 명시적 타입 + 다이아몬드
List<String> list = new ArrayList<>();
// 좌변에서 타입, 우변 다이아몬드
// var + 명시적 타입
var list = new ArrayList<String>();
// 우변에서 타입, 좌변 var
// var + 다이아몬드 — 컴파일러 추론 실패
var list = new ArrayList<>(); // ArrayList<Object> 가 됨
// 좌변 var 는 타깃 X
// 우변 <> 는 좌변 의존
// 결과: ArrayList<Object> (의도와 다름)
→ var 와 다이아몬드 동시 사용은 의도와 다른 결과 가능성.
// 1. 명확한 우변
var list = new ArrayList<String>(); // 명확
var map = new HashMap<Long, Shipment>(); // 명확
// 2. Stream 결과
var groups = shipments.stream()
.collect(Collectors.groupingBy(Shipment::getRoute));
// 타입: Map<String, List<Shipment>>
// 명시하면 매우 김
// 3. 메서드 반환 활용
var result = service.findActive();
// service.findActive() 의 반환 타입으로
// 4. 람다 매개변수 (Java 11+)
list.forEach((var s) -> System.out.println(s));
// ↑
// s 의 타입 추론 (String)
권장:
✓ 우변에서 타입이 명확
✓ 긴 제네릭 타입 (가독성 ↑)
✓ 로컬 변수
✓ Stream 결과 등 복잡한 타입
회피:
✗ 우변이 모호
✗ 변수 이름이 의미 불명확
✗ public API
✗ 디버깅 어려운 곳
public class ShipmentService {
public void processShipments() {
// var 활용
var shipments = repository.findAll();
// List<Shipment>
var byRoute = shipments.stream()
.collect(Collectors.groupingBy(Shipment::getRoute));
// Map<String, List<Shipment>>
var totalByRoute = byRoute.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().stream()
.map(Shipment::getFare)
.reduce(BigDecimal.ZERO, BigDecimal::add)
));
// Map<String, BigDecimal>
// 짧고 명확
}
}
| 항목 | 다이아몬드 <> | var |
|---|---|---|
| 도입 | Java 7 | Java 10 |
| 타입 정보 | 좌변 | 우변 |
| 사용 | 변수 선언 | 변수 선언 |
| 제약 | 거의 없음 | 초기화 필수, 람다 X |
| 활용 | 컬렉션 생성 | 모든 로컬 변수 |
var 와 다이아몬드를 어떻게 조합하나?
답:
1. 좌변 명시 + 다이아몬드 (Java 7+):
List<String> list = new ArrayList<>();var + 우변 명시 (Java 10+):
var list = new ArrayList<String>();둘 다 사용은 의도와 다름:
var list = new ArrayList<>();가이드:
public interface Comparator<T> {
int compare(T o1, T o2);
}
// 익명 클래스로 구현
Comparator<String> byLength = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
};
// Java 9+ 다이아몬드 가능
Comparator<String> byLength2 = new Comparator<>() {
@Override
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
};
// 람다로 더 간결
Comparator<String> byLength3 = (s1, s2) -> s1.length() - s2.length();
// Iterator 직접 구현
public Iterator<Shipment> activeIterator(List<Shipment> all) {
return new Iterator<Shipment>() { // Java 9+: new Iterator<>()
private int index = 0;
private Shipment next = null;
@Override
public boolean hasNext() {
while (index < all.size()) {
Shipment s = all.get(index++);
if (s.getStatus() == ShipmentStatus.ACTIVE) {
next = s;
return true;
}
}
return false;
}
@Override
public Shipment next() {
if (next == null) throw new NoSuchElementException();
Shipment result = next;
next = null;
return result;
}
};
}
// Callable, Runnable 등
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> result = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return doWork();
}
});
// 람다로
Future<String> result2 = executor.submit(this::doWork);
// 람다로 제네릭 비슷한 효과
public void process() {
Function<Integer, Integer> doubleIt = n -> n * 2;
Function<String, Integer> length = String::length;
// 람다는 함수형 인터페이스의 인스턴스
// 그 자체로 제네릭은 아님
}
// 로컬 제네릭 메서드는 불가
public void process() {
// ❌ 로컬 메서드의 제네릭 불가
// public <T> T helper(T input) { ... }
// ✓ 로컬 람다는 가능 (제네릭이 아닌 특정 타입)
Function<String, Integer> helper = String::length;
}
→ 자바엔 로컬 제네릭 메서드 없음.
// 제네릭 메서드 + 람다 매개변수
public static <T, R> List<R> map(List<T> list, Function<T, R> fn) {
List<R> result = new ArrayList<>();
for (T t : list) {
result.add(fn.apply(t));
}
return result;
}
// 사용 — 람다 전달
List<String> names = List.of("Alice", "Bob");
List<Integer> lengths = map(names, String::length);
// 또는
List<Integer> lengths2 = map(names, s -> s.length());
// 컴파일러 추론:
// 1. map(List<String>, Function<String, Integer>)
// 2. T = String, R = Integer
// 3. 결과 List<Integer>
// 익명 클래스
Comparator<String> c1 = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};
// 람다
Comparator<String> c2 = (a, b) -> a.length() - b.length();
// 차이:
// 1. 코드 길이 — 람다가 짧음
// 2. this — 익명 클래스의 this 는 자기, 람다는 외부 this
// 3. 직렬화 — 람다는 직렬화 가능 (특수 조건)
// 4. 성능 — 비슷 (JIT 후엔 거의 동일)
// 5. 표현력 — 람다는 SAM 인터페이스만, 익명은 일반 클래스 가능
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
// 다양한 활용
Function<String, Integer> length = String::length;
Function<Integer, String> toStr = String::valueOf;
Function<Shipment, BigDecimal> toFare = Shipment::getFare;
// 합성
Function<String, String> combined = length.andThen(toStr);
combined.apply("hello"); // "5"
// BiFunction<T, U, R>
BiFunction<Integer, Integer, Integer> add = Integer::sum;
// Predicate<T>
Predicate<Shipment> isActive = s -> s.getStatus() == ACTIVE;
Predicate<Shipment> isHeavy = s -> s.getWeight().compareTo(new BigDecimal("1000")) > 0;
// 조합
Predicate<Shipment> activeAndHeavy = isActive.and(isHeavy);
// Consumer<T>
Consumer<Shipment> printer = s -> System.out.println(s.getBlNo());
// Supplier<T>
Supplier<List<String>> listSupplier = ArrayList::new;
익명 제네릭 클래스와 람다 중 어느 것을?
답:
람다 권장 (대부분):
익명 클래스 사용:
→ 가능하면 람다, 어려우면 익명 클래스.
public class Collections {
// sort
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}
// binarySearch
public static <T> int binarySearch(
List<? extends Comparable<? super T>> list, T key) {
// ...
}
public static <T> int binarySearch(
List<? extends T> list, T key, Comparator<? super T> c) {
// ...
}
// max
public static <T extends Object & Comparable<? super T>> T max(
Collection<? extends T> coll) {
// ...
}
public static <T> T max(
Collection<? extends T> coll, Comparator<? super T> comp) {
// ...
}
// copy
public static <T> void copy(
List<? super T> dest, List<? extends T> src) {
// PECS!
}
// addAll
public static <T> boolean addAll(
Collection<? super T> c, T... elements) {
// ...
}
// emptyList
public static <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}
// unmodifiableList
public static <T> List<T> unmodifiableList(List<? extends T> list) {
return new UnmodifiableList<>(list);
}
}
분석:
extends for input (Producer)super for output (Consumer)Comparable<? super T> for 비교public interface Stream<T> extends BaseStream<T, Stream<T>> {
// filter — Predicate
Stream<T> filter(Predicate<? super T> predicate);
// map — Function
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
// flatMap — Function
<R> Stream<R> flatMap(
Function<? super T, ? extends Stream<? extends R>> mapper);
// forEach — Consumer
void forEach(Consumer<? super T> action);
// reduce
T reduce(T identity, BinaryOperator<T> accumulator);
Optional<T> reduce(BinaryOperator<T> accumulator);
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
// collect — Collector
<R, A> R collect(Collector<? super T, A, R> collector);
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
// min, max
Optional<T> min(Comparator<? super T> comparator);
Optional<T> max(Comparator<? super T> comparator);
// sorted
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
// 다양한 static 팩토리
static <T> Stream<T> of(T t);
static <T> Stream<T> of(T... values);
static <T> Stream<T> empty();
static <T> Stream<T> generate(Supplier<? extends T> s);
static <T> Stream<T> iterate(T seed, UnaryOperator<T> f);
}
public final class Optional<T> {
// 정적 팩토리
public static <T> Optional<T> empty() { ... }
public static <T> Optional<T> of(T value) { ... }
public static <T> Optional<T> ofNullable(T value) { ... }
// 인스턴스 메서드 — 클래스 T 활용
public T get() { ... }
public boolean isPresent() { ... }
public T orElse(T other) { ... }
public T orElseGet(Supplier<? extends T> supplier) { ... }
// 메서드 자체의 U
public <U> Optional<U> map(Function<? super T, ? extends U> mapper) { ... }
public <U> Optional<U> flatMap(
Function<? super T, ? extends Optional<? extends U>> mapper) { ... }
public void ifPresent(Consumer<? super T> action) { ... }
public Optional<T> filter(Predicate<? super T> predicate) { ... }
}
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
// 합성 — 자기 자신과 다른 Function 결합
default <V> Function<V, R> compose(
Function<? super V, ? extends T> before) {
// V → T → R
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(
Function<? super R, ? extends V> after) {
// T → R → V
return (T t) -> after.apply(apply(t));
}
// 정적
static <T> Function<T, T> identity() {
return t -> t;
}
}
분석:
compose: V → T → R (다른 Function 먼저)andThen: T → R → V (자신 먼저, 다른 후)? super V, ? extends T 등 PECS 활용public interface Comparator<T> {
int compare(T o1, T o2);
// 자기 자신 + 변형
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
default Comparator<T> thenComparing(Comparator<? super T> other) {
return (c1, c2) -> {
int res = compare(c1, c2);
return (res != 0) ? res : other.compare(c1, c2);
};
}
default <U extends Comparable<? super U>> Comparator<T> thenComparing(
Function<? super T, ? extends U> keyExtractor) {
return thenComparing(comparing(keyExtractor));
}
// 정적 팩토리
static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
return (c1, c2) -> c1.compareTo(c2);
}
static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor) {
return (c1, c2) ->
keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
}
public final class Collectors {
public static <T> Collector<T, ?, List<T>> toList() { ... }
public static <T> Collector<T, ?, Set<T>> toSet() { ... }
public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(
Function<? super T, ? extends K> classifier) { ... }
public static <T, K, A, D> Collector<T, ?, Map<K, D>> groupingBy(
Function<? super T, ? extends K> classifier,
Collector<? super T, A, D> downstream) { ... }
public static <T, K, U> Collector<T, ?, Map<K, U>> toMap(
Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) { ... }
public static <T> Collector<T, ?, Long> counting() { ... }
public static <T> Collector<T, ?, Integer> summingInt(
ToIntFunction<? super T> mapper) { ... }
}
자바 표준의 제네릭 패턴:
1. 클래스 T + 메서드 U (Optional, Stream)
<U> Optional<U> map(Function<? super T, ? extends U>)
<R> Stream<R> map(Function<? super T, ? extends R>)
2. PECS 활용 (메서드)
<T> void copy(List<? super T>, List<? extends T>)
<T> void sort(List<T>, Comparator<? super T>)
3. 재귀적 bound (Comparable)
<T extends Comparable<? super T>>
4. 정적 팩토리
static <T> Optional<T> of(T value)
static <T> Stream<T> of(T... values)
static <T> List<T> of(T... values)
5. 함수 합성
default <V> Function<T, V> andThen(Function<? super R, ? extends V>)
자바 표준 라이브러리의 제네릭 패턴 5가지는?
답:
1. 클래스 T + 메서드 U: Optional, Stream 의 map/flatMap
2. PECS: Collections.copy, sort
3. 재귀적 bound: Comparable<? super T>
4. 정적 팩토리: Optional.of, Stream.of, List.of
5. 함수 합성: Function.compose, andThen
→ 모두 타입 안전성 + 유연성 의 균형.
| Q | 핵심 답변 |
|---|---|
| 제네릭 클래스 vs 메서드? | 인스턴스 단위 vs 호출 단위 |
| 제네릭 메서드 정의 위치? | 반환 타입 앞 <T> |
| 타입 추론 메커니즘? | 컨텍스트 분석 + 제약 수집 + LUB |
| 타깃 타입? | 표현식이 기대되는 타입 |
| 제네릭 생성자? | 클래스 무관 메서드 매개변수 |
| 정적 메서드에 클래스 T? | 불가, 자신의 T 필요 |
| var 와 다이아몬드? | 우변 vs 좌변에서 타입 |
| 익명 vs 람다? | SAM 만 람다, 일반은 익명 |
| Function.compose vs andThen? | V→T→R vs T→R→V |
| Collections.copy 시그니처? | <T> + PECS |
<T extends Comparable<T>> vs <T extends Comparable<? super T>> 차이는?답:
예:
class Parent implements Comparable<Parent> { ... }
class Child extends Parent { } // Comparable<Parent> 상속
// 첫 번째 — Child 는 Comparable<Child> 가 아니라 거부
<T extends Comparable<T>> void sort(List<T> list) { ... }
sort(childList); // ❌
// 두 번째 — Child 는 Comparable<? super Child> (Parent) 라 OK
<T extends Comparable<? super T>> void sort(List<T> list) { ... }
sort(childList); // ✓
→ PECS 의 정밀 활용 (Unit 5.5).
답:
var fn = (s) -> s.length() — fn 의 타입은?답:
1. 인자 없는 메서드: Utils.<String>emptyList()
2. 모호한 추론: process(Utils.<String>empty())
3. null 인자: Utils.<String>method(null)
new ArrayList<>(Collections.emptyList()) 동작?답:
Collections.emptyList() 의 추론 필요new ArrayList<>() 의 다이아몬드와 매칭// ❌ 모호
new ArrayList<>(Collections.emptyList()).add("hello");
// ✓ 명시
new ArrayList<String>(Collections.emptyList()).add("hello");
// 또는
List<String> list = new ArrayList<>(Collections.emptyList());
list.add("hello");
답:
해결:
public class Box<T> {
// ❌ 클래스 T 사용
public static T staticMethod(T input) { return input; }
// ✓ 자기 T 정의
public static <U> U staticMethod(U input) { return input; }
}
1. 제네릭 클래스 vs 메서드
2. 타입 추론
3. 자바 표준의 정밀 패턴
이번 Unit에서 제네릭의 정밀을 봤다면, 다음은 PECS 원칙의 완성.
🚀 Phase 5 — 제네릭과 와일드카드
✅ Unit 5.1 제네릭의 등장과 raw type
✅ Unit 5.2 타입 매개변수와 타입 인자
✅ Unit 5.3 와일드카드 ? extends, ? super
✅ Unit 5.4 제네릭 메서드 + 제네릭 클래스 ← 여기
⏭ Unit 5.5 PECS 원칙 (★ 마스터 깊이) — Phase 5 완주
✅ 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 — 제네릭과 와일드카드 (4/5 진행)
총: 21/43 Unit 작성 (약 49%)