제네릭(Generic)이란?

  • 클래스에서 사용할 타입을 클래스 외부에서 설정하는 것
  • 클래스 내부 데이터 타입을 인스턴스를 생성할 때 확정하는 것

제네릭의 활용

제네릭 클래스

클래스 생성 시, 내부에서 사용할 필드의 타입을 지정할 수 있습니다. 이를 이용하여 다음과 같이 간단한 List를 구현할 수 있습니다.

// SimpleList 정의
class SimpleList<E> {
private <T> elements;
public SimpleList(int size) { (E[]) elements = new Object[size]; }
...
}

클래스의 인스턴스를 생성할 때 반드시 타입을 명시해주어야 합니다.

// SimpleList 사용
SimpleList<String> sList = new SimpleList<>(100);
sList.add("123");
sList.add("456");
System.out.println(sList.get(0) + sList.get(1)); // "123456"

SimpleList<Integer> iList = new SimpleList<>(100);
iList.add(123);
iList.add(456);
System.out.println(iList.get(0) + iList.get(1)); // 579

제네릭 인터페이스

// 제네릭 인터페이스 정의
interface GnrInterface<T1, T2> {
T1 doSomething(T2 t);
T2 doSomething2(T1 t);
}

제네릭 인터페이스를 이용하여 클래스를 구현할 때 타입을 명시해 주어야 합니다.

// 제네릭 인터페이스 구현
class GnrInterfaceImpl implements GnrInterface<String, Integer> {

@Override
public String doSomething(Integer t) {
return t.toString();
}

@Override
public Integer doSomething2(String t) {
return Integer.parseInt(t);
}
}

제네릭 메서드

제네릭 메서드는 매개 변수 타입 또는 반환 타입으로 타입 파라미터를 갖는 메소드를 말합니다.
제네릭 메서드 선언 시, 반드시 반환 타입 앞에 사용되는 타입 파라미터를 명시해 주어여 합니다.

public <T> int genericMethod(List<T> list);
public <T> List<T> genericMethod(int n);
public <T> List<T> genericMethod(List<T> list);
public <T1, T2> List<T1> genericMethod(List<T2> list);

제네릭의 장점과 사용하는 이유

  1. 타입 안정성(type-safe)을 제공한다.
    • 코드를 작성할 때 사용할 데이터 타입을 분명하게 명시해주어 컴파일 시의 타입 체크(compile-time type check) 가 가능하여 에러를 사전에 방지할 수 있습니다.
    • type-safe: Runtime 시 타입 체크로 인하여 예측 불가능한 문제가 발생하지 않음을 보장하는 것
  2. 타입 체크와 형변환을 생략할 수 있으므로(컴퍼일러가 해주기 때문에) 코드가 명확하고 간결해 집니다.
  3. 타입의 종류만 바꾸면 되는 로직일 경우, 코드 재활용이 가능합니다.

1. 타입의 안정성 제공

컴파일러는 제네릭 코드에 강한 타입 체킹을 적용해서, 코드가 타입 안전을 위반하는 경우 오류를 발생시킵니다.
런타임 오류는 프로그램의 안정성을 낮출 뿐러더 컴파일 오류에 비해 비교적 발견이 어렵습니다.

// 제네릭 미사용
List list = new ArrayList<>();
list.add("hello");
list.add(" generic");
list.add(100); // *정수 추가 가능
String s1 = (String) list.get(0) + (String) list.get(1); // "hello generic"
String s2 = (String) list.get(0) + (String) list.get(2); // [runtime error!]

// 제네릭 사용
List<String> list = new ArrayList<>(); // 타입 명시
list.add("hello");
list.add(" generic");
list.add(100); // [compile error!] *정수 추가 불가능

2. 코드의 간결화

타입 체크가 명확해지며, 해당 인스턴스를 사용할 때 일일히 형변환을 할 필요가 없습니다.

// 제네릭 미사용
List list = new ArrayList();
list.add("hello");
list.add("generic");
System.out.println((String) list.get(0)); // casting
System.out.println((String) list.get(1)); // casting

// 제네릭 사용
List<String> list = new ArrayList<>();
list.add("hello");
list.add("generic");
System.out.println(list.get(0)); // no cast
System.out.println(list.get(1)); // no cast

3. 코드의 재활용

제네릭을 사용하는 대표적인 예인 Collection은 타입만 명시해 준다면 같은 클래스를 이용하여 다른 타입을 다루는 인스턴스를 따로 생성할 수 있습니다.

List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

하지만 만약 제네릭이 없다면 다음과 같이 각각의 클래스를 따로 구현해 주어야 할것입니다.

IntegerList intList = new IntegerList();
StringList strList = new StringList();

그리고 그 클래스들의 내부 동작은 유사할 것이고 코드의 중복도 많을 것이라고 예상할 수 있습니다.

class IntegerList {
...
}

class strList {
...
}

타입 명명 관례

  • E - Element
  • K - Key
  • N - Number
  • T - Type
  • V - Value

Reference