백기선님의 스터디 14주차 과제

Generic

데이터 타입을 일반화하는 것

  • Java 5에 등장했으며, 클래스나 메서드에서 사용할 내부 데이터 타입을 컴파일 시 미리 지정하는 방법이다.
  • 제네릭 등장 이전에는 Object 타입을 원하는 타입으로 캐스팅하여 사용했지만, 오류 발생 가능성이 생겼다.
  • 컴파일시 타입체크는 클래스나 메서드 내부에서 사용되는 객체의 타입 안정성을 높일 수 있으며
  • 반환값에 대한 타입 변환 및 타입 검사에 들어가는 시간을 줄인다.

사용법

class GenericsEx<T> {
    T element;
    void setElement(T element) {
        this.element = element;
    }
    T getElement() {
        return element;
    }
}
  • 아무 이름을 지정하더라도 컴파일하는데 지장이 없지만, 네이밍 관례를 따르는 것이 좋다.
    • E : 요소(Element로 주로 자바 컬렉션에서 사용)
    • K : 키(key)
    • N : 숫자(number)
    • T : 타입(type)
    • V : 값(value)
  • 여러 개의 타입 변수는 쉼표(,)로 구분하여 명시가능하다.
  • 클래스 뿐만아니라 메서드의 패러미터, 리턴값으로 사용할 수 있다.

예시를 살펴보자.

public class Box<T> {
    private T content;

    public void add(T item) {
        this.content = item;
    }

    public T get() {
        return this.content;
    }

    public static void main(String[] args) {
        // 정수형 Box 생성
        Box<Integer> integerBox = new Box<>();
        integerBox.add(10);
        int intValue = integerBox.get();
        System.out.println("정수형 Box의 내용: " + intValue);

        // 문자열 Box 생성
        Box<String> stringBox = new Box<>();
        stringBox.add("안녕하세요!");
        String stringValue = stringBox.get();
        System.out.println("문자열 Box의 내용: " + stringValue);
    }
}

// 실행 결과
정수형 Box의 내용: 10
문자열 Box의 내용: 안녕하세요!
  • Box 클래스는 제네릭 타입 매개변수 T를 사용한다.
  • add 메서드는 T 타입의 항목을 Box에 추가하고, get 메서드는 T 타입의 항목을 반환한다.
  • main 메서드에서는 Box 클래스를 정수형과 문자열 타입에 대해 인스턴스화하고, 각각의 값을 추가하고 가져와서 출력한다.
  • 제네릭을 사용하여 다양한 타입의 데이터를 처리할 수 있는 유연성과 재사용성을 확인할 수 있다.

Generic 주요 개념

Bounded type parameter(바운디드 타입 패러미터)

  • 바운디드 타입 패러미터는 특정 타입의 서브타입으로 제한하는 것이다.
    • 예를 들어 숫자에 대해 작동하는 메서드는 Nuber 혹은 해당 하위 클래스의 인스턴스만 허용하려고 하는 경우가 있다.
<T extends UpperBound>

<T extends B1 & B2>

<T extends Class1 & Interface1 & Interface2>
  • 바운디드 타입 패러미터를 선언하려면 타입 패러패러미터의 이름, extends 키워드, 상위 바운드를 나열한다.
  • 여러 개의 상위 바운드를 가질 수 있다.
  • 만약 여러 상위 바운드 중 클래스가 있다면 해당 클래스가 가장 먼저 앞에와야 한다. (컴파일 에러)
public class BoundTypeEx <T extends Number> {
    
    public void set(T value) {}

    public static void main(String[] args) {
        BoundTypeEx<Integer> boundTypeSample = new BoundTypeSample<>();
        boundTypeEx.set("Hi"); // 컴파일 에러 발생
    }
}
  • BoundTypeEx 클래스의 타입 패러미터를 로 선언한다.
    • 이는 BoundTypeEx의 타입으로 Number의 서브 타입만 허용한다는 것을 의미한다.
  • Integer는 Number의 서브타입으로 유효한 선언이지만, set 함수의 패러미터로 String을 전달하려고 했기 때문에 컴파일 에러가 발생한다.

Wildcard(와일드카드)

