Java

Generics (Java)

devJK93 2024. 2. 12.

강의를 듣던중 지네릭스 선언 방식, 사용방식이 기억이 나지 않았습니다.

지네릭스에 관한 전반적인 내용을 블로그에 정리해보겠습니다.

// 강의 코드
@Slf4j
@RequiredArgsConstructor
public class TraceTemplate<T> {

  private final LogTrace trace;

  public <T> T execute(String message, TraceCallback<T> callback) {
    TraceStatus status = null;

    try {
      status = trace.begin(message);

      // 로직 호출
      T result = callback.call();

      trace.end(status);
      return result;
    } catch (Exception e) {
      trace.exception(status, e);
      throw e;
    }
  }
}

 

public <T> T execute(String message, TraceCallback<T> callback) { ... } : <T> 옆에 또 T?

왜 T가 중복해서 선언되는 것일까?

 

----------------------------------------------------------------------------------------------------------------------

  1. 지네릭스란 무엇일까요?

 

다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크 (compile-time type check)를 해주는 기능입니다.

 

<지네릭스를 쓰는 이유>

객체의 타입을 컴파일 시에 해주기 때문에

1️⃣ 객체의 타입 안정성을 높여줍니다.

2️⃣ 형변환의 번거로움이 줄어듭니다. → 코드가 간결해짐

 

  2. 지네릭 클래스의 선언

 

지네릭 타입은 1.클래스와 2.메서드에 선언 가능합니다.

 

1. 클래스 지네릭 선언

class Car {
  Object engine;

  public Object getEngine() {
    return engine;
  }

  public void setEngine(Object engine) {
    this.engine = engine;
  }
}

 

이런 클래스를 지네릭을 사용하여 다음과 같이 만들 수 있습니다.

class Car<T> {
  T engine;

  public T getEngine() {
    return engine;
  }

  public void setEngine(T engine) {
    this.engine = engine;
  }
}

 

Car<T>에서 T를 '타입 변수'라고 합니다.

지네릭 클래스가 된 Car클래스의 객체를 생성할 때는 참조변수와 생성자에 타입 T 대신에 사용될 실제 타입을 지정해주어야 합니다.

Car<Engine> car = new Car<Engine>(); // 실제 객체 생성시 타입 T 대신에 실제 타입을 지정
car.setEngine(new Obeject()); // 에러. T를 Engine으로 설정했기 때문에 Engine타입 대신에 다른 타입은 지정 불가능
car.setEngine(new Engine("KIA")); // OK. Engine 타입이니깐
Engine e = car.getEngine(); // OK. (Engine)car.getEngine(); 처럼 형변환 필요없음.

 

T에 Engine을 지정해주었으므로, 지네릭 클래스 Car<T>는 다음과 같은 것입니다.

class Car {
  Engine engine;

  public Engine getEngine() {
    return engine;
  }

  public void setEngine(Engine engine) {
    this.engine = engine;
  }
}

 

  3. 지네릭스 제한사항

 

1️⃣ 모든 객체에 대해 동일하게 동작해야 하는 static 멤버에 타입 변수 T를 사용할 수 없습니다.

(✨ static 멤버는 instance 멤버를 사용할 수 없고 타입 변수 T는 인스턴스 변수로 취급됩니다.)

class Car<T> {
  static T door; // 에러
  static String run(T t) { ... } // 에러
  ...
}

 

2️⃣ 지네릭 타입의 배열을 생성하는 것도 허용되지 않음

(지네릭 타입의 배열을 선언하는 것은 가능하지만, new 연산자를 사용해서 배열을 생성하는 것은 불가능)

class Car<T> {
  T[] doorArr; // OK. T타입의 배열을 위한 참조변수
  
  T[] makeDoorArr() {
  	T[] tmp = new T[4]; // 에러. new를 사용한 지네릭배열 생성 불가
    ...
    return tmp;
  }
}

 

지네릭 타입의 배열을 생성하는 것도 허용되지 않는 이유는 new 연산자 때문인데요.

new 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 하는데,

Car<T>클래스를 컴파일 하는 시점에서는 T가 어떤 타입이 될지 전혀 알 수 없기 때문에(?) 불가능하다는데.. 

저는 이 부분이 잘 이해가 되지 않습니다.

 

(지네릭스를 사용하는 이유가 컴파일 시에 타입체크를 하기 위해서이고 타입체크를 하려면 컴파일시점에 T가 정확히 어떤 타입인지 알고 있어야 가능하지 않나?)

 

어쨌든 같은 이유로 instanceof 연산자도 사용불가능합니다.

 

꼭 지네릭 배열을 생성해야할 필요가 있을 때는 new 연산자 대신 'Reflection API'의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object 배열을 생성해서 복사한 다음 'T[]'로 형변환하는 방법 등을 사용해야 합니다.

 

  4. 제한된 지네릭스 클래스

 

다음과 같이 지정할 수 있는 타입 변수 T를 제한할 수 있습니다.

class ToyBox<T extends Toy> { // Toy의 자손만 타입으로 지정가능
  List<T> list = new ArrayList<T>();
  ...
}

 

Toy의 자손으로 제한하지 않았다면 ToyBox의 타입 매개변수로 Food같은 전혀 상관없는 타입이 할당될 수도 있었습니다.

 

   5. 와일드 카드

 

