Java

[JAVA] 제네릭

개바리여리 2025. 10. 6. 23:32

선언시에는 구체적인 타입이 아닌 포괄적인 타입으로 명시하고 사용시에 구체적인 타입을 지정하는 개념이다.

클래스나 메서드를 선언할 때, 타입(자료형)을 ‘매개변수’로 받는 문법 (데이터 타입을 미리 지정하지 않음)

 

모든 클래스의 최상위 부모인 Object 로 선언하게 되면 저장까지만 괜찮겠지만

꺼내서 읽을때는 문제가 발생할 수 있다.

만약 타입을 모른다면 읽어올때 캐스팅이 불가능하여 여러 문제가 생길 수 있음.

 

public class Box <T> {
	public T content;
}

Box<String> box = new Box<String>();
box.content = "안녕하세요";
String content = box.content // 강제 타입 변환이 필요 없다.

 

근데 T자리에는 기본타입은 올 수 없다. 참조타입만 올 수 있다.

// 제네릭 클래스
class Product<K, M> {
    private K kind;
    private M model;

    public K getKind() { return kind; }
    public M getModel() { return model; }
    public void setKind(K kind) { this.kind = kind; }
    public void setModel(M model) { this.model = model; }
}

// 예시용 클래스들
class Car {
    private String name;
    public Car(String name) { this.name = name; }
    @Override
    public String toString() { return "Car: " + name; }
}

class Tv {
    private String brand;
    public Tv(String brand) { this.brand = brand; }
    @Override
    public String toString() { return "TV: " + brand; }
}

// 실행 테스트
public class ProductExample {
    public static void main(String[] args) {

        // 1 자동차 제품
        Product<Car, String> carProduct = new Product<>();
        carProduct.setKind(new Car("Hyundai Sonata"));
        carProduct.setModel("2025");
        System.out.println(carProduct.getKind());
        System.out.println("Model: " + carProduct.getModel());

        System.out.println("--------------------------");

        // 2 TV 제품
        Product<Tv, String> tvProduct = new Product<>();
        tvProduct.setKind(new Tv("Samsung OLED"));
        tvProduct.setModel("Q90R");
        System.out.println(tvProduct.getKind());
        System.out.println("Model: " + tvProduct.getModel());
    }
}
// 제네릭 인터페이스 선언
interface Storage<T> {
    void add(T item, int index);
    T get(int index);
}

// 제네릭 클래스에서 인터페이스 구현
class ArrayStorage<T> implements Storage<T> {
    private T[] items;

    @SuppressWarnings("unchecked")
    public ArrayStorage(int size) {
        // 제네릭 배열은 직접 생성 불가 -> Object 배열로 캐스팅
        items = (T[]) new Object[size];
    }

    @Override
    public void add(T item, int index) {
        items[index] = item;
    }

    @Override
    public T get(int index) {
        return items[index];
    }
}

// 테스트용 클래스
class Product {
    private String name;
    public Product(String name) { this.name = name; }
    @Override
    public String toString() { return name; }
}

// 실행 예제
public class GenericInterfaceExample {
    public static void main(String[] args) {
        // 문자열 저장소
        Storage<String> stringStorage = new ArrayStorage<>(3);
        stringStorage.add("Apple", 0);
        stringStorage.add("Banana", 1);
        System.out.println(stringStorage.get(0));
        System.out.println(stringStorage.get(1));

        System.out.println("------------------");

        // 객체 저장소
        Storage<Product> productStorage = new ArrayStorage<>(2);
        productStorage.add(new Product("TV"), 0);
        productStorage.add(new Product("Laptop"), 1);
        System.out.println(productStorage.get(0));
        System.out.println(productStorage.get(1));
    }
}

 

 

제네릭 메서드

똑같이 타입 파라미터를 가지는 메서드이다.

public <T> Box<T> boxing(T t) { ... }

타입 파라미터 T는 매개값이 어떤 타입이냐에 따라 컴파일 과정에서 구체적인 타입이 지정됨.

1. Box<Integer> box1 = boxing(100);

2. Box<String> box2 = boxing("안녕하세요");

 

제한된 타입 파라미터

경우에 따라서 타입 파라미터를 구체적인 타입으로 제한할때 사용된다.

public <T extends Number> boolean compare(T t1, T t2){  //Number나 Number의 자식객체만 가능
        double v1 = t1.doubleValue();
        double v2 = t2.doubleValue();
        return v1 == v2;
    }

 

와일드 카드 타입 파라미터

제네릭 타입을 매개값이나 리턴 타입으로 사용할때 타입 파라미터로 ? 를 사용할 수 있다.

 

? 에서 super Worker라 하게 되면 Worker의 위쪽 상속구조에서만 적용가능하고

extends Student라 하게 되면 Student 아래쪽으로 적용이 가능해진다.

 

리턴타입 메서드명(제네릭타입<? extends Student> 변수) { ... }

class Animal { void sound() { System.out.println("Animal sound"); } }
class Dog extends Animal { void sound() { System.out.println("멍멍"); } }
class Cat extends Animal { void sound() { System.out.println("야옹"); } }

public class WildCardExample2 {
    static void makeSound(List<? extends Animal> list) {
        for (Animal a : list) {
            a.sound();
        }
    }

    public static void main(String[] args) {
        List<Dog> dogs = List.of(new Dog(), new Dog());
        List<Cat> cats = List.of(new Cat(), new Cat());

        makeSound(dogs); // 가능 (Dog는 Animal의 하위타입)
        makeSound(cats); // 가능
        // makeSound(List.of("String")); // 불가능
    }
}

리턴타입 메서드명(제네릭타입<? super Worker > 변수) { ... }

public class WildCardExample3 {
    static void addNumber(List<? super Integer> list) {
        list.add(10);
        list.add(20);
        // list.add(3.14); // Double은 Integer 상위가 아님
    }

    public static void main(String[] args) {
        List<Object> objList = new ArrayList<>();
        List<Number> numList = new ArrayList<>();

        addNumber(objList); // 가능 (Object는 Integer의 상위)
        addNumber(numList); // 가능 (Number는 Integer의 상위)
    }
}

 

만약 List<?> 이렇게만 된다면 읽을수는 있지만 타입을 모르니 쓸수는 없다.

void addSomething(List<?> list) {
    // list.add("Hello");  // 컴파일 에러 (무슨 타입 리스트인지 모르니까)
    Object obj = list.get(0); // 읽는 건 가능
}