Day 17: 제네릭스
제네릭스(Generics)는 클래스나 메서드를 정의할 때 타입을 매개변수로 받는 기능입니다. 택배 상자에 비유하면, “어떤 물건이든 담을 수 있는 상자”이지만 한 번 “책 상자”로 지정하면 책만 넣을 수 있도록 제한하는 것과 같습니다. 컴파일 시점에 타입을 검사하여 런타임 에러를 방지합니다.
제네릭 클래스
타입 매개변수 <T>를 사용하여 다양한 타입에 대응하는 클래스를 만듭니다.
// 제네릭 클래스: T는 타입 매개변수
class Box<T> {
private T item;
public void put(T item) {
this.item = item;
System.out.println(item.getClass().getSimpleName() + " 저장됨: " + item);
}
public T get() {
return item;
}
public boolean isEmpty() {
return item == null;
}
}
// 여러 타입 매개변수
class Pair<K, V> {
private final K key;
private final V value;
Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
@Override
public String toString() {
return key + " = " + value;
}
}
public class GenericClassExample {
public static void main(String[] args) {
// String 타입 Box
Box<String> stringBox = new Box<>();
stringBox.put("안녕하세요");
String message = stringBox.get(); // 캐스팅 불필요!
System.out.println("꺼낸 값: " + message);
// Integer 타입 Box
Box<Integer> intBox = new Box<>();
intBox.put(42);
int number = intBox.get(); // 자동 언박싱
System.out.println("꺼낸 값: " + number);
// Pair 사용
Pair<String, Integer> nameAge = new Pair<>("홍길동", 25);
Pair<String, String> config = new Pair<>("host", "localhost");
System.out.println(nameAge);
System.out.println(config);
// 다이아몬드 연산자 (<>): Java 7+
// 오른쪽의 타입을 생략해도 추론됨
Box<Double> doubleBox = new Box<>();
doubleBox.put(3.14);
}
}
제네릭 메서드
메서드 레벨에서 타입 매개변수를 선언하여 다양한 타입을 처리합니다.
import java.util.Arrays;
import java.util.List;
public class GenericMethodExample {
// 제네릭 메서드: 반환 타입 앞에 <T> 선언
static <T> void printArray(T[] array) {
System.out.print("[");
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]);
if (i < array.length - 1) System.out.print(", ");
}
System.out.println("]");
}
// 두 값 중 더 큰 값 반환
static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
// 배열에서 특정 값 찾기
static <T> int indexOf(T[] array, T target) {
for (int i = 0; i < array.length; i++) {
if (array[i].equals(target)) {
return i;
}
}
return -1;
}
// 배열을 List로 변환
static <T> List<T> arrayToList(T[] array) {
return Arrays.asList(array);
}
public static void main(String[] args) {
Integer[] intArr = {1, 2, 3, 4, 5};
String[] strArr = {"Java", "Python", "Go"};
Double[] dblArr = {1.1, 2.2, 3.3};
printArray(intArr);
printArray(strArr);
printArray(dblArr);
System.out.println("큰 값: " + max(10, 20));
System.out.println("큰 값: " + max("apple", "banana"));
System.out.println("Python 위치: " + indexOf(strArr, "Python"));
List<String> list = arrayToList(strArr);
System.out.println("리스트: " + list);
}
}
제한된 타입 매개변수 (Bounded Type)
타입 매개변수에 상한/하한 제한을 두어 특정 타입만 허용합니다.
// 상한 제한: Number 또는 그 자식만 허용
class MathBox<T extends Number> {
private T value;
MathBox(T value) {
this.value = value;
}
double doubleValue() {
return value.doubleValue();
}
boolean isPositive() {
return value.doubleValue() > 0;
}
// 두 MathBox의 합계
<U extends Number> double add(MathBox<U> other) {
return this.doubleValue() + other.doubleValue();
}
}
// 다중 제한: 여러 인터페이스를 구현해야 함
class SortableBox<T extends Comparable<T> & java.io.Serializable> {
private T item;
SortableBox(T item) {
this.item = item;
}
boolean isGreaterThan(SortableBox<T> other) {
return this.item.compareTo(other.item) > 0;
}
T getItem() {
return item;
}
}
public class BoundedTypeExample {
public static void main(String[] args) {
MathBox<Integer> intMath = new MathBox<>(42);
MathBox<Double> dblMath = new MathBox<>(3.14);
System.out.println("double 변환: " + intMath.doubleValue());
System.out.println("양수? " + intMath.isPositive());
System.out.println("합계: " + intMath.add(dblMath));
// MathBox<String> strMath = new MathBox<>("hello"); // 컴파일 에러!
SortableBox<String> box1 = new SortableBox<>("apple");
SortableBox<String> box2 = new SortableBox<>("banana");
System.out.println("box1 > box2? " + box1.isGreaterThan(box2));
}
}
와일드카드
제네릭 타입을 유연하게 사용하기 위한 ? 기호입니다.
import java.util.ArrayList;
import java.util.List;
public class WildcardExample {
// 비한정 와일드카드: 모든 타입 허용 (읽기 전용)
static void printList(List<?> list) {
for (Object item : list) {
System.out.print(item + " ");
}
System.out.println();
}
// 상한 와일드카드: Number 이하만 허용 (읽기 용)
static double sumOfList(List<? extends Number> list) {
double sum = 0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}
// 하한 와일드카드: Integer 이상만 허용 (쓰기 용)
static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
// PECS 원칙: Producer Extends, Consumer Super
// 데이터를 꺼내(produce) 쓸 때 -> extends
// 데이터를 넣을(consume) 때 -> super
static <T> void copy(List<? extends T> source, List<? super T> dest) {
for (T item : source) {
dest.add(item);
}
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3, 4, 5);
List<Double> dblList = List.of(1.1, 2.2, 3.3);
List<String> strList = List.of("A", "B", "C");
printList(intList);
printList(strList);
System.out.println("정수 합: " + sumOfList(intList));
System.out.println("실수 합: " + sumOfList(dblList));
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println("추가된 숫자: " + numberList);
// 복사
List<Number> dest = new ArrayList<>();
copy(intList, dest);
System.out.println("복사 결과: " + dest);
}
}
오늘의 연습문제
-
제네릭 스택:
GenericStack<T>클래스를 구현하세요.push(T),pop(),peek(),isEmpty(),size()메서드를 제공하세요. 내부적으로ArrayList를 사용하세요. -
제네릭 유틸리티: 다음 제네릭 메서드를 구현하세요.
swap(T[] arr, int i, int j)- 배열의 두 요소 교환,reverse(List<T> list)- 리스트 뒤집기,filter(List<T> list, Predicate<T> pred)- 조건에 맞는 요소만 반환. -
Comparable 활용:
<T extends Comparable<T>>제한을 사용하여 리스트에서 최대값, 최소값, 정렬된 리스트를 반환하는Statistics<T>클래스를 만드세요.