같은 실수가 제네릭 유무에 따라 런타임 에러가 되거나 컴파일 에러가 되는 것을 보여줍니다.
import java.util.*;
public class RawVsGeneric {
public static void main(String[] args) {
// raw type: 컴파일 통과, 런타임 폭발
List raw = new ArrayList();
raw.add("ORD-001");
raw.add(42);
try {
for (Object o : raw) {
String id = (String) o; // 두 번째 요소에서 폭발
System.out.println("id = " + id);
}
} catch (ClassCastException e) {
System.out.println("런타임 에러: " + e.getMessage());
}
// 제네릭: 잘못된 add 는 아예 컴파일되지 않음
List<String> ids = new ArrayList<>();
ids.add("ORD-001");
// ids.add(42); // 컴파일 에러: int cannot be converted to String
for (String id : ids) System.out.println("id = " + id); // 캐스팅 불필요
}
}
// 출력:
// id = ORD-001
// 런타임 에러: class java.lang.Integer cannot be cast to class java.lang.String ...
// id = ORD-001List<String> 과 List<Integer> 는 같은 클래스리플렉션으로 런타임 타입을 찍어 보면 제네릭 정보가 사라졌음을 확인할 수 있습니다.
import java.util.*;
public class ErasureProof {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
System.out.println(strings.getClass() == ints.getClass()); // true
System.out.println(strings.getClass().getName());
// raw type 을 통하면 String 리스트에 Integer 를 넣을 수도 있다 (경고만 발생)
@SuppressWarnings({"rawtypes", "unchecked"})
List raw = strings;
raw.add(123);
System.out.println("size = " + strings.size());
try {
String s = strings.get(0); // 컴파일러가 삽입한 (String) 캐스팅에서 폭발
System.out.println(s);
} catch (ClassCastException e) {
System.out.println("소거 때문에 여기서야 터진다: " + e.getClass().getSimpleName());
}
}
}
// 출력:
// true
// java.util.ArrayList
// size = 1
// 소거 때문에 여기서야 터진다: ClassCastException? extends Number 로 어떤 숫자 리스트든 받고, ? super 로 상위 타입 컬렉션에 결과를 쓰는 패턴입니다.
import java.util.*;
public class PecsDemo {
// producer → extends: Integer, Double, Long 리스트 모두 받을 수 있다
static double sum(List<? extends Number> nums) {
double total = 0;
for (Number n : nums) total += n.doubleValue();
return total;
}
// consumer → super: List<Integer>, List<Number>, List<Object> 모두 채울 수 있다
static void fillSequence(List<? super Integer> dest, int count) {
for (int i = 1; i <= count; i++) dest.add(i);
}
// 최댓값: 재귀 타입 바운드
static <T extends Comparable<? super T>> T max(Collection<? extends T> items) {
Iterator<? extends T> it = items.iterator();
T best = it.next();
while (it.hasNext()) { T t = it.next(); if (t.compareTo(best) > 0) best = t; }
return best;
}
public static void main(String[] args) {
System.out.println(sum(List.of(1, 2, 3))); // Integer
System.out.println(sum(List.of(1.5, 2.5))); // Double
List<Number> numbers = new ArrayList<>();
fillSequence(numbers, 3);
System.out.println(numbers);
List<Object> objects = new ArrayList<>(List.of("x", "y", "z"));
Collections.copy(objects, List.of(10, 20)); // dest: ? super Integer, src: ? extends Integer
System.out.println(objects);
System.out.println(max(List.of("pear", "apple", "zoo")));
}
}
// 출력:
// 6.0
// 4.0
// [1, 2, 3]
// [10, 20, z]
// zooApiResponse<T> 와 Result<T, E>컨트롤러 응답 포맷을 통일하고, 예외 대신 값으로 실패를 표현하는 두 가지 실무 타입입니다. sealed + record + 패턴 매칭으로 JDK 21 스타일로 작성합니다.
import java.util.function.Function;
public class ApiTypes {
// 응답 봉투: 성공/실패 모두 같은 포맷
record ApiResponse<T>(boolean success, T data, String error) {
static <T> ApiResponse<T> ok(T data) { return new ApiResponse<>(true, data, null); }
static <T> ApiResponse<T> fail(String error) { return new ApiResponse<>(false, null, error); }
}
// Result: 성공(Ok) 또는 실패(Err) 둘 중 하나. sealed 라서 switch 가 완전성 검사
sealed interface Result<T, E> permits Ok, Err {
default <R> Result<R, E> map(Function<? super T, ? extends R> f) {
return switch (this) {
case Ok<T, E> ok -> new Ok<>(f.apply(ok.value()));
case Err<T, E> err -> new Err<>(err.error());
};
}
}
record Ok<T, E>(T value) implements Result<T, E> {}
record Err<T, E>(E error) implements Result<T, E> {}
record Order(String id, long amount) {}
static Result<Order, String> findOrder(String id) {
if (id.startsWith("ORD-")) return new Ok<>(new Order(id, 12_000));
return new Err<>("잘못된 주문 번호: " + id);
}
public static void main(String[] args) {
for (String id : new String[]{"ORD-1", "XXX"}) {
Result<Long, String> amount = findOrder(id).map(Order::amount);
String msg = switch (amount) {
case Ok<Long, String> ok -> "금액 = " + ok.value();
case Err<Long, String> err -> "실패 = " + err.error();
};
System.out.println(msg);
}
ApiResponse<Order> res = ApiResponse.ok(new Order("ORD-9", 500));
System.out.println(res);
System.out.println(ApiResponse.<Order>fail("not found"));
}
}
// 출력:
// 금액 = 12000
// 실패 = 잘못된 주문 번호: XXX
// ApiResponse[success=true, data=Order[id=ORD-9, amount=500], error=null]
// ApiResponse[success=false, data=null, error=not found]JpaRepository<T, ID> 의 축소판입니다. 엔티티 타입과 ID 타입을 파라미터로 받고, ID 추출 함수를 생성자에서 받아 new T() 제약을 우회합니다.
import java.util.*;
import java.util.function.Function;
public class RepoDemo {
interface Entity<ID> { ID id(); }
static class InMemoryRepository<T extends Entity<ID>, ID> {
private final Map<ID, T> store = new LinkedHashMap<>();
public T save(T entity) { store.put(entity.id(), entity); return entity; }
public Optional<T> findById(ID id) { return Optional.ofNullable(store.get(id)); }
public List<T> findAll() { return new ArrayList<>(store.values()); }
public boolean deleteById(ID id) { return store.remove(id) != null; }
public <R> List<R> mapAll(Function<? super T, ? extends R> f) { // PECS
List<R> out = new ArrayList<>();
for (T t : store.values()) out.add(f.apply(t));
return out;
}
}
record Customer(Long id, String name) implements Entity<Long> {}
record Product(String id, String name, int price) implements Entity<String> {}
public static void main(String[] args) {
var customers = new InMemoryRepository<Customer, Long>();
customers.save(new Customer(1L, "kim"));
customers.save(new Customer(2L, "lee"));
System.out.println(customers.findById(1L).map(Customer::name).orElse("없음"));
System.out.println(customers.findById(9L).map(Customer::name).orElse("없음"));
System.out.println(customers.mapAll(Customer::name));
var products = new InMemoryRepository<Product, String>();
products.save(new Product("P-1", "keyboard", 50_000));
System.out.println(products.findAll());
System.out.println(products.deleteById("P-1") + " " + products.findAll().size());
}
}
// 출력:
// kim
// 없음
// [kim, lee]
// [Product[id=P-1, name=keyboard, price=50000]]
// true 0