본문 바로가기

자바

자바 제네릭

제네릭이란?

클래스나 인터페이스, 메소드를 선언할 때에 파라미터나 반환 타입을 고정해두지 않고 해당 클래스, 인터페이스를 사용할 때에 타입을 지정할 수 있는 기능을 말한다.

즉, 하나의 클래스, 인터페이스 등으로 여러 타입을 지정하여 사용할 수 있도록 한다. 제네릭을 이용한 클래스, 인터페이스를 제네릭 타입이라고 칭한다.

제네릭 사용 이유

제네릭 이전에는 개발자가 컬렉션에서 객체를 꺼낼 때 타입 변환을 수동으로 해주었는 데 제네릭을 사용하면 그 수고를 하지 않아도 된다. 컴파일러가 타입을 이미 알고 있기 때문에 실수로 다른 타입으로 형변환하여 오류가 생기는 일을 미리 막아주어 객체 타입의 안정성을 높여준다. 또한 타입을 국한하기 때문에 매번 새롭게 타입 변환하지 않아도 돼서 성능상의 이점이 있다.

제네릭 사용법

제네릭 클래스

record Butler<T> (T pet) {}

record Cat(String name) {}

record Dog(String name) {}

public static void main(String[] args) {
    Butler<Cat> catButler = new Butler<>(new Cat("kitty"));
    Butler<Dog> dogButler = new Butler<>(new Dog("happy"));
    Butler<Dog> invalidButler = new Butler<>("happy"); // no instance(s) of type variable(s) exist so that String conforms to Dog inference variable T has incompatible bounds: equality constraints: Dog lower bounds: String

    System.out.println(catButler.pet().name());
    System.out.println(dogButler.pet().name());

}
kitty
happy

위와 같이 <> 사이에 타입 매개변수(T) 를 주면서 클래스를 선언한다. 그리고 나서 Bulter 타입의 객체를 만들 때에 T 에 대한 타입을 명시해주면, 컴파일러는 T 를 명시해준 클래스 타입(예시에서는 Dog.class 이나 Cat.class) 로 인식하게 된다.

제네릭 타입에 Dog 클래스라고 선언하고 String 타입을 주면 위의 예시와 같이 컴파일 에러가 나는 것을 볼 수 있다.

제네릭 인터페이스

public interface Pet<T> {
    T getType();
}

public class Mammal {

    @Getter
    private final String name = "포유류";
}

record Cat(String name) implements Pet<Mammal> {
    @Override
    public Mammal getType() {
        return new Mammal();
    }
}

record Dog(String name) implements Pet<Mammal> {
    @Override
    public Mammal getType() {
        return new Mammal();
    }
}

public static void main(String[] args) {
    Butler<Cat> catButler = new Butler<>(new Cat("kitty"));
    Butler<Dog> dogButler = new Butler<>(new Dog("happy"));

    System.out.println(catButler.pet().name());
    System.out.println(catButler.pet().getType().getName());
    System.out.println(dogButler.pet().name());
    System.out.println(dogButler.pet().getType().getName());
}
kitty
포유류
happy
포유류

위의 예제와 같이 인터페이스도 클래스처럼 제네릭을 사용하면 된다.

Pet 이라는 인터페이스의 제네릭 매개변수 T 가 Mammal 클래스 타입으로 대응된다고 보면 된다.

제네릭 주요 개념

바운디드 타입

타입을 매개변수로 받을 때 특정 타입의 서브 타입만 받을 수 있도록 제한할 수 있다.

record Butler<T extends Pet<Mammal>> (T pet) {}

Butler 클래스에 생성자로 주입받는 pet 변수는 Pet<Mammal> 인터페이스의 하위 클래스만이 들어올 수 있도록 할 수 있다.

Pet 클래스를 구현하지 않은 클래스는 물론이고, Pet<Reptile> 을 구현하지 않은 클래스 타입도 들어오면 컴파일 에러가 발생한다.

public class Reptile {

    @Getter
    private final String name = "파충류";
}

record Lizard(String name) implements Pet<Reptile> {
    @Override
    public Reptile getType() {
        return new Reptile();
    }
}

Butler<Lizard> lizardButler = new Butler<>(new Lizard("blizrd")); // Type parameter 'Lizard' is not within its bound; should implement 'Pet<Mammal>'

와일드 카드

와일드 카드를 사용하면 타입을 매개변수로 받을 때 임의의 타입(unknwon type)이 올 수 있다. ? 키워드를 사용한다.

record Butler<T> (T pet) {

    void greet(Butler<?> other) {
        Object otherPet = other.pet;

        System.out.println(String.format("other pet: %s", otherPet));
    }
}

public static void main(String[] args) {
    Cat kitty = new Cat("kitty");
    Butler<Cat> catButler = new Butler<>(kitty);
    Dog happy = new Dog("happy");
    Butler<Dog> dogButler = new Butler<>(happy);

		catButler.greet(dogButler);
    dogButler.greet(catButler);
}
other pet: Dog[name=happy]
other pet: Cat[name=kitty]

