[Java] 제네릭

Minit88·2023년 3월 6일
0

[Java]

목록 보기
11/18
post-thumbnail

제네릭

제네릭의 필요성

아래의 예시로 알아보면

class Basket {
    private String item;

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

    public String getItem() {
        return item;
    }

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

Basket 클래스는 오로지 String 타입의 데이터만을 저장할 수 있는 인스턴스를 만들 수 있다. 그에 따라, 다양한 타입의 데이터를 저장할 수 있는 객체를 만들고자 할 때 제네릭을 사용할 수 있다.

제네릭의 사용

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에 해당하는 타입명을 인스턴스를 생성할 때에 정의를 해주어야 한다.

아래의 예제를 통해 정의 예시를 확인해보자.

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

위 코드처럼 T에 String 타입으로 정의를 해 basket1은 String 타입의 데이터를 갖는 인스턴스이다.

제네릭 클래스 정의

제네릭이 사용된 클래스를 제네릭 클래스라고 한다. 앞에서 봤던 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는 타입 매개변수라 한다.
만약, 타입 매개변수를 여러 개 사용해야 한다면, 아래와 같이 선언하면 된다.

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

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

제네릭 클래스에서 타입 매개변수를 임의의 타입으로 사용할 수 있다고 하였다. 이때 아래와 같이 클래스 변수에는 타입 매개변수를 사용할 수 없다.

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

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

제네릭 클래스의 다형성

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<Flower> flowerBasket = new Basket<>();
        flowerBasket.setItem(new Rose());      // 다형성 적용
        flowerBasket.setItem(new RosePasta()); // 에러
    }
}

flowerBasket은 Flower 타입의 데이터를 저장할 수 있고, new Rose() 는 Flower을 상속받아 flower.Basket.setItem() 메서드에 Rose 타입으로 정의가 될 수 있다.
반면, RosePasta 타입은 Flower와 아무런 관계가 없기에, flowerBasket 인스턴스에서 정의 될 수 없다.

제한된 제네릭 클래스

앞서 살펴본 예제의 Basket 클래스는 인스턴스화 할 때 어떠한 타입도 지정해줄 수 있다. 즉, 타입을 지정하는 데에 있어 제한이 없다.

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<>(); // 에러
    }
}

위 코드를 통해 제한된 제네릭 클래스를 알아보자

Basket 클래스의 타입 매개변수 T는 Flower 을 상속받게 된다. 즉, Flower 또는 Flower 을 상속하는 type만 정의가 될 수 있다. 따라서, T는 Flower 또는 Rose 로 정의가 될 수 있다.

제한된 클래스는 특정 클래스를 상속받은 클래스만 타입으로 제한하는 것 뿐만 아니라, 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한할 수 있다.

아래의 예제를 통해 알아보면

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

class Basket<T extends Flower & Plant> { // (1)
    private T item;
	
		...
}

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

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

인터페이스 Plant 의 구현된 Flower와 Flower 의 상속 Plant의 구현클래스인 Rose 로 Basket이 정의가 될 수 있다.
이때 Flower 을 상속받으면서 동시에 Plant 을 구현한 범위 내에서 정의가 되려면 & 를 사용한다.

제네릭 메서드

클래스 전체를 제네릭으로 선언할 수 있지만, 클래스 내부의 특정 메서드만 제네릭으로 선언할 수 있다.
제네릭 메서드의 예시는

class Basket {
		...
		public <T> void add(T element) {
				...
		}
}

처럼 정의가 될 수 있다.

제네릭 메서드의 타입 매개변수는 제네릭 클래스의 타입 매개변수와 별개이다. 아래의 예시를 통해 보면

class Basket {
		...
		public <T> void add(T element) {
				...
		}
}

Basket 클래스의 T와 add 메서드의 T는 별개의 것으로 간주된다.
이는 타입이 지정되는 시점이 서로 다르기 때문이다.

클래스명 옆에 선언한 타입 매개변수는 클래스가 인스턴스화될 때 타입이 지정되고 , 제네릭 메서드의 타입 지정은 메서드가 호출될 때 이루어진다.

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

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

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

제네릭 메서드를 정의하는 시점에ㅓ 실제 어떤 타입이 입력되는 지 알 수 없기에, 제네릭 메서드를 정의하는 시점에서 length() 을 사용할 수 없다.

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

하지만 Object 클래스의 메서드는 사용이 가능하다. 모든 클래스는 Object를 상속받기 때문이다.

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

와일드 카드

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

일반적으로 와일드카드는 extends와 super 키워드를 조합하여 사용한다.

<? extends T>
<? super T>
  • <? extends T> 는 와일드카드에 상한 제한을 두는 것으로서, T와 T 를 상속받는 하위 클래스 타입만 타입 파라미터로 받을 수 있도록 지정한다.
  • <? super T>는 와일드카드에 하한 제한을 두는 것으로, T와 T의 상위 클래스만 타입 파라미터로 받도록 한다.
profile
" To be BE "

0개의 댓글