Java의 제네릭(generics) 기능은 타입 안전성, 코드 재사용성 및 가독성을 향상시키는 강력한 도구입니다. 올바르게 사용하면 많은 프로그래밍 오류를 예방하고, 더욱 깔끔하고 관리하기 쉬운 코드를 작성할 수 있습니다.
제네릭 장점 및 특징
1. 타입 안전성 확보
목적: 런타임에 발생할 수 있는 잘못된 타입 사용에 의한 오류를 컴파일 시점에 방지합니다.
예시
:
1 2 3
List<String> strings = new ArrayList<>(); strings.add("hello"); // strings.add(1); // 컴파일 오류
2. 타입 캐스팅 최소화
목적: 명시적인 타입 캐스팅을 줄여 코드의 가독성과 유지보수성을 향상시킵니다.
예시
:
1 2 3
List<String> strings = new ArrayList<>(); strings.add("hello"); String value = strings.get(0); // 캐스팅 필요 없음
3. 코드 재사용성 강화
목적: 동일한 코드를 다양한 타입에 대해 사용하여 유연성과 재사용성을 증가시킵니다.
예시
:
1 2 3 4 5
public <T> void printList(List<T> list) { for (T element : list) { System.out.println(element); } }
4. 명확한 API 설계
목적: 사용자가 API를 사용할 때 어떤 타입의 객체를 사용해야 하는지 명확하게 알 수 있도록 합니다.
예시
:
1 2 3
Map<String, Integer> map = new HashMap<>(); map.put("Apple", 10); map.put("Banana", 20);
5. 컴파일 시점의 오류 탐지
목적: 잘못된 타입의 사용을 컴파일 시점에서 쉽게 찾아내어 런타임 오류의 가능성을 줄입니다.
예시
:
1 2
List<Integer> numbers = new ArrayList<>(); // numbers.add("hello"); // 컴파일 오류
제네릭 사용시 주의사항
- 와일드카드 사용: 필요에 따라 와일드카드(
?,<? extends T>,<? super T>)를 사용하여 더 큰 유연성을 제공할 수 있습니다. - 타입 제한: 필요한 경우, 상위 또는 하위 타입을 제한하여 타입 안전성을 강화할 수 있습니다.
- 주의: 제네릭은 런타임에 타입 정보가 소거(erase)되므로 일부 상황에서는 런타임 타입 정보에 의존할 수 없습니다.
Class<?>
Java에서 Class<?>는 Class 클래스의 제네릭 타입입니다. <?>는 Type의 와일드카드(wildcard) 타입을 나타냅니다. 즉, Class<?>는 어떤 종류의 클래스도 참조할 수 있는 타입입니다.
예를 들어, 다음과 같은 코드는 Object 타입의 인스턴스 obj의 클래스를 Class<?> 타입의 변수 c1에 저장합니다.
1
2
Object obj = new Object();
Class<?> c1 = obj.getClass();
c1은 Class<?> 타입이므로, c1은 Object 클래스뿐만 아니라 String, Integer, Date 등 모든 클래스를 참조할 수 있습니다.
Class<?>는 주로 리플렉션(reflection) 기능을 사용하기 위해 사용됩니다. 리플렉션은 클래스나 인스턴스의 정보를 얻거나, 클래스나 인스턴스를 조작하는 기능입니다. Class<?> 타입을 사용하면, 어떤 종류의 클래스든 리플렉션을 통해 정보를 얻거나 조작할 수 있습니다.
다음과 같은 코드는 Class<?>를 사용하여 클래스 이름을 얻는 예입니다.
1
2
Class<?> c1 = String.class;
String className = c1.getName();
이 코드는 String 클래스의 클래스 이름을 className 변수에 저장합니다.
Class<?>는 Java에서 매우 유용한 타입입니다. 리플렉션 기능을 사용하기 위해 반드시 필요한 타입입니다.
리플렉션
리플렉션 기능은 컴파일 타임에 알지 못하는 클래스의 정보를 실행 시간에 동적으로 얻거나 조작할 수 있는 기능입니다.
예를 들어, 다음과 같은 코드는 String 클래스의 정보를 얻는 코드입니다.
1
2
3
4
5
6
7
8
9
10
Class<?> c1 = String.class;
// 클래스 이름을 얻습니다.
String className = c1.getName();
// 클래스의 부모 클래스를 얻습니다.
Class<?> superClass = c1.getSuperclass();
// 클래스의 메서드 목록을 얻습니다.
Method[] methods = c1.getMethods();
이 코드는 String 클래스의 클래스 이름, 부모 클래스, 메서드 목록을 얻습니다.
리플렉션 기능은 다음과 같은 경우에 유용하게 사용할 수 있습니다.
- 런타임에 클래스의 정보를 얻거나 조작해야 하는 경우
- 프레임워크나 라이브러리에서 클래스를 동적으로 처리해야 하는 경우
- 디버깅이나 테스트 목적으로 클래스의 정보를 얻어야 하는 경우
리플렉션 기능은 매우 강력한 기능이지만, 사용에 주의해야 합니다. 리플렉션 기능을 잘못 사용하면 보안상 취약점이 발생하거나 성능이 저하될 수 있습니다.
리플렉션 기능을 사용할 때는 다음과 같은 점에 유의해야 합니다.
- 리플렉션 기능을 사용하면 클래스의 접근 제어자를 무시할 수 있으므로, 보안상 취약점이 발생할 수 있습니다.
- 리플렉션 기능을 사용하면 성능이 저하될 수 있습니다.
- 리플렉션 기능을 사용하면 코드가 복잡해질 수 있습니다.
리플렉션 기능을 사용할 때는 이러한 점에 유의하여 신중하게 사용해야 합니다.
제네릭을 통한 코드 안전성과 효율성
제네릭 타입은 여러 가지 타입이 들어가는 자료형에서 명확한 타입이 명시되지 않아서 발생하는 에러를 최소화하는 데 도움이 됩니다.
예를 들어, 다음과 같은 코드는 List를 사용하여 다양한 종류의 객체를 저장합니다.
1
2
3
4
5
List list = new ArrayList<>();
list.add(new String("Hello"));
list.add(new Integer(10));
list.add(new Date());
이 코드에서 list에 String, Integer, Date 등 다양한 종류의 객체를 저장할 수 있습니다. 그러나, 이 코드에는 다음과 같은 문제가 있습니다.
list에 저장되는 객체의 타입을 확인하기 어렵습니다.list에 저장되는 객체의 메서드를 호출할 때 타입 오류가 발생할 수 있습니다.
다음과 같은 코드는 List<T>를 사용하여 String, Integer, Date 등 다양한 종류의 객체를 저장합니다.
1
2
3
4
5
List<T> list = new ArrayList<>();
list.add("Hello");
list.add(10);
list.add(new Date());
이 코드는 list에 저장되는 객체의 타입을 T로 지정합니다. 따라서, 다음과 같이 list에 저장된 객체의 타입을 확인하거나, 메서드를 호출할 때 타입 오류가 발생하지 않습니다.
1
2
3
4
5
6
T obj = list.get(0);
if (obj instanceof String) {
String str = (String) obj;
System.out.println(str);
}
이 코드는 list의 첫 번째 요소를 obj 변수에 저장하고, obj 변수가 String 타입인지 확인합니다. obj 변수가 String 타입이면, obj 변수를 String 타입으로 형변환하여 str 변수에 저장합니다.
이 코드에서 obj 변수를 String 타입으로 형변환할 수 있는 이유는 list에 저장된 첫 번째 요소의 타입이 String이기 때문입니다. 이는 List<T>를 사용함으로써 list에 저장되는 객체의 타입을 명확하게 지정했기 때문에 가능합니다.
따라서, 제네릭 타입을 사용하면 다음과 같은 장점이 있습니다.
- 타입 오류를 방지할 수 있습니다.
- 코드의 가독성과 유지보수성을 높일 수 있습니다.
- 성능을 향상시킬 수 있습니다.
제네릭 관례
제네릭 타입에 <> 내부 문자는 아무거나 해도 상관 없습니다. 그러나, 관례적으로 하나의 대문자를 사용합니다. 예를 들어, List<T>, Map<K, V>, Set<E>와 같이 사용합니다.
물론, 여러 문자를 사용해도 상관없습니다. 예를 들어, List<MyClass>, Map<String, Integer>, Set<Person>와 같이 사용해도 됩니다.
제네릭 타입에 사용되는 문자는 다음과 같은 조건을 만족해야 합니다.
- ASCII 문자여야 합니다.
- 공백이 아니어야 합니다.
- 예약어여서는 안 됩니다.
따라서, 다음과 같은 문자는 사용할 수 없습니다.
- 공백 문자
- 예약어
- 특수 문자
예를 들어, 다음과 같은 제네릭 타입은 사용할 수 없습니다.
List< >Map<if>Set<@>
제네릭 타입에 사용되는 문자는 개발자의 임의로 지정할 수 있습니다.
-
는 **Type**의 약자로, 어떤 타입이든 지정할 수 있는 제네릭 타입입니다. - <K,V>는 Key와 Value의 약자로, 키와 값으로 구성된 쌍을 저장하는 제네릭 타입입니다.
-
는 **Element**의 약자로, 집합 자료형에 저장되는 요소를 지정하는 제네릭 타입입니다.
예를 들어, List<T>는 T 타입의 요소를 저장하는 리스트입니다. Map<K, V>는 K 타입의 키와 V 타입의 값으로 구성된 쌍을 저장하는 맵입니다. Set<E>는 E 타입의 요소를 저장하는 집합입니다.
다음은 각 제네릭 타입의 예입니다.
-
List<String>: 문자열을 저장하는 리스트List<Integer>: 정수를 저장하는 리스트List<MyClass>:MyClass클래스의 인스턴스를 저장하는 리스트
- <K,V>
Map<String, Integer>: 문자열 키와 정수 값을 저장하는 맵Map<Person, Address>: 사람 키와 주소 값을 저장하는 맵Map<Student, Grade>: 학생 키와 학점 값을 저장하는 맵
-
Set<String>: 문자열을 저장하는 집합Set<Integer>: 정수를 저장하는 집합Set<MyClass>:MyClass클래스의 인스턴스를 저장하는 집합