제네릭스

2025. 2. 26. 17:32Java/Java 문법

1. 제네릭스(Generics)

1-1. 제네릭스란? 

  • 자바에서 클래스나 메서드가 다룰 데이터 타입을 컴파일 시점에 지정할 수 있도록 하는 기능
  • 쉽게 말해서, 타입을 변수처럼 사용할 수 있게 해주는 것
  • 제네릭스를 활용하는 제네릭 클래스는 제네릭 타입(T, E, K, V)을 활용하여 하나의 클래스로 해당 제네릭 타입에 변화를 줘서 제네릭 클래스의 인스턴스를 다양한 타입을 지닌 인스턴스로 활용할 수 있다.
  • 제네릭 클래스와 제네릭 메서드를 사용하면 여러 타입을 유연하게 처리 가능
  • 제네릭 선언은 다이아몬드 연산자 <>를 이용하여 작성하고 <>안에는 참조형을 선언해줘야 한다

1-2. 제네릭 클래스 

// 제네릭 클래스 정의
class Box<T> {  // T는 타입 변수 (T, E, K, V 등 아무 이름이나 가능)
    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

public class Main {
    public static void main(String[] args) {
        //제네릭 클래스 Box를 인스턴스화할때 타입 T를 String으로 생성 
        Box<String> stringBox = new Box<>(); 
        stringBox.set("Hello");
        System.out.println(stringBox.get()); // Hello

        //제네릭 클래스 Box를 인스턴스화할때 타입 T를 Integer으로 생성 
        Box<Integer> intBox = new Box<>(); //
        intBox.set(123);
        System.out.println(intBox.get()); // 123
    }
}

1-3. 제네릭 메서드 

class Util {
	//제네릭 메서드 선언
    public static <T> void print(T data) {
        System.out.println(data);
    }
}

public class Main {
    public static void main(String[] args) {
    	//한 가지 메서드로 여러 타입을 처리할 수 있어서 코드가 깔끔하다. 
        Util.print("Hello"); // 매개변수를 String 타입으로 사용
        Util.print(123); // 매개변수를 Integer 타입으로 사용
        Util.print(3.14); // 매개변수를 Double 타입으로 사용
    }
}

 

2. 제네릭스를 사용하는 이유

2-1. 다양한 타입을 사용할 수 있음 → 재사용성 UP

  • List<T> 같은 제네릭 클래스를 만들면, List<String>, List<Integer> 등 다양한 타입을 지정해서 사용 가능
  • 같은 코드로 여러 타입을 다룰 수 있으니까 코드 재사용성이 증가한다. 
//List를 제네릭 인터페이스로 정의 
public interface List<E> extends Collection<E> {
    boolean add(E e);
    E get(int index);
    // ...
}

//같은 List클래스를 다양한 타입으로 활용 가능
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

 

2-2. 타입 안정성 확보 + 형 변환 불필요

  • 타입 안정성 확보: 제네릭을 사용하면 컴파일 타임에 타입 체크가 돼서 잘못된 타입을 넣는 실수를 방지할 수 있음 
  • 형변환 불필요: 명확한 타입을 지정해서 사용하기 때문에 데이터를 꺼낼 때도 형 변환이 필요 없어서 코드가 깔끔해짐.
// 1. 제네릭 미사용 (Object 사용)
List list = new ArrayList();

list.add("Hello");
list.add(123);  // 다른 타입도 들어감 -> 문제 발생 가능

String str = (String) list.get(0);  // 데이터 꺼낼 때 마다 형 변환 필요


// 2. 제네릭 사용
//String타입만 저장 가능한 리스트
List<String> list = new ArrayList<>(); 

list.add("Hello");
//list.add(123);  // 다른 타입이 들어갈 경우 컴파일 에러! -> 안정적인 코드 작성 가능

String str = list.get(0);  // 명확한 타입이 지정되어 있기 때문에 형 변환 필요 없음

 

3. 와일드 카드 

  • 제네릭 클래스의 인스턴스를 유연하게 활용하기 위한 문법
  • 메소드의 매개변수로 받을 시 타입을 원하는 만큼으로 제한하는 것
  • 불특정한 제네릭 클래스 타입을 조금 더 활용할 수 있다.
  • 종류
    • <?>: 모든 타입을 허용하는 와일드 카드
    • <? extends T>: T 타입 또는 T의 하위 타입을 허용하는 와일드 카드
    • <? super T>: T 타입 또는 T의 상위 타입을 허용하는 와일드 카드
/* 토끼농장에 있는 토끼가 어떤 토끼이던 상관 없다. */
public void anyType(RabbitFarm<?> farm) {
	farm.getAnimal().cry();
}

/* 토끼농장의 토끼는 Bunny이거나 그 후손 타입으로 만들어진 토끼농장만 매개변수로 사용 가능 */
public void extendsType(RabbitFarm<? extends Bunny> farm) {
	farm.getAnimal().cry();
}

/* 토끼농장의 토끼는 Bunny이거나 그 부모 타입으로 만들어진 토끼농장만 매개변수로 사용 가능 */
public void superType(RabbitFarm<? super Bunny> farm) {
	farm.getAnimal().cry();
}