본문 바로가기

프로그래밍언어/JAVA

[JAVA] Generics

반응형

1. 지네릭스

지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하여 객체의 타입 안정성을 높이고 형변환의 번거로움을 줄여서 코드를 간결하게 유지할 수 있다.

 

간단하게 얘기하여 ArrayList 와 같은 컬렉션 클래스에 들어갈 수 있는 객체의 종류를 미리 명시해두어 번거로운 형변환을 줄여준다.

 

 

2. 지네릭 클래스의 선언

 

지네릭 타입은 클래스와 메서드에 선언할 수 있는데, 다음과 같이 사용할 수 있다.

 

class Box<T> {
    T element;

    void setElement(T element) {
	    self.element = element;
    }

    T getElement() {
	    return element;
    }
}

 

위의 예제에서는 지네릭 타입으로 T 를 사용하였는데 이는 type variable 에서 따온 글자이다. Map<K, V> 나 ArrayList<E> 와 같이 다른 기호를 사용하여도 상관없다. 이들은 글자만 다를 뿐 임의의 참조형 타입을 의미한다는 것은 모두 같다.

 

실제 지네릭 타입의 객체를 생성할 때는 참조변수의 자리에 실제로 사용할 타입을 지정하여 선언해주면 된다.

 

Box<String> box = new Box<String>(); // 타입 T 대신에 String 을 지정
box.setElement(123); // String 타입이 아니기 때문에 error 발생
box.setElement("Hello");
String element = box.getElement(); // String 으로 지네릭 타입을 선언했기 때문에 따로 형변환이 필요없다.

 

- 지네릭스 용어

 

class Box<T> { ... }

/**
Box<T> : 지네릭 클래스. T의 Box, 또는 T Box 라고 읽는다.
T : 타입 변수 또는 타입 매개변수 (T는 타입 문자)
Box : 원시 타입 (raw type)
**/

Box<String> b = new Box<String>();
// String : 대입된 타입, 매개변수화된 타입. parameterized type

 

- 지네릭스 제한

 

지네릭 클래스의 객체를 생성할 때 각 객체별로 다른 타입을 지정해줄 수 있다. 그러나 static 멤버의 경우 모든 객체에서 동일하게 동작해야 하기 때문에 타입 변수를 사용하여 선언할 수 없다.

 

그리고 new T[10]; 과 같이 지네릭 타입의 배열을 생성하는 것도 허용되지 않는다. 그 이유는 new 연산자 때문인데., 이 연산자는 컴파일 시점에 타입 T가 어떤 타입인지 정확히 알아야 한다. 그러나 지네릭스 타입은 컴파일 시에 정확한 타입을 파악할 수 없기 때문에 new 연산자와 함께 사용할 수 없다. 이와 동일한 이유로 instanceof 연산자도 지네릭 타입을 사용할 수 없다.

 

 

3. 지네릭 클래스의 객체 생성과 사용

 

지네릭 클래스를 사용하여 객체를 생성할 때는 참조변수와 생성자에 대입된 타입이 일치해야 한다. 일치하지 않는 경우 에러가 발생한다.

 

List<Integer> a = new List<Integer>();
List<Integer> b = new List<String>(); // error 발생

 

지네릭 클래스 타입은 상속관계에 있는 클래스들을 사용하는 경우 문제가 되지 않는다.

 

// Parent 가 조상 클래스, Child 가 자손 클래스인 경우
Parent<Integer> a = new Child<Integer>();

 

JDK 1.7 부터는 추정이 가능한 경우 타입을 생략할 수 있게 되었다. 참조 변수에 선언된 타입으로 생성자의 타입이 결정되기 때문에 생성자에서 반복해서 타입을 지정해주지 않아도 된다.

 

List<String> a = new List<String>();
Lsit<String> b = new List<>(); // String 타입으로 지정해주지 않아도 자동으로 지네릭스 타입이 String 이 된다.

 

 

4. 제한된 지네릭 클래스

 

지네릭 클래스를 선언 시에 extends 키워드를 사용하면 지네릭 타입에 대입할 수 있는 타입의 종류를 제한할 수 있다. 아래와 같이 extends 키워드 뒤에 특정 타입 클래스를 선언하면 해당 타입의 자손 클래스들만 대입할 수 있게 제한된다.

 

