제네릭

박태현·2025년 4월 18일

Java

목록 보기
9/17

제네릭이란 ?


클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법

이는 객체별로 다른 타입의 자료가 저장될 수 있도록 함

Ex) ArrayList<String> list = new ArrayLisy<>() ⇒ 꺾쇠 괄호 : 제네릭

즉, 배열의 타입을 지정하듯이 리스트 자료형 같은 컬렉션 클래스나 메서드에서 사용할 내부 데이터 타입을 파라미터 주듯이 외부에서 지정하는 타입을 변수화 하는 기능

⇒ 변수를 선언할 때 타입을 지정 해주듯이, 제네릭은 객체에 타입을 지정 해주는 것

제네릭 타입 매개변수 ( 타입 변수 )


함수나 클래스에서 사용할 타입을 외부에서 유연하게 지정할 수 있도록 해주는 변수

제네릭은 <> 괄호 키워드를 사용하는데 이를 다이아몬드 연산자라고 부름

이 꺾쇠 관호 안에 식별자 기호를 지정함으로써 파라미터화 할 수 있음

⇒ 마치 메서드가 매개변수를 받아 사용하는 것과 비슷하여 제네릭의 타입 매개변수 / 타입 변수라고 부름

List , <T> : 타입 매개변수

List stringList = new ArrayList() , <String> : 매개변수화 된 타입

<T>는 제네릭을 선언하는 부분이고, T는 그 제네릭 타입을 사용하는 타입 매개변수

이러한 타입 매개변수는 제네릭을 이용한 클래스나 메서드를 설계할 때 사용됨

class FruitBox<T> { => 외부에서 타입을 지정받음 !!
	List<T> fruits = new ArrayList<>();
	
	public void add(T fruit) {
		fruits.add(fruit);
	}
}
FruitBox<Integer> intBOx = new FruitBox<>();
FruitBox<Double> doubleBOx = new FruitBox<>();
...

위 코드를 보면 외부에서 지정하는 타입으로 인해 타입이 정해지는 것을 볼 수 있는데

, 이를 구체화( Specialization )라고 함

타입 파라미터 생략

FruitBox<Integer> intBOx = new FruitBox<>()

앞에서 제네릭을 지정해주었기에 new 부분에는 제네릭 타입을 다시 지정할 필요가 없음

타입 파라미터 할당 가능 타입

제네릭에서 할당 받을 수 있는 타입은 Reference 타입뿐이다 !

즉, int, double .. 등의 기본형을 제네릭의 파라미터 타입으로 넘길 수 없다는 것

또한 제네릭 타입 파라미터에 클래스가 타입으로 온다는 것은, 상속을 통해 관계를 맺는 다형성의 원리가 적용이 된다는 것 !

class Fruit { }
class Apple extends Fruit { }
class Banana extends Fruit { }

class FruitBox<T> {
    List<T> fruits = new ArrayList<>();

    public void add(T fruit) {
        fruits.add(fruit);
    }
}

public class Main {
    public static void main(String[] args) {
        FruitBox<Fruit> box = new FruitBox<>();
        
        // 제네릭 타입은 다형성 원리가 그대로 적용 가능
        box.add(new Fruit());
        box.add(new Apple());
        box.add(new Banana());
    }
}

복수 타입 파라미터

제네릭 타입 파라미터는 반드시 한 개만 사용하라는 법이 없으며, 만약 타입 지정이 여러 개가 필요한 경우 여러 개를 지정할 수 있음

<> 안에서 ,로 구분

class Apple {}
class Banana {}

class FruitBox<T, U> {
    List<T> apples = new ArrayList<>();
    List<U> bananas = new ArrayList<>();

    public void add(T apple, U banana) {
        apples.add(apple);
        bananas.add(banana);
    }
}

public class Main {
    public static void main(String[] args) {
    	// 복수 제네릭 타입
        FruitBox<Apple, Banana> box = new FruitBox<>();
        box.add(new Apple(), new Banana());
        box.add(new Apple(), new Banana());
    }
}

중첩 타입 파라미터

제네릭 객체를 제네릭 타입 파라미터로 받는 형식도 표현할 수 있음

public static void main(String[] args) {
    ArrayList<LinkedList<String>> list = new ArrayList<LinkedList<String>>();

    LinkedList<String> node1 = new LinkedList<>();
    node1.add("aa");
    node1.add("bb");

    LinkedList<String> node2 = new LinkedList<>();
    node2.add("11");
    node2.add("22");

    list.add(node1);
    list.add(node2);
    System.out.println(list); // [[aa, bb], [11, 22]]
}

