Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

일상기록

Java - 제네릭(Generic) 본문

Java

Java - 제네릭(Generic)

너 구 나 2023. 4. 17. 15:08

제네릭(Generic)이란

  • 컴파일시 타입을 체크해주는 기능(Compile-time type check) - JDK1.5
  • 객체타입의 안정성을 높이고(타입 체크를 정확히 할 수 있다.)형변환의 번거로움을 줄여준다.
ArrayList<Tv> list = new ArrayList<>();

list.add(new Tv()); // OK
list.add(new Audio()); // 에러 Tv타입 이외 다른 타입 저장 불가

런타임 에러를 컴파일 타임으로 가져오는 역활도 한다.

제네릭 생략시 Object타입으로 들어가 형변환이 필요하지만 제네릭을 사용하면 형변환이 필요없다.

제네릭의 장점
    1. 타입의 안정성을 제공한다. (classcastException 방지)
    2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.

클래스 작성시 Object(일반클래스)타입 대신 타입 변수E(제네릭클래스)를 선언해서 사용 JDK1.5 부터 바뀜

제네릭 클래스의 선언

E 타입변수 사용 Object →  E
보통 자주사용하는 타입변수

물론 반드시 한 글자일 필요는 없다. 또한 설명과 반드시 일치해야 할 필요도 없다. 예로들어 <Ele>라고 해도 전혀 무방하다. 다만 대중적으로 통하는 통상적인 선언이 가장 편하기 때문에 위와같은 관례규칙이 있을 뿐이다.

기호의 종류만 다른 뿐 '임의의 참조형 타입'을 의미한다는 것은 모두 같다.

마치 수학식 'f(x, y) = x + y'가 'f(k, v) = k + v'와 다르지 않은 것처럼 
public static void main(String[] args) {
		Box box= new Box();
		box.setItem(new Object()); // 경고 unchecked or unsafe operation
		box.setItem("ABC");        // 경고 unchecked or unsafe operation
	}
	static class Box<E>{
		E item;
		
		void setItem(E item) {
			this.item = item;
		}
		E getItem() {
			return this.item;
		}
	}
	Box<Object> box = new Box<>();
		box.setItem(new Object()); // 경고발생 안함
		box.setItem("ABC");        // 경고발생 안함

제네릭이 도입되기 이전의 코드와 호환성을 유지하기 위해서 제네릭을 사용하지 않은 코드를 허용할 뿐 앞으로 제네릭 클래스를 사용할 때는 반드시 타입을 지정해서 제네릭과 관련된 경고가 나오지 않도록 한다.

제네릭의 용어

타입 문자 T는 제네릭 클래스 Box<T>의 타입 변수 또는 타입 매개변수라고 하는데, 메소드의 매개변수와 유사한 면이 있기 때문에이다. 그래서 매개변수에 타입을 지정하는 것을 '제네릭 타입 호출'이라 하고, 지정된 타입 'String'을 '매개변수화된 타입'(Parameterized type)이라고 한다.

 

예를 들어 Box<String>과 Box<Integer>는 제네릭 클래스 Box<T>에 서로 다른 타입을 대입하여 호출한 것일 뿐, 이 둘은 별개의 클래스를 의미하는 것은 아니다. 이는 마치 매개변수의 값이 다른 메소드 호출, 즉 add(3, 5), add(2, 4)가 서로 다른 메소드를 호출하는 것이 아닌 것과 같다. 

 

컴파일 후에 Box<String>과 Box<Integer>는 이들의 '원시 타입'인 Box로 바뀐다. 즉, 제네릭 타입이 제거된다.

제네릭의 제한

제네릭 클래스 Box의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절하다. 제네릭은 이처럼 인스턴스별로 다르게 동작하도록 하려고 만든 기능이다.

Box<Apple> appleBox = new Box<Apple>(); // OK Apple객체만 저장가능
Box<Grape> grapeBox = new Box<Grape>(); // OK Grape객체만 저장가능

그러나 모든 객체에 대해 동일하게 동작해야하는 static맴버에 타입 변수 T를 사용할 수는 없다. T는 인스턴스변수로 간주되기 때문이다. static맴버는 인스턴스 변수를 참조할 수 없다.

 class Box<T>{
		static T item; // 에러
		
		static int compare (T t1, T t2) { // 에러
		...
		}
		...
}

