제네릭(Generic)

김수민·2023년 3월 12일
0

백엔드 부트캠프

목록 보기
18/52

제네릭

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

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;
    }
}
Basket<String> basket1 = new Basket<String>("기타줄");

"Basket 클래스 내의 TString으로 바꿔라"

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

제네릭 클래스

제네릭 클래스 정의

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: 매개변수, 꺽쇠 안에 넣어 클래스 일므 옆에 작성해줌으로써 클래스 내부에서 사용할 타입 매개변수를 선언할 수 있음

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

여러개의 타입 매개변수 사용.

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

클래스 변수에는 타입 매개변수를 사용할 수 없음

class Basket<T> {
  private T item1; // O
  static T item2; // X
}

클래스 변수에 타입 매개변수를 사용할 수 있다면 클래스 변수의 타입이 인스턴스 별로 달라지게 됨.
클래스 변수에 타입 매개변수를 사용할 수 있다면, Basket<String> 으로 만든 인스턴스와 Basket<Integer>로 만든 인스턴스가 공유하는 클래스 변수의 타입이 서로 달라지게 되어 클래스 변수를 통해 같은 변수를 공유하는 것이 아니게 됨. 따라서 static이 붙은 변수 또는 메서드에는 타입 매개변수를 사용할 수 없음.

제네릭 클래스의 사용

제네릭 클래스는 멤버를 구성하는 코드에 특정한 타입이 지정되지 않은 클래스이므로, 제네릭 클래스를 인스턴스화할 때에는 의도하고자 하는 타입을 지정해줘야함.
단, 타입 매개변수에 치환될 타입으로 기본 타입을 지정할 수 없음. 만약, int. double과 같은 원시 타입을 지정해야하는 맥락에서는 Integer, Double과 같은 래퍼 클래스를 사용.

제한된 제네릭 클래스

타입을 지정하는 데에 있어 제한이 없음.

class Flower { ... }
class Rose extends Flower { ... }
class RosePasta { ... }

//제네릭 클래스 정의
class Basket<T> {
  private T 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<>();
  }
}

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

class Flower { ... }
class Rose extends Flower { ... }
class RosePasta { ... }

class Basket<T extends Flower> {
  private T 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 {
  ...
  public <T> void add(T element) {
    ...
  }
}

제네릭 메서드의 타입 매개변수는 제네릭 클래스의 타입 매개변수와 별개의 것임. 즉, T라는 타입 매개변수명을 사용한다하더라도, 같은 알파벳 문자를 이름으로 사용하는 것일 뿐, 서로 다른 타입 매개변수로 간주됨.

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

이는 타입이 지정되는 시점이 서로 다르기 때문. 클래스명 옆에서 선언한 타입 매개변수는 클래스가 인스턴화할 때 타입이 지정됨. 그러나 제네릭 메서드의 타입은 메서드가 호출될 때 이루어짐.

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

클래스 타입 매개변수와 달리 메서드 타입 매개변수는 static 메서드에서도 선언하여 사용할 수 있음

class Basket {
  ...
  static <T> int setPrice(T element) {
    ...
  }
}

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

class Basket {
  public <T> void print(T item) {
    System.out.println(item.length()); // 불가
  }
}

하지만 모든 자바 클래스의 최상위 클래스인 Object 클래스의 메서드는 사용간으함. 모든 클래스는 Object 클래스를 상속받기 때문.

class Bakset {
  public <T> void getPrint(T item) {
    System.out.println(item.equals("Kim coding")); //가능
  }
}

와일드카드

와일드카드: 어떠한 타입으로든 대체될 수 있는 타입 파미터를 의미하며, 기호 ?로 와일드카드를 사용할 수 있음. 일반적으로 extendssuper 키워드를 조합하여 사용.

<? extends T>
<? super T>

<? extends T>는 와일드카드에 상한 제한을 두는 것으로서, TT를 상속받는 하위 클래스 타입만 타입 파라미터로 받을 수 있도록 지정함.
<? super T>는 와일드카드에 하안 제한을 두는 것으로, TT의 상위 클래스만 타입 파라미터로 받도록 함.
extendssuper 키워드와 조합하지 않은 와일드카드(<?>)는 <? extends Object>와 같음. 즉, 모든 클래스 타입은 Object 클래스를 상속받으므로, 모든 클래스 타입을 타입 파라미터로 받을 수 있음을 의미함.

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;
		}
}

  • call: 휻폰의 기본적인 통화 기능으로, 모든 휴대폰에서 사용할 수 있는 기능
    -> ? extends Phone으로 타입을 제한할 수 있음
  • faceId: 애플의 안면 인식 보안 기능으로, 아이폰만 사용 가능
    -> ? extends Iphone으로 타입을 제한할 수 있음
  • samsungPay: 삼성 휴대폰의 결제 기능으로, 삼성 휴대폰에서만 사용 가능
    -> ? extends Galaxy로 타입을 제한할 수 있음
  • recordVoice: 통화 녹음 기능을 일컬으며, 아이폰을 제외한 안드로이드 휴대폰에서만 사용 가능
    -> ? super Galaxy로 타입을 제한할 수 있음
class PhoneFunction {
    public static void call(User<? extends Phone> user) {
        System.out.println("-----------------------------");
        System.out.println("user.phone = " + user.phone.getClass().getSimpleName());
        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("안드로이드 폰에서만 통화 녹음이 가능합니다. ");
    }
}
/////////////////////////////////////////////////////////////////////////////

public class Example {
    public static void main(String[] args) {
        PhoneFunction.call(new User<Phone>(new Phone()));
        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

        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())); // X
//        PhoneFunction.recordVoice(new User<ZFlip3>(new ZFlip3())); // X
    }
}
  1. 왜 에러가 발생할까?
    X로 표기된 코드들은 호출하고 있는 메서드에 표기된 매개 변수의 타입과 정확히 일치하지 않는 경우를 의미. 예를 들어 faceId의 매개변수는 User<? extends Iphone>으로 faceId를 호출할 때에는 User의 타입으로 IPhone 도는 IPhone을 상속받는 클래스를 타입으로 넣어주어야 함. 따라서 IPhone 타입이 아니거나 Iphone을 상속받는 클래스 타입이 아닌 다른 클래스 타입의 객체를 넣어 faceId를 호출하는 경우 에러가 발생함.

  2. recordVoiceS22ZFlip3를 타입으로 지정하면서 호출할 때 왜 에러가 발생할까?
    recordVoice의 매개변수를 보면 User<? super Galaxy> 타입의 객체를 매개 변수로 받고 있음. <? super Galaxy>는 상속 계층도 상에서 GalaxyGalaxy 보다 위에 있는 상위 클래스만 타입으로 지정할 수 있게 제한해줌. 따라서 Galaxy보다 상속 계층도 상 아래에 있는 S22ZFlip3을 타입으로 지정하면서 recordVoice를 호출할 수 없음.

0개의 댓글