제네릭 사용 이유와 이점


  1. 컴파일 시점에 타입 검사를 통해 예외 방지

    자바의 Object를 사용한다면 ??

    class FruitBox {
        private Object[] fruit;
    
        public FruitBox(Object[] fruit) {
            this.fruit = fruit;
        }
    
        public Object getFruit(int index) {
            return fruit[index];
        }
    }
    
    FruitBox box = new FruitBox(new Apple[]{new Apple(), new Apple()});
    Apple a = (Apple) box.getFruit(0); // 형 변환 해줘야 함, OK
    ~~Banana b = (Banana) box.getFruit(1)~~; // 컴파일 시점에 오류가 발생하지 않고 런타임 때 발생

    Object 배열 안에 Apple 객체가 들어간 상태인데 이를 다시 꺼낼 때는
    컴파일러 입장에서 “안에 뭐가 들어 있을지 몰라서” 꺼낼 때 Object라고 간주하기 때문에 캐스팅을 다시 해줘야 함

    제네릭을 사용한다면 ??

    Class FruitBox<T> {
    	private T[] fruit
    	
    	public FruitBox(T[] fruit) {
    		this.fruit = fruit;
    	}
    	
    	public T getFruit(int idx) {
    		return fruit[idx];
    	}
    }
    
    FruitBox<Apple> box = new FruitBox<>(new Apple[]{new Apple()});
    Apple a = box.getFruit(0);       // 형변환도 필요 없음
    Banana b = box.getFruit(1);      // 컴파일 에러 발생! ( 미리 방지 )
  2. 불필요한 캐스팅을 없애 성능 향상

    Apple[] arr = { new Apple(), new Apple(), new Apple() };
    FruitBox box = new FruitBox(arr);
    
    // 가져온 타입이 Object 타입이기 때문에 일일히 다운캐스팅을 해야함 - 쓸데없는 성능 낭비
    Apple apple1 = (Apple) box.getFruit(0);
    Apple apple2 = (Apple) box.getFruit(1);
    Apple apple3 = (Apple) box.getFruit(2);
    
    ---
    
    // 미리 제네릭 타입 파라미터를 통해 형(type)을 지정해놓았기 때문에 별도의 형변환은 필요없음
    FruitBox<Apple> box = new FruitBox<>(arr);
    
    Apple apple = box.getFruit(0);
    Apple apple = box.getFruit(1);
    Apple apple = box.getFruit(2);

제네릭 사용 주의사항


  1. 제네릭 타입의 객체는 생성이 불가

    class Sample<T> {
        public void someMethod() {
            // Type parameter 'T' cannot be instantiated directly
            T t = new T();
        }
    }

    제네릭 타입을 지정해서 객체를 생성하는것은 불가능 함

    → new 연산자 뒤에 제네릭 타입 파라미터가 오면 안됨

  2. static 멤버에 제네릭 타입이 올 수 없음

    public class Box<T> {
        // static T value;  // "Cannot make a static reference to the non-static type T"
    
        static int count;   // 가능
        T instanceValue;    // 인스턴스 변수는 가능
    }

    static 멤버는 클래스가 로딩될 때 메모리에 올라가는데, 제네릭 타입 파라미터 T는 인스턴스가 생성될 때 결정되기 때문에, static 영역에는 T의 타입이 아직 확정되지 않은 상태

    따라서 타입이 정해지지 않은 채로 메모리에 올라가게 되어, 타입 안정성을 보장할 수 없기 때문에 컴파일 에러가 발생

    즉, T의 타입이 뭔지 결정되기도 전에 static 영역에 들어가게 되니까

  3. 타입 불변성 ( Java ver )

    기본적으로 타입 간의 상속 관계가 있어도, 제네릭 타입끼리는 상속 관계가 성립하지 않음

    즉, Apple이 Fruit의 자식 클래스라고 해도 List은 List의 하위 타입이 아님

    class Fruit {}
    class Apple extends Fruit {}
    class Banana extends Fruit {}
    
    Apple[] appleArr = new Apple[1];
    Fruite[] fruits = appleArr; // 이건 가능
    fruits[0] = new Orange(); // 오류 발생, fruits는 Banana[]를 참조하고 있는데 Orange를 넣으니까
    
    List<Apple> appleList = new ArrayList<>();
    List<Fruit> fruitList = appleList; //  컴파일 오류!
    
    만약 위의 오류가 되는 코드가 허용된다면
    fruitList.add(new Banana) 가 성립되므로 List<Apple> 안에 banana가 들어가버림

    ⭐⭐⭐
    자바에서 배열은 공변성을 허용하지만, 제네릭 타입( List, Set .. )은 불변성을 지님

    자바에서는 이러한 문제 해결을 위해 와일드 카드를 사용 : ? extends , ? super → 읽기만 가능

profile
꾸준하게

0개의 댓글