🙈

⃝ 동글동글 ⃝

🪐ᐩ˖ 🍎

Java/기초

[Java 기초] Generics(제네릭)

JONG_UK 2023. 6. 2. 00:03
728x90
반응형
SMALL

Generics 제네릭

제네릭이란 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴피일 시의 타입체크(compile-time type check)를 해주는 기능

객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움을 줄여준다.

 

타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다.

Generics :
- 다룰 객체의 타입을 미리 명시해 줌으로써 번거로운 형변환을 줄여주는 것
- 인스턴스별로 다르게 동작하도록 하기 위해 만든 기능

 

제네릭의 용어

Box<T> // 제네릭 클래스, 'T의 Box' 또는 'T Box'라고 읽는다.
T      // 타입 변수 또는 타입 매개변수 (T는 타입 문자)
Box    // 원시 타입 (Raw Type)
// 예시
Box<String> b = new Box<String>();
// 부르는 명칭
Box<String> // 제네릭 타입 호출
<String>    // 대입된 타입(매개변수화된 타입, parameterized type)

 

제네릭 클래스의 선언

제네릭 타입은 클래스와 메서드에 선언할 수 있다.

// 제네릭 선언 전
class Box {
	Object item;
    
    void setItem(Object item) { this.item = item; }
    Object getItem() { return item; }
}

// 제네릭 변환
class Box<T> {
	T item;
    
    void setItem(T item) { this.item = item; }
    T getItem() { return item; }
}

제네릭은 <> 안에 선언하게 된다. 위 예제의 Box <T>의 <T>타입 변수(type variable)라고 한다. ('Type'의 첫 글자에서 따온 것)

 

타입 변수는 T가 아닌 E도 존재한다. 예를 들어 ArrayList <E>의 경우, 타입 변수 EElement(요소)의 첫 글자를 따서 사용했다.

Map의 경우 제네릭으로 Map<K, V>라고 정의할 수 있으며, K는 Key, V는 Value를 의미한다.

이 타입 변수는 기호의 종류만 다를 뿐 '임의의 참조형 타입'을 의미한다는 것은 모두 같다.
꼭 'T'뿐만 아니라 어떠한 문자를 사용해도 상관없으며, 여러 개의 타입 변수는 쉼표(,)로 구분하여 명시할 수 있다.
타입 변수는 클래스에서뿐만 아니라 메서드의 매개변수나 반환값으로도 사용할 수 있다. 

 

제네릭 클래스가 된 Box 클래스의 객체를 생성할 때는 타입 변수 T 대신 실제 사용할 타입을 지정해 주면 된다.

제네릭 클래스를 생성할 때 사용할 실제 타입을 명시하면, 내부적으로는 정의된 타입 변수가 명시된 실제 타입으로 변환되어 처리된다.

 

제네릭 클래스는 형변환을 생략 가능 (예시)
Box<String> b = new Box<String>(); // Type T 대신 실제 타입 지정
b.setItem(new Object());  // 불가능 -> String 이외의 타입은 지정 불가
b.setItem("ABC"); // 가능
String item = (String)b.getItem(); // 형변환이 필요 없음
제네릭의 인스턴스별 다른 동작 구현 예시
Box<Apple> appleBox = new Box<Apple>(); // Apple 객체만 저장 가능
Box<Grape> grapeBox = new Box<Grape>(); // Grape 객체만 저장 가능

 

제네릭 클래스 생성 제한 요소

static 클래스

이유 : static 멤버에 타입 변수 T를 사용하면 T는 인스턴스 변수로 간주된다. static 멤버는 인스턴스 변수를 참조할 수 없다.

배열

이유 : new 연산자를 사용하여 배열을 생성하게 되는데, new 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다. 하지만 Box <T> 클래스를 컴파일하는 시점에서는 T가 어떤 타입이 될지 전혀 알 수 없기 때문.

 

 

제네릭 클래스의 객체 생성과 사용

참조변수와 생성자에 대입된 타입이 일치해야 한다. 

Box<Apple> appleBox = new Box<Apple>(); // Apple 객체만 저장 가능
Box<Grape> grapeBox = new Box<Grape>(); // Grape 객체만 저장 가능
// 에러. new 연산자로 Grape 객체만 생성 가능
Box<Grape> grapeBox = new Box<Apple>();

 

JDK 1.7 부터는 추정이 가능한 경우 타입을 생략할 수 있게 되었다. 따라서 아래와 같이 생성이 가능하다.

// 두 문장은 동일함
Box<Apple> appleBox = new Box<Apple>();
Box<Apple> appleBox = new Box<>(); // 생성자에 타입 지정 생략

 

지금까지 했던 것들의 예제
import java.util.ArrayList;

class Fruit               { public String toString() { return "Fruit";}}
class Apple extends Fruit { public String toString() { return "Apple";}}
class Grape extends Fruit { public String toString() { return "Grape";}}
class Toy                 { public String toString() { return "Toy"  ;}}

class FruitBoxEx1 {
	public static void main(String[] args) {
		Box<Fruit> fruitBox = new Box<Fruit>();
		Box<Apple> appleBox = new Box<Apple>();
		Box<Toy>   toyBox   = new Box<Toy>();
//		Box<Grape> grapeBox = new Box<Apple>(); // 에러. 타입 불일치

		fruitBox.add(new Fruit());
		fruitBox.add(new Apple()); // OK. void add(Fruit item)

		appleBox.add(new Apple());
		appleBox.add(new Apple());
//		appleBox.add(new Toy()); // 에러. Box<Apple>에는 Apple만 담을 수 있음

		toyBox.add(new Toy());
//		toyBox.add(new Apple()); // 에러. Box<Toy>에는 Apple을 담을 수 없음

		System.out.println(fruitBox);
		System.out.println(appleBox);
		System.out.println(toyBox);
	}  // main의 끝
}

