선언시에는 구체적인 타입이 아닌 포괄적인 타입으로 명시하고 사용시에 구체적인 타입을 지정하는 개념이다.
클래스나 메서드를 선언할 때, 타입(자료형)을 ‘매개변수’로 받는 문법 (데이터 타입을 미리 지정하지 않음)
모든 클래스의 최상위 부모인 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); // 읽는 건 가능
}
'Java' 카테고리의 다른 글
| [JAVA] 스트림_내부반복자 (0) | 2025.10.13 |
|---|---|
| [JAVA] Daemon Thread와 Thread pool (0) | 2025.10.07 |
| [JAVA] 리소스 자동닫기와 예외 떠넘기기 (0) | 2025.10.06 |
| [JAVA] Call By Value & Call By Reference (0) | 2025.09.15 |
| [JAVA] JVM 역할 및 내부구조 (1) | 2025.05.08 |