[Java] 제네릭 ③

kiteB·2022년 2월 13일
0

Java2

목록 보기
5/36
post-thumbnail

[ 제한된 타입 파라미터(<T extends 최상위타입>) ]

숫자를 연산하는 제네릭 메소드는 매개값으로 Number 타입 또는 하위 클래스 타입 (Byte, Short, Integer, Long, Double)의 인스턴스만 가져야 한다.

이와 같이 제한된 타입 파라미터(bounded type paramter)를 사용하여 타입을 제한해야 하는 경우가 있다.

📌 제한된 타입 파라미터 선언방법

타입 파라미터 뒤에 extends 키워드를 붙이고 상위 타입을 명시한다.

public <T extends 상위 타입> 리턴타입 메소드(매개변수, ...) { ... }
  • 상위 타입은 클래스뿐만 아니라 인터페이스도 가능하다. 하지만 인터페이스라고 해서 implements를 사용하지는 않고, 똑같이 extends를 사용해야 한다.
  • 타입 파라미터에 지정되는 구체적인 타입은 상위 타입이거나 상위 타입의 하위 또는 구현 클래스만 가능하다.
  • 메소드의 중괄호 {} 안에서 타입 파라미터의 변수로 사용 가능한 것은 상위 타입의 멤버(필드, 메소드)로 제한된다. 하위 타입에만 있는 필드와 메소드는 사용할 수 없다.

✔️ 예제 | 숫자 타입만 구체적인 타입으로 갖는 제네릭 메소드 compare()

  • 두 개의 숫자 타입을 매개값으로 받아 차이를 리턴한다.
public <T extends Number> int compare(T t1, T t2) {
    double v1 = t1.doubleValue();	//Number의 doubleValue() 메소드 사용
    double v2 = t2.doubleValue();	//Number의 doubleValue() 메소드 사용
    return Double.compare(v1, v2);
}
  • doubleValue() 메소드는 Number 클래스에 정의되어 있는 메소드로 숫자를 double 타입으로 변환한다.
  • Double.compare() 메소드는 첫 번째 매개값이 작으면 -1을, 같으면 0을, 크면 1을 리턴한다.

  • Util (제네릭 메소드)
public class Util {
    public static <T extends Number> int compare(T t1, T t2) {
        double v1 = t1.doubleValue();	//Number의 doubleValue() 메소드 사용
        double v2 = t2.doubleValue();	//Number의 doubleValue() 메소드 사용
        return Double.compare(v1, v2);
    }
}
  • BoundedTypeParameterExample (제네릭 메소드 호출)
public class BoundedTypeParameterExample {
    public static void main(String[] args) {
        //String str = Util.compare("a", "b");	(x) -> String은 Number 타입이 아니다
        
        int result1 = Util.compare(10, 20);	//int -> Integer (자동 Boxing)
        System.out.println(result1);
        
        int result2 = Util.compare(4.5, 3);	//double -> Double (자동 Boxing)
        System.out.println(result2);
    }
}

[ 와일드카드 타입(<?>, <? extends ...>, <? super ... >) ]

와일드카드(wildcard)란 이름에 제한을 두지 않음을 표현하는데 사용하는 기호를 의미하는데, 자바의 제네릭에서는 ? 기호를 사용하여 와일드카드를 사용할 수 있다.

제네릭 타입을 매개값이나 리턴 타입으로 사용할 때 구체적인 타입 대신에 와일드카드를 다음과 같이 세 가지 형태로 사용할 수 있다.

<?> - Unbounded Wildcards (제한 없음)

<?>	//타입 변수에 모든 타입을 사용할 수 있다.

모든 클래스나 인터페이스 타입이 올 수 있다.


<? extends T> - Upper Bounded Wildcards (상위 클래스 제한)

<? extends T> 	//T 타입과 T 타입을 상속받는 자손 클래스 타입만을 사용할 수 있다.

T와 그 자손 클래스 타입을 구현한 객체들만 올 수 있다.


<? super T> - Lower Bounded Wildcards (하위 클래스 제한)

<? super T>	//T 타입과 T 타입이 상속받은 조상 클래스 타입만을 사용할 수 있다.

T와 그 조상 클래스 타입을 구현한 객체들만 올 수 있다.


✔️ 예제 | 제네릭 타입 Course