static맴버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이여야 하기 때문이다. 즉, 'Box<Apple>.item'과 'Box<Grape>.item'이 다른 것이어서는 안된다는 뜻(static맴버는 공유되는 변수)이다. 그리고 제네릭 타입의 배열을 생성하는 것도 허용되지 않는다. 제네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, 'new T[10]'과 같이 배열(객체)을 생성하는 것은 안된다는 뜻이다. 

class Box<T>{
	T[] itemArr; // T타입의 배열을 위한 참조변수
    	...
    T[] toArray(){
    	T[] tmpArr = new T[itemArr.length]; // 에러 제네릭 배열 생성불가
        ...
        return tmpArr;
    }
    	...
}

타입이 결정되지 않은 배열은 생성 가능하지만, 확정되어진 객체, 배열은 생성할 수 없다.

new 연산자 사용시 컴파일 시점에 타입 T가 무슨 타입인지 확실히 알아야 한다. 그런데 위의 코드에 정의된 Box<T>클래스를 컴파일하는 시점에는 T가 어떤 타입이 될지 전혀 알 수 없다. instanceof연산자도 new연산자와 같은 이유로 피연산자로 사용할 수 없다.

꼭 생성이 필요한 경우 new 연산자 대신 'Reflection API'의 newInstance()와 같이 동적으로 객체를 생성하는 메소드로 배열을 생성하거나, Object배열을 생성해서 복사한 다음에 'T[]'로 형변환하는 방법 등을 사용한다.

제한된 제네릭 클래스

타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만, 그래도 여전히 모든 종류의 타입을 지정할 수 있다는 것에는 변함이 없다. 그렇다면, 타입 매개변수 T에 지정할 수  있는 타입의 종류를 제한할 수 있는 방법은 없을까?

FruitBox<Toy> fruitBox = new FruitBox<Toy>();
fruitBox.add(new Toy()); // OK 과일상자에 장난감을 담을 수 있다.

제네릭 타입에 'extends'를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.

class FruitBox<T extends Fruit>{ // Fruit의 자손만 타입으로 지정가능
	ArrayList<T> list = new ArrayList<T>();
    ...
}

여전히 한 종류의 타입만 담을 수 있지만, Fruit클래스의 자손들만 담을 수 있다는 제한이 추가된다.

FruitBox<Apple> appleBox = new FruitBox<Apple>(); // OK
FruitBox<Toy> toyBox = new FruitBox<Toy>(); // 에러 Toy는 Fruit의 자손이 아님

또 add()매개변수의 타입 T도 Fruit와 그 자손 타입이 될 수 있으므로, 아래와 같이 여러 과일을 담을 수 있는 상자가 가능하게 된다.

FruitBox<Fruit> fruitBox = new FruitBox<Apple>();
fruitBox.add(new Apple()); // OK Apple이 Fruit의 자손
fruitBox.add(new Grape()); // OK Grape가 Fruit의 자손

다형성에서 상위클래스(조상타입)의 참조변수로 하위클래스(자손타입)의 객체를 가리킬 수 있는 것처럼, 매개변수화된 타입의 자손 타입도 가능한 것이다. 타입 매개변수 T에 Object를 대입하면, 모든 종류의 객체를 저장할 수 있게 된다.

 

 만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면, 이때도 'extends'를 사용해야한다. 'implements'를 사용하지 않는 다는 점에 주의하자

interface Eatable {...}
class Fruit<T extends Eatable> {...}

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

class Fruit<T extends Fruit & Eatable> {...}

이렇게 하면 FruitBox에는 Fruit의 자손이면서 Eatable을 구현한 클래스만 타입 매개변수 T에 대입될 수 있다.

'Java' 카테고리의 다른 글

Java - 제네릭 타입의 형변환  (0) 2023.04.18
Java - 제네릭 메소드  (0) 2023.04.18
Java - 와일드 카드  (0) 2023.04.17
Java - OOP  (0) 2023.04.16
String과 StringBuffer/StringBuilder  (0) 2023.04.13