공부/JAVA

[Java] 제네릭

raptorhs 2024. 7. 3. 12:00

학습 목표

  • 제네릭의 장점을 이해한다.
  • 제네릭 클래스를 정의하고 활용할 수 있다.
  • 제네릭 메서드를 정의하고 활용할 수 있다.

제네릭이란?

제네릭의 필요성

class Basket {
    private String item;

    Basket(String item) {
        this.item = item;
    }

    public String getItem() {
        return item;
    }

    public void setItem(String item) {
        this.item = item;
    }
}

위 코드는 오로지 String 타입의 데이터만을 저장할 수 있는 인스턴스를 만들 수 있습니다.

다양한 타입의 데이터를 저장할 수 있는 객체를 만들고자 한다면 타입별로 별도의 클래스를 만들어야 합니다.

 

하지만 제네릭을 사용하면 단 하나의 Basket 클래스만으로 모든 타입의 데이터를 저장할 수 있는 인스턴스를 만들 수 있습니다.

package Generic;

public class Basket<T> {
    private T item;

    public Basket(T item){
        this.item = item;
    }

    public T getItem() {
        return item;
    }

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

 

String 으로 지정했던 타입들이 T라는 문자 하나로 바뀌었습니다.

Basket<String> basket1 = new Basket<String>("기타 줄");

 위 코드를 실행하면 Basket 클래스 내부의 T가 모두 String으로 치환되는 것처럼 동작하게 됩니다.

 

Basket<Integer> basket2 = new Basket<Integer>(1);

// 위와 같이 인스턴스화하면 Basket 클래스는 아래와 같이 변환됩니다.
class Basket<Integer> {
    private Integer item;

    public Basket(Integer item) {
        this.item = item;
    }

    public Integer getItem() {
        return item;
    }

    public void setItem(Integer item) {
        this.item = item;
    }
}

 

제네릭은 타입을 구체적으로 지정하는 것이 아니라, 추후에 지정할 수 있도록 일반화해두는 것을 의미합니다. 클래스 또는 메서드의 코드가 특정 데이터 타입에 얽매이지 않게 해 둔것을 의미합니다.

 

제네릭 클래스

제네릭 클래스 정의

제네릭이 사용된 클래스를 제네릭 클래스라고 합니다. 앞서 살펴보았던 Basket 클래스가 바로 제네릭 클래스입니다.

class Basket<T> {
    private T item;

    public Basket(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }

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

 

T를 타입 매개변수라고 하며,<T>와 같이 꺾쇠 안에 넣어 클래스 이름 옆에 작성해줌으로써 클래스 내부에서 사용할 타입 매개변수를 선언할 수 있습니다.

class Basket<K, V> { ... }

타입 매개변수를 여러개 사용해야한다면 이렇게 선언하면 됩니다.

 

제네릭 클래스를 정의할 때 주의할 점

제네릭 클래스 변수에는 타입 매개변수를 사용할 수 없습니다. 만약, 클래스 변수에 타입 매개변수를 사용할 수 있다면 클래스 변수의 타입이 인스턴스 별로 달라지게 됩니다.

 

제네릭 클래스 사용

타입 매개변수에 치환될 타입으로 기본 타입을 지정할 수 없습니다.만약 int,double과 같은 원시 타입을 지정해야 하는 맥락에서는 Integer, Double과 같은 래퍼 클래스를 활용합니다.

Basket<String>  basket1 = new Basket<String>("Hello");
Basket<Integer> basket2 = new Basket<Integer>(10);
Basket<Double>  basket3 = new Basket<Double>(3.14);

위 코드에서 구체적인 타입을 생략하고 작성해도 됩니다. 참조 변수의 타입으로부터 유추할 수 있기 때문입니다.

 

제한된 제네릭 클래스

타입 매개변수를 선언할 때 아래와 같이 코드를 작성해 주면 Basket 클래스를 인스턴스화할 때 타입으로 Flower 클래스의 하위 클래스만 지정하도록 제한됩니다.

 

package Generic;
class Flower{}
class Rose extends Flower{}
class RosePasta{}
class Basket<T extends Flower> {
    private T item;
//
//    public Basket(T item) {
//        this.item = item;
//    }

