평소 제네릭을 사용은 했었지만 왜 사용하는 것인지, 어떤 역할인지 이해하고 있지 않은 것 같아 완벽히 이해하는 것을 목표로 정리해 봤습니다.
제네릭이란?
제네릭은 클래스, 메소드에서 사용할 타입을 외부에서 주입받는 것을 말합니다.
우리가 HashMap에서 <String>으로 사용했던 것이 바로 제네릭입니다.
HashMap<String, String> map = new HashMap();
ArrayList<String> arrList = new ArrayList<>();
정의로는 제네릭이 무엇인지 감이 안 오지만 예제를 통해 쉽게 이해할 수 있습니다.
예를 들어, 우리가 숫자 리스트를 저장할 수 있는 ArrayList라는 클래스를 만들었다고 가정하겠습니다.
public class main{
public static void main(String[] args) {
ArrayList arrList = new ArrayList();
arrList.add(100); // int만 가능
arrList.add(200);
arrList.add(300);
}
}
public class ArrayList implements List {
int[] elementData;
public boolean add(int e) { ... } // int
public int set(int index, int e) { ... }
}
기능이 변경되어 String이나 사용자 정의 클래스를 ArrayList에 담을 수 있도록 변경하려면 어떡할까요?
타입 별로 클래스를 만들거나 Object 객체(모든 객체의 조상)를 사용할 수도 있겠지만 비효율적입니다.
이때 필요한 게 제네릭입니다. 제네릭을 이용하면 타입을 클래스 내부에서 지정하는 것이 아니라 외부에서 타입을 지정하게 할 수 있습니다.
클래스 선언 시 제네릭 **<E>**를 통해 외부에서 전달받을 타입을 지정하면 클래스 영역 안에서 타입을 사용할 수 있습니다.
제네릭 클래스는 객체 생성 시 타입을 꼭 전달해야 합니다.
public class main{
public static void main(String[] args) {
ArrayList<int> intList = new ArrayList<int>();
intList.add(100);
ArrayList<String> strList = new ArrayList<String>();
strList.add("aaa");
}
}
public class ArrayList<E> implements List<E> {
public boolean add(E e) { ... } // int e가 아닌 E e
public void set(E e) { ... }
}
제네릭의 선언
우리가 자주 사용하는 HashMap의 실제 클래스 선언 부분을 보면 public class HashMap<K,V>로 K, V 타입을 선언하고 있습니다.
HashMap<Int, String> map = new HashMap(); 으로 HashMap 객체를 생성할 경우 K = Int, V = String 타입이 됩니다.
public class HashMap<K,V> { // 불필요한 부분 생략
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
}
제네릭은 어떤 문자로든 사용할 수 있고 주로 의미를 알 수 있는 영어 1글자 대문자를 사용합니다.
- T : type
- E : Element
- K : key
- V : value
- N : number
ClassName<String> a = new ClassName<String>();
class ClassName<홍길동> { // 에러 X
}
클래스에서 선언된 제네릭은 클래스 영역 어디서든 사용할 수 있습니다.(코드 출처)
// 제네릭 클래스
class ClassName<E> {
private E element; // 제네릭 타입 변수
void set(E element) { // 제네릭 파라미터 메소드
this.element = element;
}
E get() { // 제네릭 타입 반환 메소드
return element;
}
}
class Main {
public static void main(String[] args) {
ClassName<String> a = new ClassName<String>();
ClassName<Integer> b = new ClassName<Integer>();
a.set("10");
b.set(10);
}
}
제네릭을 사용하는 이유
- 재사용성이 증가하며 타입 변환, 타입 검사에 들어가는 노력을 줄일 수 있다.
JDK 1.5에 제네릭이 도입되었고 그 전까지 여러 타입을 사용하는 클래스나 메소드에서 모든 객체의 조상인 Object 타입을 사용했습니다. Object 객체를 반환받을 경우 원하는 타입인지 체크해야하기 때문에 번거럽고, 변환하는 과정에서 에러가 발생할 수 도 있습니다.
제네릭을 사용할 경우 미리 타입이 정해지므로 번거로운 작업을 생략할 수 있습니다. - 클래스, 메소드에서 사용되는 타입 안정성을 높일 수 있다.
제네릭을 이용하면 타입에 조건을 줄 수 있습니다.
타입의 제약
extends와 super를 이용해 제네릭에 사용 가능한 타입에 제약을 줄 수 있습니다.
타입 제약을 이용하여 안정성을 높일 수 있고 컴파일 단계에서 에러를 발견할 수 있습니다.
- <E extends A> : E는 A의 Child만 가능
- <E super A> : E는 A의 Parent만 가능
- <? super A> : A의 Parent만 가능. 타입을 지정하지 않아 참조는 불가능하지만 제약은 줄 수 있음
참고글
'Java' 카테고리의 다른 글
Java 메일 전송 시 권한 에러 해결방법(Sending the email to the following server failed : smtp.gmail.com:465) (0) | 2022.11.01 |
---|