class Box<T> {
	ArrayList<T> list = new ArrayList<T>();
	void add(T item)  { list.add(item); }
	T get(int i)      { return list.get(i); }
	int size() { return list.size(); }
	public String toString() { return list.toString();}
}

 

타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한하는 제네릭 클래스

제네릭은 'T'와 같은 타입 변수(type variable)를 사용하여 타입을 제한한다.

// 클래스 FruitBox는 
// class FruitBox<T> { }라고 정의되어 있다는 가정
FruitBox<Toy> fruitBox = new FruitBox<Toy>();
fruitBox.add(new Toy());

하지만 타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만, 그래도 여전히 모든 종류의 타입을 저장할 수 있다는 것에는 변함이 없다.

 

이때 extends 키워드를 사용하면 타입 변수에 특정 타입만을 사용하도록 제한할 수 있다.

// extends 키워드를 통해 Fruit 타입만 가능하게 설정
class FruitBox<T extends Furit> {
    ArrayList<T> list = new ArrayList<T>();
    ...
}

// Apple과 Grape은 Fruit 클래스를 상속받는다고 가정
FuritBox<Fruit> fruitBox = new FuritBox<Fruit>();
fruitBox.add(new Apple()); // Apple이 Fruit의 자손이기 때문에 가능
// fruitBox.add(new Toy()); // Toy는 Fruit를 상속받지 않기 때문에 불가능
fruitBox.add(new Grape()); // Grape이 Fruit의 자손이기 때문에 가능

⛔️ 클래스가 아닌 인터페이스를 제한사항으로 할 경우에도 implements 키워드가 아닌 extends 키워드를 사용해야만 한다.

 

클래스 Fruit의 자손이면서 Eatable 인터페이스도 구현해야 한다면, '&' 기호로 연결한다.

// 최상위 Eatable 인터페이스
interface Eatable { ... }
// Eatable 인터페이스를 상속받는 Fruit클래스
class Fruit implements Eatable { ... } 
// 클래스 Fruit의 자손이면서 Eatable 인터페이스도 구현하는 경우의 FruitBox 클래스
class FruitBox<T extends Fruit & Eatable> { ... }

 

제네릭 와일드 카드 '?'

class Juicer {
    static Juice makeJuice(FruitBox<Fruit> box) { // <Fruit>으로 타입 지정
        String tmp = "";
        for(Fruit f : box.getList()) tmp += f + " ";
        return new Juice(tmp);
    }
}

위와 같이 Juicer 처럼 제네릭 클래스가 아닌 클래스의 메서드에 제네릭 <Fruit> 타입으로 지정해준 경우에는 아래와 같이 타입이 일치하는 인스턴스만 메서드를 실행할 수 있다.

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>(); // 매개변수 타입 일치
FruitBox<Apple> appleBox = new FruitBox<Apple>(); // Juicer 메서드의 매개변수 타입 불일치

Juicer.makeJuice(fruitBox); // 가능 -> 매개변수 FruitBox<Fruit> 일치
Juicer.makeJuice(appleBox); // 불가능 -> 매개변수 FruitBox<Fruit> 불일치

이 문제를 해결하기 위해 Juicer 클래스에 makeJuice() 메서드를 오버로딩 하게 된다면 아래와 같이 되지만

class Juicer {
    static Juice makeJuice(FruitBox<Fruit> box) { // <Fruit>으로 타입 지정
        String tmp = "";
        for(Fruit f : box.getList()) tmp += f + " ";
        return new Juice(tmp);
    }
    // 오버로딩
    static Juice makeJuice(FruitBox<Apple> box) { // <Apple>으로 타입 지정
        String tmp = "";
        for(Fruit f : box.getList()) tmp += f + " ";
        return new Juice(tmp);
    }
}

제네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기 때문에 컴파일 에러가 발생할 것이다. 

제네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거해버리기 때문에 두 메서드는 오버로딩이 아니라 '메서드 중복 정의'이다.

 

따라서 이럴 때 사용하기 위해 고안된 것이 바로 '와일드 카드'인 '?'이다.

와일드 카드는 어떠한 타입도 될 수 있다.

'?' 만으로는 Object 타입과 다를 게 없으므로 다음과 같이 'extends'와 'super'로 상한(upper bound)과 하한(lower bound)을 제한할 수 있다.

<? extends T> // 와일드 카드의 상한 제한, T와 그 자손들만 가능
<? super T>   // 와일드 카드의 하한 제한, T와 그 조상들만 가능
<?>           // 제한 없음, 모든 타입 가능, <? extends Object>와 동일
class Juicer {
    // 모든 종류의 FruitBox가 매개변수로 가능
    static Juice makeJuice(FruitBox<? extends Fruit> box) { // 와일드 카드 적용
        String tmp = "";
        for(Fruit f : box.getList()) tmp += f + " ";
        return new Juice(tmp);
    }
}

이렇게 정의하면 makeJuice 메서드의 매개변수로 FruitBox<Fruit> 뿐만 아니라 FuritBox<Apple>, FruitBox<Grape>도 가능하다.

제네릭 클래스와 달리 와일드 카드에는 '&' 기호를 사용할 수 없다. 즉 <? extends T & E>는 불가능하다.
728x90
반응형
LIST