Java 26일 코스 - Day 17: 제네릭스

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);
    }
}

오늘의 연습문제

  1. 제네릭 스택: GenericStack<T> 클래스를 구현하세요. push(T), pop(), peek(), isEmpty(), size() 메서드를 제공하세요. 내부적으로 ArrayList를 사용하세요.

  2. 제네릭 유틸리티: 다음 제네릭 메서드를 구현하세요. swap(T[] arr, int i, int j) - 배열의 두 요소 교환, reverse(List<T> list) - 리스트 뒤집기, filter(List<T> list, Predicate<T> pred) - 조건에 맞는 요소만 반환.

  3. Comparable 활용: <T extends Comparable<T>> 제한을 사용하여 리스트에서 최대값, 최소값, 정렬된 리스트를 반환하는 Statistics<T> 클래스를 만드세요.

이 글이 도움이 되었나요?