[JAVA] 제네릭
선언시에는 구체적인 타입이 아닌 포괄적인 타입으로 명시하고 사용시에 구체적인 타입을 지정하는 개념이다.
클래스나 메서드를 선언할 때, 타입(자료형)을 ‘매개변수’로 받는 문법 (데이터 타입을 미리 지정하지 않음)
모든 클래스의 최상위 부모인 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); // 읽는 건 가능
}