// SomeType 의 자손 타입만 T 에 대입할 수 있다.
class GenericExample<T extends SomeType> {
	...
}

 

만약 클래스 타입이 아니라 인터페이스를 구현해야 하는 경우에도 implements 가 아니라 extends 를 사용하면 된다.

만약 여러 타입을 선언해야 하는 경우 & 기호를 사용하여 연결할 수 있다.

 

interface NewInterface()

// NewInterface 인터페이스를 구현한 클래스만 타입 매개변수 T 에 대입할 수 있다.
class GenericExample1<T extends NewInterface> { ... }

// NewClass 클래스의 자손 타입이자 NewInterface 인터페이스를 구현한 클래스 타입만 매개변수 T 에 대입할 수 있다.
class GenericExample2<T extends NewClass & NewInterface> { ... }

 

 

5. 와일드 카드

 

지네릭 타입을 고정해 놓으면, 지정한 타입을 제외한 다른 타입은 대입될 수 없다. 만약 함수의 매개변수를 다양하게 받게 하기 위해서 지네릭 타입만 변경하여 오버로딩 하게되면 컴파일 에러가 발생하게 된다. 지네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거하는데, 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기 때문이다.

 

이러한 문제를 해결하기 위해 고안된 것이 와일드 카드이다. 와일드 카드는 기호 "?" 로 표현하는데, 와일드 카드는 어떠한 타입도 될 수 있다. 만약 extends super 를 사용한다면 상한과 하한을 제한할 수 있다.

 

<? extends T> 와일드 카드의 상한 제한. T 타입과 그 자손들만 가능
<? super T> 와일드 카드의 하한 제한. T 타입과 그 조상들만 가능
<?> 제한 없이 모든 타입이 대입 가능

 

 

6. 지네릭 메서드

 

메서드의 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라고 한다. Collections.sort() 메서드가 지네릭 메서드인데 다음과 같이 선언되어 있다.

 

static <T> void sort(List<T> list, Comparator<? super T> c)

 

메서드에 선언된 지네릭 타입은 지역 변수를 선언한 것과 같다고 생각하면 이해하기 쉽다. 이 타입의 매개변수는 메서드 내에서만 지역적으로 사용되기 때문에 메서드가 static 이건 아니건 상관이 없다.

 

 

7. 지네릭 타입의 형변환

 

generic 타입과 primitive 타입 간의 형변환은 가능하다. 다만 경고가 발생한다. 또한 generic 타입에서 <? extrends Object> 로의 형변환 또한 가능하다. 하지만 이미 대입된 지네릭 타입들 간의 형변환은 불가능하다.

 

Box<Object> objBox = new Box<String>(); // 에러 발생

 

Optional 클래스를 통해 지네릭 타입의 형변환 예시를 확인할 수 있다.

Optional 클래스는 EMPTY 변수에 empty 객체를 생성해서 저장한다. 그러다가 empty() 를 호출하게 되면 이때 대입된 타입으로 형변환하여 객체를 반환한다.

순서대로 표현하면 다음과 같다.

 

// <?> 는 <? extends Object> 를 의미하며,
// <> 는 <Object> 를 의미한다.
// Optional<Object> 가 아닌 Optional<?> 로 선언한 이유는 Object<T> 로 형변환이 가능하기 때문이다.
Optional<?> EMPTY = new Optional<>();
-> Optional<? extends Object> EMPTY = new Optional<>();
-> Optional<? extends Object> EMPTY = new Optional<Object>();

 

 

8. 지네릭 타입의 제거

 

컴파일러는 지네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다. 그리고 지네릭 타입을 제거한다. 즉 컴파일된 파일 (*.class) 에는 지네릭 타입에 대한 정보가 없는 것이다.

 

컴파일 된 파일에 지네릭 타입에 대한 정보를 제외하는 이유는 지네릭 타입 이전의 JDK1.5 이전의 코드와의 호환성을 위해서이다.

반응형

'프로그래밍언어 > JAVA' 카테고리의 다른 글

[JAVA] Annotation  (0) 2022.02.23
[JAVA] Enums  (0) 2022.02.21
[JAVA] Collections  (0) 2022.02.04
[JAVA] Properties  (0) 2022.02.04
[JAVA] TreeMap  (0) 2022.02.04