java [9] Generics

lsy·2022년 10월 25일
0

자바

목록 보기
11/14
post-custom-banner

제네릭(Generic)이란?

제네릭은 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시 타입 체크를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.

제네릭 클래스의 선언

제네릭 타입은 클래스메서드에 선언할 수 있는데 먼저 클래스에 선언하는 타입부터 알아본다.

class Box {
	Object item;
    void setItem(Object item) {
    	this.item = item;
    }
    Object getItem() {
    	return item;
    }
}

Box 클래스는 다음과 같이 제네릭 클래스로 변경할 수 있다.

class Box<T> {
	T item;
    void setItem(T item) {
    	this.item = item;
    }
    T getItem() {
    	return item;
    }
}

변수 T타입 변수라고 한다. Type의 첫 글자에서 따온 것이며 다른 것을 사용해도 된다. 보통 상황에 맞게 의미 있는 문자를 선택하여 사용한다.

위 제네릭 클래스의 객체를 생성할 때에는 타입 T 대신 사용될 실제 타입을 지정해주어야 한다.

Box<String> b = new Box<String>();

또한 JDK1.7부터는 다음과 같이 생략도 가능하다.

Box<String> b = new Box<>();

이렇게 선언하고 나면 제네릭 클래스 Box<T>는 다음과 같이 정의된 것과 같다.

class Box {
	String item;
    void setItem(String item) {
    	this.item = item;
    }
    String getItem() {
    	return item;
    }
}

제네릭의 제한 조건

제네릭은 객체를 생성할 때 객체별로 다른 타입을 지정할 수 있다.

Box<String> b = new Box<>();
Box<Integer> b = new Box<>();

하지만 타입 변수 T는 인스턴스 변수로 간주되기 때문에, static 멤버에는 타입 변수 T를 사용할 수 없다. static멤버는 인스턴스 변수를 참조할 수 없기 때문이다.

class Box<T> {
	static T item; // 에러
    static void print(T t) {...} // 에러
}

또한 제네릭 타입의 배열의 참조변수를 생성하는 건 가능하지만, 배열 인스턴스를 생성하는 것은 허용하지 않는다. 왜냐하면 new 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 하지만 Box<T> 클래스를 컴파일하는 시점에서는 T가 어떤 타입이 될지 알 수 없기 때문이다.

class Box<T> {
	T[] arr; // 허용
    Box() {
    	arr = new T[10]; // 허용하지 않음
    }
}

꼭 제네릭 배열을 생성해야할 필요가 있을 때에는 Reflection API의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object 배열을 생성한 뒤 T[]로 형변환하는 방법을 사용한다.

제네릭 클래스를 제한하기

제네릭에는 타입 변수를 제한할 수 있는 키워드가 존재한다. extendssuper를 사용한다.

extends상한 제한이라고 하며 지정한 클래스를 포함한 자식 클래스들만 올 수 있게 한다.
super하한 제한이라고 하며 지정한 클래스를 포함한 조상 클래스들만 올 수 있게 한다.

제네릭 클래스를 만들때에는 오직 extends 키워드만 사용 가능하며 super사용 불가능하다. 이유는 여기의 글을 참조바란다.

제네릭 클래스에서는 다음과 같이 사용한다.

class Box<T extends Number> {
	T item;
    void setItem(T item) {
    	this.item = item;
    }
    T getItem() {
    	return item;
    }
}

이렇게 사용하면, T에는 Number 클래스를 포함한 자식들만 올 수 있게 된다. 즉 Number, Integer, Double 같은 클래스만 올 수 있다.

만약 Number의 자식이면서, Serializable 인터페이스도 구현해야 한다면 다음과 같이 & 기호로 연결한다.

class Box<T extends Number & Serializable> {
	T item;
    void setItem(T item) {
    	this.item = item;
    }
    T getItem() {
    	return item;
    }
}

와일드 카드

와일드 카드는 메서드의 인자로 제네릭 타입의 클래스 객체를 받을 때 사용할 수 있으며 어떠한 타입도 될 수 있다는 것을 나타낸다. 제네릭 클래스를 만들 때는 사용하지 못한다.

static void print(List<Number> list) {
	for(Number n : list) {
    	System.out.println(n);
    }
}

내가 원하는 건 Number 클래스를 상속 받은 Integer, Double 등의 리스트안의 요소를 출력하는 것이다. 하지만 위와 같이 코드를 작성한다면 print()메서드의 인자로 오직 List<Number> 밖에 들어오지 못하게 된다. 따라서 이 때 와일드 카드를 사용한다.

static void print(List<? extends Number> list) {
	for(Number n : list) {
    	System.out.println(n);
    }
}

이렇게 작성하면 이제 원하는 행위가 가능해진다. Number 클래스를 포함한 자식 클래스들이 모두 올 수 있게 되며 List<Integer>도 올 수 있고, List<Double>도 올 수 있게 된다.

static void print(List<? super Number> list) {
	for(Number n : list) {
    	System.out.println(n);
    }
}

적절한 예시는 아니지만, super를 이용하여 Number를 포함한 조상 클래스들만 올 수 있게 할 수도 있다.

static void print(List<?> list) {
	for(Number n : list) {
    	System.out.println(n);
    }
}
static void print(List<? extends Object> list) {
	for(Number n : list) {
    	System.out.println(n);
    }
}

또한 위 두 개의 선언은 똑같은 의미다. 모든 타입이 올 수 있게 한다. 다만 조금 다른 부분이 있는데, 여기를 참조바란다.

제네릭 메서드의 선언

메서드의 선언부에 제네릭 타입이 선언된 메서드를 제네릭 메서드라고 한다.

public <T> void print() {
	T item;
    ...
}
public <T> void print(List<T> list) {
	...
}

제네릭 클래스와 타입 변수가 같아도 전혀 별개의 것으로 취급하니 주의해야한다.

class Box<T> {
	
    // 이 T와
	T item;
    
    // 이 T는 다름!!
    public <T> void print(List<T> list) {
		...
	}
}

또한 아까 static에서는 타입 변수가 선언이 불가능했지만, 메서드에서의 타입 선언은 지역 변수를 선언한 것과 같기 때문에 static 메서드에서도 선언이 가능하다. 즉 메서드가 static이건 말건 상관하지 않는다.

// 둘 다 가능
public <T> void print1(List<T> list) {
	...
}
public static <T> void print2(List<T> list) {
	...
}

앞서 나왔던 와일드 카드를 이용한 메서드를 제네릭 메서드로 바꾸면 다음과 같이 바꿀 수 있다.

static void print(List<? extends Number> list) {
	for(Number n : list) {
    	System.out.println(n);
    }
}
static <T extends Number> void print(List<T> list) {
	for(Number n : list) {
    	System.out.println(n);
    }
}

얼핏 보면 같은 메서드지만, 전자는 타입을 사용할 수 없고 후자는 타입 T를 이용해 참조 변수를 생성하는 등의 행위가 가능하다. 그래서 다음과 같이 사용한다.

와일드 카드는 타입에는 관심이 없고, 행위 자체에만 관심 있을 때 사용한다.
제네릭 메서드는 타입에 관심이 있고 타입을 이용해 무언가 행위를 해야할 때 사용한다.

제네릭 메서드에서는 super 키워드도 물론 사용 가능하다.

static <T super Number> void print(List<T> list) {
	for(Number n : list) {
    	System.out.println(n);
    }
}
profile
server를 공부하고 있습니다.
post-custom-banner

0개의 댓글