어떤 자료 구조(클레스)를 만들어 배포할 때 지원하는 type(String, Integer 등)이 많아질 수록 구현해야하는 코드가 많아진다. 이를 Generic(제네릭)을 이용해 외부에서 사용자가 지정할 수 있도록 한다.
Generic의 장점
- 제네릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다.
- 클래스 외부에서 타입을 지정하기 때문에 타입을 체크하고 변환할 필요가 없다. 즉, 관리하기 편하다.
- 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다.
타입을 다른 글자로 써도 아무런 문제는 없지만 용도를 표시하기 위해 암묵적으로 아래와 같이 쓴다.
타입 | 설명 |
<T> | Type |
<E> | Element |
<K> | Key |
<V> | Value |
<N> | Number |
클래스 및 인터페이스 선언
public class ClassName <T> { ... }
public Interface InterfaceName <T> { ... }
T 타입은 해당 블럭 { ... } 안에서만 유효하다. 제네릭 타입을 2개로 두어서 매개 변수 타입을 여러 개 입력 받을 수도 있다.
public class ClassName <T, K> { ... }
public Interface InterfaceName <T, K> { ... }
// HashMap의 경우 아래와 같이 선언되어있을 것이다.
public class HashMap <K, V> { ... }
이렇게 생성된 제네릭 클래스의 객체를 생성할 때 구체적인 타입을 명시해준다.
public class ClassName <T, K> { ... }
public class Main {
public static void main(String[] args) {
ClassName<String, Integer> a = new ClassName<String, Integer>();
}
}
T는 String이 되고, K는 Integer가 된다.
주의할 점은 타입 파라미터로 명시할 수 있는 것은 참조 타입(Reference Type) 밖에 올 수 없다. 즉, int double, char 같은 primitive type은 올 수 없다. primitive Type의 경우 Integer, Double 같은 Wrapper Type으로 쓴다. 바꿔 말하면, 참조 타입이 올 수 있다는 것은 사용자가 정의한 클래스도 타입으로 올 수 있다는 것을 의미한다.
public class ClassName <T> { ... }
public class Student { ... }
public class Main {
public static void main(String[] args) {
ClassName<Student> a = new ClassName<Student>();
}
}
제네릭 프로그래밍 예시
class ClassName<K, V> {
private K first; // K 타입(제네릭)
private V second; // V 타입(제네릭)
void set(K first, V second) {
this.first = first;
this.second = second;
}
K getFirst() {
return first;
}
V getSecond() {
return second;
}
<T> T genericMethod(T o){ // 제네릭 메소드
return o;
}
}
class Main {
public static void main(String[] args){
ClassName<String, Integer> a = new ClassName<String, Integer>();
a.set("10", 10);
System.out.println(" K type: " + a.getFirst().getClass().getName());
// K Type : java.lang.String
System.out.println(" V type: " + a.getSecond().getClass().getName());
// V Type : java.lang.Integer
}
}
제네릭 메소드
public <T> T genericMethod(T o) { // 제네릭 메소드
...
}
// 구조
[접근 제어자] <제네릭 타입> [반환 타입] [메서드 명]([제네릭 타입] [파라미터]){
...
}
클래스와 다르게 반환 타입 이전에 <> 제네릭 타입을 선언한다. genericMethod는 파라미터 타입에 따라 T 타입이 결정된다. 즉, 클래스에서 지정한 제네릭 타입과 별도로 메소드에 독립적인 제네릭 타입을 가질 수 있다. 메소드가 독립적으로 제네릭 타입을 갖도록 만든 이유는 static 선언으로 정적 메소드로 만들면 이미 메모리에 올라가서 타입을 얻어올 수 없기 때문이다. (따라서 클래스 이름을 통해 바로 쓸 수 있는 것임.)
받고 싶은 타입을 특정 범위 내로 좁히고 싶다면 extends, super, ?를 사용한다.
<K extends T> // T와 T의 자손 타입만 가능 (K는 들어오는 타입으로 지정됨)
<K super T> // T와 T의 부모 타입만 가능 (K는 들어오는 타입으로 지정됨)
<? extends T> // T와 T의 자손 타입(T를 상속한 클래스)만 가능
<? super T> // T와 T의 부모 타입(T가 상속받은 클래스)만 가능
<?> // 모든 타입 가능
<K extends T>와 <? extends T>의 차이점은 K는 특정 타입으로 지정되지만, ?는 타입이 지정되지 않는다.
<K extends Number>
/*
Number를 상속하는 Integer, Short, Double 등의 타입은 사용 가능하며
객체 혹은 메서드를 호출 할 경우 K는 지정된 타입으로 참조 가능하다.
*/
<? extends Number>
/*
Nubmber를 상속하는 Integer, Short, Double 등의 타입은 사용 가능하며
객체 혹은 메서드를 호출할 경우 지정되는 타입이 없어서 타입 참조를 할 수 없다.
*/
아래와 같은 예제가 있다고 하자.
public class ClassName <E extends Comparable<? super E>> { ... }
<? super E>는 E 객체와 그 부모 타입을 받겠다는 뜻이다.
public class SaltClass <E implements Comparable<? super E>> { ... }
public class Person { ... }
public class Student extends Person implements Comparable<Person> {
@Override
public int compareTo(Person o) { ... };
}
public class Main {
public static void main(String[] args){
SaltClass<Student> a = new SaltClass<Student>();
}
}
java는 다중 상속을 지원하지 않아서 다중 상속을 원하는 경우 interface로 상속받는다. 단, implements한 클래스의 메서드는 반드시 오버라이딩해야한다. interface(class)가 interface(class)를 상속 받을 땐 extends를 사용한다. (출처)
Comparable 인터페이스를 상속받으면 Comparable 인터페이스의 메서드는 반드시 오버라이딩 해야한다. <? super E> 가 아닌 <E>만 사용한다면, a.compareTo를 사용할 때 지정한 Student 타입 보다 상위 타입인 Person 객체들을 비교하면서 예상치 못한 에러가 발생한다. <? super E>를 사용한다면 이러한 문제를 해결할 수 있다.
와일드 카드 <?>
<?>는 어떠한 타입도 받을 수 있다. 데이터가 아닌 '기능'에 관심이 있을 경우 사용한다.
'JAVA' 카테고리의 다른 글
Java 언어 특징 (0) | 2022.04.09 |
---|---|
JAVA Stream map (0) | 2022.03.16 |