자바에서 모든 객체는 Object 타입을 상속받는다. 그래서 Object 타입으로 선언되어 사용된다.

Bounded 와일드 카드

와일드카드로 인자를 선언하면 자바에서 Object 타입으로만 인식된다. 자바의 와일드 카드는 unknown type 이기 때문이다.

특정 클래스 타입으로 받고 싶을 수 있는 데 제한이 있는 것이다. 이러한 제한을 해결하고자 한정적 와일드 카드(bounded wildcard) 기능을 제공한다.

Upper Bounded

상한 경계 와일드 카드는 extends 키워드를 사용함으로써 타입의 상한 경계를 설정할 수 있다.

void greet(Butler<? extends Pet<Mammal>> other) {
    Pet<Mammal> otherPet1 = other.pet; // compile success!
    Object otherPet2 = other.pet; // compile success!
    Cat otherCat3 = other.pet; // compile error!
    Dog otherDog4 = other.pet; // compile error!
}

위의 코드를 해석하면 Pet<Mammal> 타입의 unknown type 이 other 변수로 들어온다. unknwon type 이기 때문에 Pet<Mammal> 타입의 하위 타입인 Dog 이나 Cat 으로 캐스팅이 되지 않아서 컴파일 에러가 난다.

그래서 other 는 Pet<Mammal> 타입의 이상의 타입으로만 선언할 수 있다.

Lower Bounded

하한 경계 와일드 카드는 super 키워드를 사용함으로써 타입의 하한 경계를 설정할 수 있다.

void greet(Butler<? super Pet<Mammal>> other) {
    Pet<Mammal> otherPet1 = other.pet; // compile error!
    Object otherPet2 = other.pet;
    Cat otherCat3 = other.pet; // compile error!
    Dog otherDog4 = other.pet; // compile error!
}

위의 코드를 해석하면 Pet<Mammal> 의 부모 타입의 unknown type 이 other 변수로 들어온다. 마찬가지로 부모 타입을 특정할 수 없어서 Pet<Mammal> 이나 Dog, Cat 으로 캐스팅되지 않아 컴파일 에러가 난다.

자바의 모든 클래스는 Object 클래스를 상속받기 때문에 Object 로는 선언할 수 있다.

제네릭 메소드 만들기

제네릭 메소드를 선언하는 방법은 접근제어자 다음에 <> 기호와 함께 타입 파라미터(e.g. T)를 기입한다. 그리고 리턴타입 및 메소드 파라미터타입(T) 를 사용하여 선언한다.

public static <T, V> void print(Pet<T> pet1, Pet<V> pet2) {
    System.out.println(String.format("first pet: type: %s name: %s", pet1.getType(), pet1.getName()));
    System.out.println(String.format("second pet: type: %s name: %s", pet2.getType(), pet2.getName()));
}

public static <T> T getType(Pet<T> pet) {
    return pet.getType();
}

위의 예제처럼 타입 파라미터를 2개를 받을 수도 있고, 반환, 인자에서 사용할 수 있다.

제네릭 타입 Erasure

제네릭은 자바 1.5 부터 제공된다. 그래서 이전 코드와의 호환을 위해서 컴파일된 후에는 타입 파라미터 섹션(<T>)을 소거한다. 타입 파라미터(T) 는 Object 로 변환된다.

다음 제네릭 타입의 인터페이스 대한 바이트 코드를 한 번 확인해보자.

public interface Pet<T> {

    T getType();

    String getName();
}
// class version 61.0 (61)
// access flags 0x601
// signature <T:Ljava/lang/Object;>Ljava/lang/Object;
// declaration: me/skrew/javastudy/generic/Pet<T>
public abstract interface me/skrew/javastudy/generic/Pet {

  // compiled from: Pet.java

  // access flags 0x401
  // signature ()TT;
  // declaration: T getType()
  public abstract getType()Ljava/lang/Object;

  // access flags 0x401
  public abstract getName()Ljava/lang/String;
}

컴파일된 후의 바이트 코드에서 Pet<T> 에서 <T> 가 사라지고 getType 함수 리턴타입이 Object 클래스를 반환하는 것을 볼 수 있다.

ref

- https://docs.oracle.com/javase/tutorial/java/generics/types.html

- https://mangkyu.tistory.com/241

- https://velog.io/@yglee8048/Generic-Type-Erasure

'자바' 카테고리의 다른 글

java.util.ConcurrentModificationException 원인과 해결  (1) 2025.01.28
자바 람다식  (1) 2024.09.18
자바 I/O  (0) 2024.07.15
자바 애노테이션 프로세서  (1) 2024.04.12
자바 애노테이션  (0) 2024.04.03