    public T getItem() {
        return item;
    }

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

class Main{
    public static void main(String[] args) {
        Basket<Rose> roseBasket = new Basket<>();
//        Basket<RosePasta> rosePastaBasket = new Basket<>(); 오류발생
    }
}

 

특정 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한할 수도 있습니다. 이 경우에도 동일하게 extends 키워드를 사용합니다.

interface Plant { ... }
class Flower implements Plant { ... }
class Rose extends Flower implements Plant { ... }

class Basket<T extends Plant> {
    private T item;

		...
}

class Main {
    public static void main(String[] args) {

        // 인스턴스화
        Basket<Flower> flowerBasket = new Basket<>();
        Basket<Rose> roseBasket = new Basket<>();
    }
}

 

특정 클래스를 상속받으면서 동시에 특정 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한하려면 아래와 같이 &를 사용하여 코드를 작성해 주면 됩니다.

 

interface Plant { ... }
class Flower implements Plant { ... }
class Rose extends Flower implements Plant { ... }

class Basket<T extends Flower & Plant> {
    private T item;

		...
}

class Main {
    public static void main(String[] args) {

        // 인스턴스화
        Basket<Flower> flowerBasket = new Basket<>();
        Basket<Rose> roseBasket = new Basket<>();
    }
}

 

제네릭 메서드

클래스 전체를 제네릭으로 선언할 수도 있지만, 클래스 내부의 특정 메서드만 제네릭으로 선언할 수 있습니다. 이를 제네릭 메서드라고 합니다.

 

class Basket<T> {                        // 1 : 여기에서 선언한 타입 매개 변수 T와
		...
		public <T> void add(T element) { // 2 : 여기에서 선언한 타입 매개 변수 T는 서로 다른 것입니다.
				...
		}
}

 

클래스명 옆에서 선언한 타입 매개 변수는 클래스가 인스턴스화될 때 타입이 지정됩니다.

그러나, 제네릭 메서드의 타입 지정은 메서드가 호출될 때 이루어집니다.

Basket<String> basket = new Bakset<>(); // 위 예제의 1의 T가 String으로 지정됩니다.
basket.<Integer>add(10);                // 위 예제의 2의 T가 Integer로 지정됩니다.
basket.add(10);                         // 타입 지정을 생략할 수도 있습니다.

 

제네릭 메서드는 메서드가 호출되는 시점에서 제네릭 타입이 결정되므로, 제네릭 메서드를 정의하는 시점에서는 실제 어떤 타입이 입력되는지 알 수 없습니다. 따라서 length()와 같은 String 클래스의 메서드는 제네릭 메서드를 정의하는 시점에 사용할 수 없습니다. 하지만 Object 클래스의 메서드는 사용 가능합니다.

 

 

와일드카드

자바의 제네릭에서 와일드카드는 어떠한 타입으로든 대체될 수 있는 타입 파라미터를 의미하며, 기호 ?로 와일드카드를 사용할 수 있습니다. 와일드카드는 다음과 같은 형태로 사용됩니다.

<? extends T><? super T>

<? extends T>는 와일드카드에 상한제한, 즉 T와 T를 상속받는 하위클래스타입만 타입 파라미터로 받게 지정합니다.

<? super T>는 와일드카드에 하한제한, 즉 T와 T의 상위 클래스만 타입 파라미터로 받도록 합니다.

extends와 super가 쓰이지 않은 <?>는 <? extends Object>와 같습니다.

 

package polymorphism_example.generic;

public class Phone { }

class IPhone extends Phone{}
class Galaxy extends Phone{}

class IPhone12Pro extends IPhone{}
class IPhoneXS extends IPhone{}

class S22 extends Galaxy{}
class ZFlip3 extends Galaxy{}

class User<T> {
    public T phone;

    public User(T phone){
        this.phone = phone;
    }
}

 

package polymorphism_example.generic;

public class PhoneFunction {
    public static void call(User<? extends Phone>user){
        System.out.println("------------------------");
        System.out.println("user.phone = " + user.phone.getClass().getSimpleName()); // phone은 제네릭 타입이여서 쌩으로 Class.java안에 있는 getSimplename을 사용할 수 없으니까 getClass()를 써줫다.
        System.out.println("모든 Phone은 통화를 할 수 있습니다.");
    }
    public static void faceId(User<? extends IPhone> user) {
        System.out.println("-----------------------------");
        System.out.println("user.phone = " + user.phone.getClass().getSimpleName());
        System.out.println("IPhone만 Face ID를 사용할 수 있습니다. ");
    }