class Juicer {
  static Juice makeJuice(FruitBox<Fruit> box) {
    String tmp = "";
    for (Fruit fruit : box.getList()) {
      tmp += fruit + " ";
    }
    return new Juice(tmp);
  }
}

 

Juicer 클래스는 지네릭클래스가 아닙니다. 지네릭클래스로 만들어도 static 메서드인 makerJuice 메서드에는 지네릭스를 사용할 수 없습니다.

 

그래서 makeJuice의 매개변수에 다음과 같이 타입을 'static Juice makeJuice(FruitBox<Fruit> box) { ... }' 처럼 직접 지정해줄 수 밖에 없는데..

그러면 다음과 같은 문제점이 발생합니다.

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();

Juicer.makeJuice(fruitBox); // OK.
Juicer.makeJuice(appleBox); // 에러.

 

'new FruitBox<Apple>' 과 'new FruitBox<Fruit>'는 다른 타입이기 때문에 매개변수에 들어갈 수 없다는 문제점이 생기게 됩니다.

 

이 문제를 해결하기 위해 'new FruitBox<Apple>'을 매개변수로 받는 메서드를 만드는 방법을 생각할 수 있습니다. (메서드 오버로딩)

  static Juice makeJuice(FruitBox<Fruit> box) {
    String tmp = "";
    for (Fruit fruit : box.getList()) {
      tmp += fruit + " ";
    }
    return new Juice(tmp);
  }
  
  static Juice makeJuice(FruitBox<Apple> box) {
    String tmp = "";
    for (Fruit fruit : box.getList()) {
      tmp += fruit + " ";
    }
    return new Juice(tmp);
  }

 

하지만 이 방법은 불가능한데요.. 왜냐하면 지네릭 타입이 다른 것만으로는 오버로딩이 성립되지 않기때문이다..

 

그래서 등장한 것이 바로 '와일드 카드'입니다.

<? extends T> : 와일드 카드의 상한 제한. T와 그 자손들만 가능
<? super T>   : 와일드 카드의 하한 제한. T와 그 조상들만 가능
static <T> void sort(List<T> list, Comparator<? super T> c) 처럼 Compatator에서 많이 쓴다.
<?>           : 제한 없음. 모든 타입이 가능하다. <? extends Object>와 같다.

 

와일드 카드를 사용해서 방금 전의 문제를 해결해보겠습니다.

  static Juice makeJuice(FruitBox<? extends Fruit> box) {
    String tmp = "";
    for (Fruit fruit : box.getList()) {
      tmp += fruit + " ";
    }
    return new Juice(tmp);
  }

 

  6. 지네릭 메서드

 

메서드 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라 합니다.

(선언 위치는 반환타입 바로 앞)

static <T> void sort(List<T> list, Comparator<? super T> c)

 

지네릭 클래스에 정의된 타입 변수 T

지네릭 메서드에 정의된 타입 변수 T

2개는 전혀 별개입니다.

 

class FruitBox<T> {
  ...
  static <T> void sort(List<T> list, Comparator<? super T> c) {
  ...
  }
}

 

FruitBox<T> 의 T

static <T> 의 T

문자만 같을 뿐 둘은 다른 것입니다.

 

'3. 지네릭스 제한사항'에서 static 멤버에는 타입 변수 T를 사용할 수 없다고 말씀드렸는데요

위와 같이 메서드에 지네릭 타입을 선언하고 사용하는 것은 가능합니다.

 

메서드에 선언된 지네릭 타입은 지역 변수를 선언한 것과 같다고 생각하면 됩니다.

(메서드 내에서만 지역적으로 사용될 것이기 때문입니다.)

 

지네릭 메서드를 활용해서 이전의 예제를 변경할 수 있습니다.

// 와일드 카드를 활용해서 static 메서드의 매개변수를 적절히 처리함.
class Juicer {
  static Juice makeJuice(FruitBox<? extends Fruit> box) {
    String tmp = "";
    for (Fruit fruit : box.getList()) {
      tmp += fruit + " ";
    }
    return new Juice(tmp);
  }
}

// 지네릭 메서드를 활용해서 처리
class Juicer {
  static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {
    String tmp = "";
    for (Fruit fruit : box.getList()) {
      tmp += fruit + " ";
    }
    return new Juice(tmp);
  }
}

 

지네릭 메서드를 호출할 때는 아래와 같이 타입 변수에 타입을 대입해야 합니다.

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();

Juicer.<Fruit>makeJuice(fruitBox);
Juicer.<Apple>makeJuice(appleBox);

 

하지만 대부분의 경우 컴파일러가 타입을 추정할 수 있기 때문에 생략해도 됩니다.

 

  7. 알게된 점

 

강의에서 나온 코드를 다시 보니

  public <T> T execute(String message, TraceCallback<T> callback) {
    ...
  }

 

<T>는 지네릭 메서드이고 바로 옆의 T는 지네릭 클래스의 타입 변수였다는걸 알 수 있었습니다.

 

----------------------------------------------------------------------------------------------------------------------

출처: 자바의 정석 3판 (저자 : 남궁성)

'Java' 카테고리의 다른 글

[Java] JDK, JRE, JVM 질의응답 (ChatGPT)  (1) 2024.08.31
[Java] Reflection  (2) 2024.02.21
JVM 메모리 구조 (JAVA)  (1) 2024.02.08

댓글