<? Extends UpperBound>
  • 제네릭으로 구현된 메서드의 경우 선언된 타입으로만 매개변수를 입력해야한다.
  • 이를 상속받은 클래스 또는 부모 클래스를 사용하고 싶어도 불가능하며, 어떤 타입이 와도 상관없는 경우에는 대응하기 좋지 않다.
  • 이를 위한 해결책인 와일드 카드('?')는 알 수 없는 유형을 나타내며, 패러미터 변수, 필드, 지역변수의 타입 등 다양한 상황에서 사용할 수 있다.

  • Unbounded Wildcard
    • List<?>와 같은 형태로 물음표만 가지고 정의된다.
    • 내부적으로 Object로 정의되어서 사용되고 모든 타입의 인자를 받을 수 있다.
    • 타입 패러미터에 의존하지 않는 메서드만을 사용하거나 Object 메서드가 제공하는 기능으로 충분한 경우에 사용한다.
    • 예시로 코드 타입 매개변수에 의존하지 않는 제네릭 클래스의 메서드를 사용하는 경우인 List.clear, List.size가 있다.
import java.util.ArrayList;
import java.util.List;

public class UnboundedWildcardExample {
    public static double sumList(List<? extends Number> list) {
        double sum = 0;
        for (Number number : list) {
            sum += number.doubleValue();
        }
        return sum;
    }

    public static void main(String[] args) {
        List<Integer> integerList = new ArrayList<>();
        integerList.add(1);
        integerList.add(2);
        integerList.add(3);

        List<String> stringList = new ArrayList<>();
        stringList.add("Hello");
        stringList.add("World");

        double sum1 = sumList(integerList); // 정수형 리스트의 합 계산
        double sum2 = sumList(stringList); // 컴파일 에러!

        System.out.println("정수형 리스트 합: " + sum1);
        System.out.println("실수형 리스트 합: " + sum2);
    }
}
  • sumList 메서드는 제한되지 않은 상위 바운드 와일드카드(List<? extends Number>)를 사용하여 Number 클래스의 하위 타입 리스트를 인자로 받는다.
  • 그러나 sumList 메서드에 문자열 리스트를 전달하면 컴파일 에러가 발생한다.
  • 이는 문자열이 Number 클래스의 하위 타입이 아니기 때문이다.

  • Upper Bounded Wildcard
    • List<? extends Foo>와 같은 형태로 사용하며 특정 클래스의 자식 클래스만을 인자로 받는다.
    • 임의의 Foo 클래스를 상속받은 어느 클래스가 와도되지만 사용할 수 있는 기능은 Foo에 정의된 기능만 사용가능하다.
  • Lower Bounded Wildcard
    • List<? super Foo> 와 같은 형태로 사용하며 특정 클래스의 부모 클래스만을 인자로 받는다.
import java.util.ArrayList;
import java.util.List;

public class BoundedWildcardExample {
    public static void addNumbers(List<? super Integer> list) {
        for (int i = 1; i <= 5; i++) {
            list.add(i);
        }
    }

    public static void main(String[] args) {
        List<Object> objectList = new ArrayList<>();
        objectList.add("Hello");

        List<String> stringList = new ArrayList<>();
        stringList.add("World");

        addNumbers(objectList); // Object 타입 리스트에 정수형 추가
        addNumbers(stringList); // 컴파일 에러!

        System.out.println("Object 타입 리스트: " + objectList);
        System.out.println("문자열 리스트: " + stringList);
    }
}
  • addNumbers 메서드는 하위 바운드 와일드카드(List<? super Integer>)를 사용하여 Integer 클래스의 상위 타입 리스트를 인자로 받는다.
  • 하지만 addNumbers 메서드에 문자열 리스트를 전달하면 컴파일 에러가 발생
  • 이는 문자열이 Integer 클래스의 상위 타입이 아니기 때문이다.

Parameterize type(매개변수화 타입)

  • 하나 이상의 타입 매개변수를 선언하고 있는 클래스, 인터페이스를 제니릭 클래스, 인터페이스라고 하며 이를 제네릭 타입이라 한다.
  • 각 제네릭 타입에서는 매개변수화 타입들을 정의한다.
List<String> list = new ArrayList<>();

// 컴파일 시
ArrayList list = new ArrayList();
  • <> 안의 String은 실 타입 매개변수라하며 List 인터페이스에 선언되어있는 List의 E를 형식 타입 매개변수라 한다.
  • 제네릭은 타입 소거자(Type erasure)에 의해 자신의 타입 요소 정보를 삭제한다.

Generic 메서드 만들기

