클래스 내부에서 데이터 타입을 지정하는 것이 아닌 외부에서 사용자에 의해 데이터 타입이 지정되는 것을 말한다.
한마디로 제네릭은 특정 데이터 타입을 필요에 의해 지정할 수 있도록 일반화하는 것을 의미한다. 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법이다.
보통 제네릭은 아래 그림과 같은 표의 타입들이 많이 사용된다.
반드시 위 표처럼 한글자로 타입을 사용할 필요도 없고 설명과 일치할 필요도 없다. 예를들어 <E>
를 <Ele>
라고 사용해도 전혀 무관하다.
[기본적인 제네릭 타입의 클래스 및 인터페이스 선언]
public class GenericClass <T>{
...
}
public interface GenericInterface <T>{
...
}
T
타입은 해당 블럭{...}
안에서까지 유효하다. 위 코드처럼 제네릭 타입을 하나만 사용하는것 뿐만 아니라 제네릭 타입을 두개로 두는것도 가능하다. (대표적으로 HashMap이 있다.)
[제네릭 타입을 두개 두는 경우]
public class GenericClass <T, K>{
...
}
public interface GenericInterface <T, K>{
...
}
public class HashMap <T, K>{
...
}
이렇게 생성된 제네릭 클래스를 사용할 경우 구체적인 타입을 명시해 주어야 한다.
[제네릭 클래스 사용방법 예시]
public class GenericClass <T, K>{
...
}
public class UsingGeneric {
public static void main(String[] args) {
GenericClass<Integer, String> genericClass = new GenericClass<>();
}
}
T
는 Integer
이 되고, K
는 String
이 된다.
이때 주의해야 할 점이 있는데 타입 파라미터로 명시할 수 있는 것은 Integer
, String
같은 참조 타입(Reference Type)만 가능하다. 즉, int, float, char 같은 primitive type은 올 수 없다.
또한, 참조 타입이 가능하다는것은 사용자 정의 클래스도 타입으로 명시할 수 있다는 것을 의미한다.
[사용자 정의 클래스 사용]
public class Person {
...
}
... // 코드생략
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
}
[제네릭 클래스]
public class Main {
public static void main(String[] args) {
ClassName<String> className1 = new ClassName<>("10");
ClassName<Integer> className2 = new ClassName<>(10);
System.out.println("className1: " + className1.getElement());
System.out.println("className1 Type: " + className1.getElement().getClass().getName());
System.out.println("className2: " + className2.getElement());
System.out.println("className2 Type: " + className2.getElement().getClass().getName());
}
}
class ClassName<E> {
private final E element; // 제네릭 타입 변수
public ClassName(final E element) {
this.element = element;
}
E getElement() { // 제네릭 타입 반환 메서드
return element;
}
}
// 실행결과
className1: 10
className1 Type: java.lang.String
className2: 10
className2 Type: java.lang.Integer
ClassName
객체를 생성할 때 꺽새괄호(<>)안에 타입 파라미터를 지정하는데 className1
의 E
제네릭 타입은 String
으로 className2
의 E
제네릭 타입은 Integer
로 모두 변환된다.
[제네릭 타입을 두개 사용할 경우]
public class Main {
public static void main(String[] args) {
ClassName2<String, Integer> className3 = new ClassName2<>("10", 10);
System.out.println("first: " + className3.getFirst());
System.out.println("K type: " + className3.getFirst().getClass().getName());
System.out.println("second: " + className3.getSecond());
System.out.println("V type: " + className3.getSecond().getClass().getName());
}
}
class ClassName2<K, V> {
private final K first;
private final V second;
public ClassName2(final K first, final V second) {
this.first = first;
this.second = second;
}
K getFirst() { // 제네릭 타입 반환 메서드
return first;
}
V getSecond() { // 제네릭 타입 반환 메서드
return second;
}
}
// 실행결과
first: 10
K type: java.lang.String
second: 10
V type: java.lang.Integer
외부에서 제네릭 클래스를 생성할 때 꺽새괄호(<>) 안에 타입을 파라미터로 보내 제네릭 타입을 지정해 준다.
메소드의 선언부에 타입 변수를 사용한 메서드를 의미한다. 이때 타입 변수의 선언은 메서드 선언부에서 반환 타입 바로 앞에 위치한다.
제네릭 클래스는 클래스 이름 옆에 <E>
라는 제네릭 타입을 붙여 클래스 내에서 사용할 수 있는 E
타입으로 일반화 했다. 이 외에 메서드에 한정한 제네릭도 사용이 가능하다.
[제네릭 메서드 선언방법]
public <T> T genericMethod(T t){
...
}
[접근제어자] [<제네릭타입>] [반환타입] [메소드명] ([제네릭타입] [파라미터]){
... // 생략
}
클래스와는 다르게 반환타입 이전에 제네릭 타입을 선언한다.
public class Main {
public static void main(String[] args) {
System.out.println(className1.genericMethod(5).getClass().getName());
System.out.println(className1.genericMethod("5").getClass().getName());
System.out.println(className1.genericMethod(className2).getClass().getName());
}
}
class ClassName<E> {
private final E element; // 제네릭 타입 변수
public ClassName(final E element) {
this.element = element;
}
E getElement() { // 제네릭 타입 반환 메서드
return element;
}
public <T> T genericMethod(T t) {
return t;
}
}
// 실행결과
java.lang.Integer
java.lang.String
ClassName
위 코드를 보면 genericMethod()
는 파라미터 타입에 따라 T
타입이 결정된다.
즉, 클래스에서 지정한 제네릭 유형과 별도로 메서드에서 독립적으로 제네릭 유형을 선언하여 사용할 수 있다.
이렇게 메서드에서 독립적으로 제네릭 유형을 사용하는 경우는 정적 메서드 를 선언할 때 필요하기 때문이다.
static 키워드가 붙은 정적 메서드는 기본적으로 프로그램 실행시 메모리에 이미 올라가있다. 그렇기 때문에 객체를 생성할 필요없이 클래스 이름을 통해 해당 메서드를 바로 사용할 수 있다.
하지만, 여기서 문제는 static 메서드는 객체생성 이전에 메모리에 올라가기 때문에 클래스로부터 타입을 얻어올 수 없다는데 있다.
[제네릭 클래스와 별도로 독립적인 제네릭 사용]
public class Main {
public static void main(String[] args) {
int result = ClassName3.genericMethod(3);
System.out.println(result);
}
}
class ClassName3<E> {
private final E element;
public ClassName3(final E element) {
this.element = element;
}
E get(){
return element;
}
static <T> T genericMethod(T t) { // 제네릭 메서드
return t;
}
}
// 실행결과
3
위 코드에서 볼 수 있듯이 제네릭 메서드는 제네릭 클래스 타입과 별도로 지정된다.
T와 같은 타입 변수를 사용하여 타입을 제한한다.
특정 범위 내로 좁혀서 제네릭을 사용하고 싶다면 extends
, super
그리고 물음표(?
) 를 사용하면 된다. 물음표는 쉽게 말해 알 수 없는 타입 을 나타낸다.
<K extends T> // T와 T의 자손 타입만 가능 (K는 들어오는 타입으로 지정)
<K super T> // T와 T의 부모 타입만 가능 (K는 들어오는 타입으로 지정)
<? extends T> // T와 T의 자손 타입만 가능
<? super T> // T와 T의 부모 타입만 가능
<?> // 모든 타입 가능. <? extends Object> 랑 동일한 의미
간단히 말하면
extends T
-> 상한 경계? super T
-> 하한 경계<?>
-> 와일드 카드여기서 주의해야 할 부분이 하나 있는데 K extends T
와 ? extends T
이다. K
는 특정 타입으로 지정되지만 ?
는 타입이 지정되지 않았다는 것을 의미한다.
<K extends Number>
/*
* Number와 이를 상속하는 Integer, Double, Long 등의 타입이 지정될 수 있으며
* 객체 혹은 메서드를 호출 할 경우 K는 지정된 타입으로 변환된다.
*/
<? extends T>
/*
* Number와 이를 상속하는 Integer, Double, Long 등의 타입이 지정될 수 있으며
* 객체 혹은 메서드를 호출 할 경우 지정 되는 타입이 없어 타입 참조가 불가능.
*/
따라서 만약 특정 타입의 데이터를 조작해야 할 경우에는 K
같이 특정 제네릭 인수로 지정해주어야 한다.
다음 그림과 같이 클래스들이 상속관계를 갖는다고 가정해보자.
<K extends T>
와 <? extends T>
이 경우 T
타입을 포함한 자식 타입만 가능하다는 의미이다.
<T extends B> // B와 C 타입만 올 수 있다.
<T extends E> // E 타입**텍스트**만 올 수 있다.
<T extends A> // A, B, C, D, E 타입 모두 올 수 있다.
<? extends B> // B와 C타입만 올 수 있다.
<? extends E> // E 타입만 올 수 있다.
<? extends A> // A, B, C, D, E 타입 모두 올 수 있다.
extends
뒤에 오는 타입이 최상위 타입으로 한계가 정해지는 경우이다. 대표적으로 제네릭 클래스에서 수를 표현하는 클래스만 받고 싶은 경우가 있다. Integer
, Long
, Double
과 같은 래퍼 클래스들은 Number
클래스를 상속받는다.
아래 코드처럼 사용할 수 있다.
public class ClassName <K extends Number> {
...
}
이렇게 특정 타입 및 그 하위 타입만 제한 하고 싶을 경우 사용하면 된다. 만약 Number
클래스를 상속받았는데 String
클래스를 사용하려하면 에러(Bound mismatch)가 발생한다.
[제한된 범위 밖의 잘못된 제네릭 타입 사용]
public class LimitedGeneric <K extends Number> {
}
class LimitedGenericMain{
public static void main(String[] args) {
LimitedGeneric<Integer> limitedGeneric1 = new LimitedGeneric<>();
LimitedGeneric<String> limitedGeneric2 = new LimitedGeneric<String>(); // 에러
}
}
// 에러메시지
Type parameter 'java.lang.String' is not within its bound; should extend 'java.lang.Number'
<K super T>
와 <? super T>
이 경우는 T
타입의 부모 타입만 가능하다는 의미이다.
<K super B> // B와 A 타입만 올 수 있다.
<K super E> // E, D 그리고 A 타입이 올 수 있다.
<K super A> // A 타입만 가능.
<? super B> // B와 A 타입만 올 수 있다.
<? super E> // E, D 그리고 A 타입이 올 수 있다.
<? super A> // A 타입만 가능.
super
키워드는 뒤에 오는 타입이 최 하위 타입으로 한계가 정해진다.
대표적으로 업 캐스팅이 될 필요가 있을때 사용한다. 예를 들어 과일 이라는 클래스가 있고 이 클래스를 상속받는 사과, 딸기 클래스가 있다고 가정해보자.
이때 사과와 딸기 둘 다 과일로 보고 자료를 조작해야할 수도 있다. 그럴 때 사과를 과일로 캐스팅 해야 하는데 과일이 상위 타입이므로 업 캐스팅을 해야한다. 이럴 때 사용하는 것이 super
이다.
[업 캐스팅 예제]
public class ClassName <E extends Comparable<? super E>> {
...
}
위와 같은 코드는 우선순위큐(PriorityQueue) 같이 값을 정렬하는 클래스에서 특정 제네릭에 대한 자기 참조 비교를 하고싶을 경우 위와 같은 형식을 취한다.
위 코드에서 E extends Comparable
은 Comparable
을 구현하여 최상위 타입을 정해야 한다는 의미라고 볼 수 있다.
[Comparable<E>
와 <? super E>
]
public class School <E extends Comparable<E>> { ... } // Error가능성 있음
public class School <E extends 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) {
School<Student> a = new School<Student>();
}
}
위 코드처럼 Student 보다 더 큰 범주인 Person 클래스를 둔다고 가정해 보자. 만약, Comparable 구현부인 compareTo에서 Person 타입으로 업캐스팅한다면 정렬이 안되거나 에러가 날 수 있다.
왜냐하면, School 인스턴스를 생성할때 타입 파라미터로 Student를 주지만 Comparable에서는 그보다 상위 타입인 Person으로 비교하기 때문이다.
따라서 E 객체의 상위 타입으로 범위를 한정하기 위해 <? super E>
를 해주어 에러 가능성을 줄일 수 있는 것이다.
<E extends Comparable<? super E>>
전체 의미는 E
자기 자신 및 조상 타입과 비교할 수 있는 E
를 말한다.
<?>
(와일드카드)와일드 카드 <?>
는 <? extends Object>
와 같은 의미이다. Object
는 자바에서 모든 API 및 사용자 클래스의 최상위 타입이다.
public class ClassName{
...
}
public class ClassName extends Object{
...
}
위 두 클래스는 같은 의미라고 볼 수 있다. extends Object
를 묵시적으로 상속받는 것이나 다름 없다. 한마디로 <?>
는 어떤 타입이든 상관 없다는 의미이다.