    public static void samsungPay(User<? extends Galaxy> user) {
        System.out.println("-----------------------------");
        System.out.println("user.phone = " + user.phone.getClass().getSimpleName());
        System.out.println("Galaxy만 삼성 페이를 사용할 수 있습니다. ");
    }

    public static void recordVoice(User<? super Galaxy> user) {
        System.out.println("-----------------------------");
        System.out.println("user.phone = " + user.phone.getClass().getSimpleName());
        System.out.println("안드로이드 폰에서만 통화 녹음이 가능합니다. ");
    }
}

 

call: 휴대전화의 기본적인 통화기능으로, 모든 휴대전화에서 사용할 수 있는 기능입니다.

faceId: 애플의 안면 인식 보안기능으로, 아이폰만 사용 가능합니다.

samsungPay: 삼성휴대전화에서만 사용가능합니다.

recordVoice: 아이폰을 제외한 안드로이드 휴대전화에서만 사용가능합니다.

 

package polymorphism_example.generic;

public class Example {
    public static void main(String[] args) {
        Phone phone = new Phone();
        PhoneFunction.call(new User<Phone>(new Phone())); // T -> Phone 객체  ... User클래스의 생성자부분참조
        PhoneFunction.call(new User<IPhone>(new IPhone()));
        PhoneFunction.call(new User<Galaxy>(new Galaxy()));
        PhoneFunction.call(new User<IPhone12Pro>(new IPhone12Pro()));
        PhoneFunction.call(new User<IPhoneXS>(new IPhoneXS()));
        PhoneFunction.call(new User<S22>(new S22()));
        PhoneFunction.call(new User<ZFlip3>(new ZFlip3()));

        System.out.println("\n#####################################\n");

//        PhoneFunction.faceId(new User<Phone>(new Phone())); // X
        PhoneFunction.faceId(new User<IPhone>(new IPhone()));
        PhoneFunction.faceId(new User<IPhone12Pro>(new IPhone12Pro()));
        PhoneFunction.faceId(new User<IPhoneXS>(new IPhoneXS()));
//        PhoneFunction.faceId(new User<Galaxy>(new Galaxy())); // X
//        PhoneFunction.faceId(new User<S22>(new S22())); // X
//        PhoneFunction.faceId(new User<ZFlip3>(new ZFlip3())); // X
        //faceId의 매개 변수는 User<? extends IPhone>로, faceId를 호출할 때에는 User의 타입으로 IPhone 또는 IPhone을 상속받는 클래스를 타입으로 넣어주어야 합니다.

        System.out.println("\n######################################\n");

//        PhoneFunction.samsungPay(new User<Phone>(new Phone())); // X
//        PhoneFunction.samsungPay(new User<IPhone>(new IPhone())); // X
//        PhoneFunction.samsungPay(new User<IPhone12Pro>(new IPhone12Pro())); // X
//        PhoneFunction.samsungPay(new User<IPhoneXS>(new IPhoneXS())); // X
        PhoneFunction.samsungPay(new User<Galaxy>(new Galaxy()));
        PhoneFunction.samsungPay(new User<S22>(new S22()));
        PhoneFunction.samsungPay(new User<ZFlip3>(new ZFlip3()));

        System.out.println("\n######################################\n");

        PhoneFunction.recordVoice(new User<Phone>(new Phone()));
//        PhoneFunction.recordVoice(new User<IPhone>(new IPhone())); // X
//        PhoneFunction.recordVoice(new User<IPhone12Pro>(new IPhone12Pro())); // X
//        PhoneFunction.recordVoice(new User<IPhoneXS>(new IPhoneXS())); // X
        PhoneFunction.recordVoice(new User<Galaxy>(new Galaxy()));
//        PhoneFunction.recordVoice(new User<S22>(new S22())); // <? super Galaxy>는 상속 계층도 상에서 Galaxy 및 Galaxy보다 위에 있는 상위 클래스만 타입으로 지정할 수 있게 제한해 줍니다.
//        PhoneFunction.recordVoice(new User<ZFlip3>(new ZFlip3())); // X

    }

}