// Collections.sort
public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}
  • 제네릭 메서드는 메서드의 선언부에 타입 변수를 사용한 메서드를 의미한다.
  • 타입 변수의 선언은 메서드 선언부에서 반환 타입 바로 앞에 위치한다.
  • 제네릭 클래스에서 정의된 타입 변수 T와 제네릭 메서드에서 사용된 타입 변수 T는 별개이다.

Erasure

Type Erasure

  • 컴파일러는 컴파일 타임에 타입 패러미터를 사용하는 대상의 타입을 컴파일러가 정하는 타입으로 대체하는 Type Erasure를 실행한다.
  • 컴파일된 바이트 코드에서는 T 대신 특정 타입으로 대체된다.
  • 제네릭 타입의 타입 패러미터가 상하한이 있는 경우에 타입 패러미터를 한계 타입으로, 없는 경우 모든 타입 패러미터를 Object로 바꾼다.
    • 이렇게 생성된 바이트 코드에는 보통 클래스, 인터페이스 및 메서드만 포함된다.
  • type-safety를 유지하기 위해 필요한 경우 타입 캐스팅을 사용할 수 있다.
  • 제네릭 타입을 상속받은 클래스에서는 다형성을 유지하기 위해 브릿지 메서드를 생성한다.

제네릭 타입 Erasure

  • Java 컴파일러는 타입 Erasure 프로세스로써 모든 타입 패러미터를 지우고 타입 패러미터가 바인드된 경우 첫 번째 바인드로 대체하고, 바인드되지 않은 경우 Object로 대체한다.
public class WitchPot<T> {
    private T meterial;

    public WitchPot(T meterial) {
        this.meterial = meterial;
    }

    public T get() {
        return this.meterial;
    }
    public void set(T meterial) {
        this.meterial = meterial;
    }

}

// 바이트 코드
javap -c WitchPot 
Warning: File ./WitchPot.class does not contain class WitchPot
Compiled from "WitchPot.java"
public class javageneric.WitchPot<T> {
  public javageneric.WitchPot(T);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: aload_1
       6: putfield      #2                  // Field meterial:Ljava/lang/Object;
       9: return

  public T get();
    Code:
       0: aload_0
       1: getfield      #2                  // Field meterial:Ljava/lang/Object;
       4: areturn

  public void set(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field meterial:Ljava/lang/Object;
       5: return
}
  • 바이트코드 확인 시, T가 사용되는 부분에 lang/Object 즉 Object로 대체됨을 확인할 수 있다.

브릿지 메서드

  • 제네릭 클래스를 상속받거나, 제네릭 인터페이스를 구현하는 클래스, 인터페이스를 컴파일할 때 컴파일러는 타입 Erasure 프로세스의 일부로 브릿지 메서드라는 합성 메서드를 만들어야할 수도 있다.
  • 이 브릿지 메서드는 매개변수나 반환타입을 소거하기 위해 만들어지는 메서드이다.
public class Shape<T> {
    public void draw(T shape) {
        System.out.println("Drawing shape: " + shape);
    }
}

public class Circle extends Shape<String> {
    @Override
    public void draw(String shape) {
        System.out.println("Drawing circle: " + shape);
    }
}

public class Main {
    public static void main(String[] args) {
        Circle circle = new Circle();
        circle.draw("Red circle");
    }
}
  • Shape 클래스는 제네릭 타입 T를 받는 메서드 draw를 가지고 있고, Circle 클래스는 Shape을 상속받으며, draw 메서드를 오버라이딩하여 문자열을 받도록 정의한다.
  • 이때, Shape 클래스에서 draw 메서드의 시그니처는 draw(T shape)이다. 그러나 제네릭 타입 T는 타입 소거에 의해 Object 타입으로 지워지므로, 실제로는 draw(Object shape)로 동작하게 된다.
  • 따라서 Circle 클래스에서 오버라이딩한 draw 메서드는 draw(String shape)이다.
  • 여기서 컴파일러에 의해 브릿지 메서드가 자동으로 생성되며 브릿지 메서드는 Circle 클래스에 추가되어서 draw(Object shape)의 형태로 생성된다.
  • 이렇게 된다면 Shape 클래스의 draw 메서드와 Circle 클래스의 오버라이딩한 draw 메서드 간에 호환성이 유지되며, 타입 안전성을 보장하면서 상위 클래스와 하위 클래스 간의 형 변환 문제를 해결할 수 있다.

참고자료

  • https://docs.oracle.com/javase/tutorial/java/generics/index.html - docs
  • https://rockintuna.tistory.com/102#type-erasure - erasuer 파트