Course는 과정 클래스로 과정 이름과 수강생을 저장할 수 있는 배열을 가지고 있다. 타입 파라미터 T가 적용된 곳은 수강생 타입 부분이다.

  • Course (제네릭 타입)
public class Course<T> {
    private String name;
    private T[] students;
    
    public Course(String name, int capacity) {
        this.name = name;
        students = (T[]) (new Object[capacity]);	//타입 파라미터로 배열을 생성하려면 new T[n] 형태로 배열을 생성할 수 없고
        											//(T[]) (new Object[n])으로 생성해야 한다.
    }
    
    public String getName() { return name; }
    public T[] getStudents() { return students; }
    public void add(T t) {	//배열에 비어있는 부분을 찾아서 수강생을 추가하는 메소드
        for (int i = 0; i < students.length; i++) {
            if (students[i] == null) {
                students[i] = t;
                break;
            }
        }                                                    
    }
}

수강생이 될 수 있는 타입은 다음 4가지 클래스라고 가정하자. Person의 하위 클래스로 WorkerStudent가 있고, Student의 하위 클래스로 HighStudent가 있다.

  • Course<?>
    • 수강생은 모든 타입(Person, Worker, Student, HighStudent)이 될 수 있다.
  • Course<? extends Student>
    • 수강생은 StudentHighStudent만 될 수 있다.
  • Course<? super Worker>
    • 수강생은 WorkerPerson만 될 수 있다.


[ 제네릭 타입의 상속과 구현 ]

제네릭 타입도 다른 타입과 마찬가지로 부모 클래스가 될 수 있다.

다음은 Product<T, M> 제네릭 타입을 상속해서 ChildProduct<T, M> 타입을 정의한다.

public class ChildProduct<T, M> extends Product<T, M> { ... }

자식 제네릭 타입은 추가적으로 타입 파라미터를 가질 수 있다.
다음은 세 가지 타입 파라미터를 가진 자식 제네릭 타입을 선언한 것이다.

public class ChildProduct<T, M, C> extends Product<T, M> { ... }

✔️ 예제

  • Product (부모 제네릭 클래스)
public class Product<T, M> {
    private T kind;
    private M model;
    
    public T getKind() { return this.kind; }
    public M getModel() { return this.model; }
    
    public void setKind(T kind) { this.kind = kind; }
    public void setModel(M model) { this.model = model; }
    }
}

class Tv { }
  • ChildParent (자식 제네릭 클래스)
public class ChildProduct<T, M, C> extends Product<T, M> {
    private C company;
    public C getCompany() { return this.company; }
    public void setCompany(C company) { this.company = company; }
}

제네릭 인터페이스를 구현한 클래스도 제네릭 타입이 되는데, 다음과 같이 제네릭 인터페이스가 있다고 가정해보자.

  • Storage (제네릭 인터페이스)
public interface Storage<T> {
    public void add(T item, int index);
    public T get(int index);
}

제네릭 인터페이스인 Storage<T> 타입을 구현한 StorageImpl 클래스도 제네릭 타입이어야 한다.

  • StorageImple (제네릭 구현 클래스)
public class StorageImpl<T> implements Storage<T> {
    private T[] array;
    
    public StorageImpl(int capacity) {
        this.array = (T[]) (new Object[capacity]);	//타입 파라미터로 배열을 생성하려면 new T[n] 형태로 생성할 수 없고
        											//(T[n])(new Object[n])으로 생성해야 한다.
    }
    
    @Override
    public void add(T item, int index) {
        array[index] = item;
    }
    
    @Override
    public T get(int index) {
        return array[index];
    }
}

다음 ChildProductAndStorageExampleChildProduct<T, M, C>StorageImpl<T> 클래스의 사용 방법을 보여준다.

public class ChildProductAndStorageExample {
    public static void main(String[] args) {
        ChildProduct<Tv, String, String> product = new ChildProduct<>();
        product.setKind(new Tv());
        product.setModel("SmartTV");
        product.setCompany("Samsung");
        
        Storage<Tv> storage = new Storage<tv>(100);
        storage.add(new Tv(), 0);
        Tv tv = storage.get(0);
    }
}

[ 참고자료 ]

이것이 자바다 책
http://tcpschool.com/java/java_generic_various

profile
🚧 https://coji.tistory.com/ 🏠

0개의 댓글