JDK1.5
에 처음 도입되었습니다.
다양한 타입의 객체들을 다루는 메서드나 클래스에
컴파일 시의 타입 체크
를 해주는 기능
- Generics add stability to your code by making more of your bugs detectable at compile time. – Oracle Javadoc
Type Checking(자료형 검사)는 자료형의 제약 조건을 지키는 지 검증하는 것
좀 더 깊게 보자면, Statkc type cheking 과 Dynamic type checking이 있는데,
자료형 검사를 컴파일 시에 하는 가 실행 시간에 하는 가에 따른 차이입니다.
하지만 저는 자바를 사용하므로 정적 타입 체크만 알아보겠습니다.
변수를 만들 때 최초에 자료형을 초기화해주고 따로 형변환(Type Casting)을 하지 않는 이상 그 자료형은 고정됩니다.
자료형 검사를 컴파일 시에 하므로 자료형으로 인한 오류를 쉽게 잡아낼 수 있습니다.
List<String> stringList = new ArrayList<>();
stringList.add(1); // error
stringList.add('이것은되는가?'); // error
stringList.add("이것이 의도한 타입");
위의 코드를 실행하면 Compile Error
가 발생하며 컴파일 시에 다음과 같이 오류가 나게 됩니다.
변수명.getClass().getName()
String str = "ABC"; // String
Integer integer = 123;
int i = 123; // int
List<String> list = new ArrayList<>(); // ArrayList
System.out.println(str.getClass().getName());
System.out.println(integer.getClass().getName());
System.out.println(list.getClass().getName());
System.out.println(i.getClass().getName()); // error
다음과 같이 타입을 확인할 수 있습니다.
추가적으로 int
는 primitive type
이므로 reference type
이 아니기 때문에 당연히 확인할 수 없습니다.
본격적으로 제네릭에 대해 알아보겠습니다.
혹시 Object 클래스
를 사용하면 굳이 제네릭을 사용할 필요는 없지 않을까요?
자바의 모든 클래스는 Object 클래스를 상속받기 때문에 Object 타입으로 받게 된다면 어떤 타입이든 받을 수 있습니다.
public class ObjectArrayList {
private int size;
private Object[] elementData = new Object[5];
public void add(Object value) {
elementData[size++] = value;
}
public Object get(int index) {
return elementData[index];
}
public static void main(String[] args) {
ObjectArrayList list = new ObjectArrayList();
list.add(10);
list.add(100);
Integer value1 = (Integer) list.get(0);
Integer value2 = (Integer) list.get(1);
System.out.println(value1 + value2);
}
}
Object로 ArrayList를 만드는 다음의 코드를 실행하면 오류 없이 110이라는 값이 나오게 됩니다.
add() 메서드
가 파라미터로 Object
를 받기 때문에 어떤 데이터 탕칩이든 받을 수 있습니다. 따라서 get() 메서드
사용 시 형변환만 잘 시켜준다면 어떤 데이터 타입이든 저장할 수 있습니다.
public static void main(String[] args) {
ObjectArrayList list = new ObjectArrayList();
list.add("10");
list.add("100");
Integer value1 = (Integer) list.get(0);
Integer value2 = (Integer) list.get(1);
System.out.println(value1 + value2);
}
add() 메서드에 넣는 데이터 타입을 String
으로 바꾸어 실행해본다면 컴파일까지는 문제가 없지만,
Runtime Error
가 발생하며 다음과 같은 오류가 나오게 됩니다.
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
at techtalk.ObjectArrayList.main(ObjectArrayList.java:21)
java.lang.ClassCastException
이 발생하는데, 즉, 타입 캐스팅이 잘못됐다는 오류 메세지입니다. String
을 넣고 Integer
로 형변환했기 때문에 발생한 오류입니다.
가장 큰 문제점은
Compile Error
가 아닌,Runtime Error
가 발생한다는 것입니다.
이를 해결하기 위해 각각의 Type을 가질 수 있는 ArrayList를 만든다면 코드의 중복이 생기기 시작합니다. add()
와 get()
메서드는 같은 역할을 하지만 파라미터 타입과 반환 타입이 달라 인터페이스나 상속으로 해결할 수도 없습니다.
class GenericArrayList<T> {
private int size;
private Object[] elementData = new Object[5];
public void add(T value) {
elementData[size++] = value;
}
public T get(int index) {
return (T) elementData[index];
}
public static void main(String[] args) {
GenericArrayList<Integer> genericIntList = new GenericArrayList<>();
genericIntList.add(10);
genericIntList.add(100);
int value1 = genericIntList.get(0);
int value2 = genericIntList.get(1);
System.out.println(value1 + value2);
String value = genericIntList.get(0); // Compile Error
}
<T>
가 바로 제네릭입니다. GenericArrayList 객체를 생성할 때 타입을 지정하면, 생성되는 객체 안에 T
의 위치에 지정한 타입이 들어가는 것으로 컴파일러
가 인식합니다. 정확하게는 Raw 타입
으로 사용하는데 컴파일러에 의해 필요한 곳에 형변환 코드가 추가된 것입니다.
ex) List<String>
을 List
로만 사용하는 것
이전에 Runtime E별도의 형변환 없이 지정한 타입과 다른 타입의 참조변수를 선언하면 이전에
Runtime Error가 나던 것과 달리
Compile Error`가 발생하는 것이 중요합니다.
맨 아래 줄을 실행 시 다음과 같은 에러가 발생합니다.
java: incompatible types: java.lang.Integer cannot be converted to java.lang.String
제네릭 클래스는 다음과 같은 형식으로 정의됩니다.
class name <T1, T2, ..., Tn>
<>
로 구분 된 타입 매개 변수는 클래스 이름 뒤에 오며, 객체가 생성될 때 타입 파라미터를 받는 부분입니다.
타입 파라미터와 일반 클래스 또는 인터페이스 이름의 차이를 구분하기 위해 정해진 규칙에 따라 타입 파라미터는 단일 대문자를 사용합니다.
일반적으로 사용되는 타입 매개변수 이름입니다.
E - element
K - key
N - number
T - type
V - value
S, U, V - 2nd, 3rd, 4th declared types
public interface Map<K, V>{
Set<Map.Entry<K, V>> etrySet();
}
public interface List<E>{
Iterator<E> iterator();
public interface BiFunction<T, S, R>{ // R: return type
R ally(T t, S s);
}
사실 아무 이름이나 지정해도 컴파일하는 데 문제가 없지만, 네이밍은 약속이므로 컨벤션을 꼭 지키면서 사용합시다!
(돌고 돌아 본인이 편해집니다.)
제네릭 타입에서 타입 인자로 사용할 수 있는 타입을 제한하려는 경우가 있을 수 있습니다.
바운디드 타입은 특정 타입의 서브 타입
으로 제한합니다.
클래스나 인터페이스를 설계할 때, 가장 흔하게 사용하는 방식입니다.
바운디트 타입 파라미터를 선언하려면 타입 파라미터의 이름, extend
, 상위 바운드를 나열합니다.
<T extends UpperBound>
여기에서 extends
는 implements
의 기능까지 포함하기 때문에 상위 바운드는 인터페이스
가 될 수 있습니다.
여러 개의 상위 바운드를 가질 수도 있습니다.
<T extends B1 & B2 & B3>
만약 여러 개의 상위 바운드 중에 클래스
가 있다면 해당 상위 바운드가 가장 앞에 와야 합니다.
그렇지 않을 시 Compile Error
가 발생합니다.
<T extends ClassA * InterfaceA & InterfaceB>
유연하게는 만들고 싶고, 모두 허용(타입에 대해)하자니 명세상 와서는 안 되는 타입도 들어와지고...
적당히 유연하게
만들고 싶을 때가 많을 것 같습니다.
예를 들어, 아래와 같이 숫자에 대해서만 작동하는 메서드는 Number
또는 해당 하위 클래스의 인스턴스만 허용하려 할 수 있습니다.
public class BoundedType <T extends Number> {
public void set(T value) {}
public static void main(String[] args) {
BoundedType<Integer> boundedType = new BoundedType<>();
boundedType.set("HI");
}
}
이 때, boundedType.set("HI");
부분에서 Compile Error
가 발생합니다.
BoundType 클래스는 타입 파라미터로 <T extends Number>
를 선언하고 있기 때문에
Number
의 서브 타입만 허용하게 됩니다.
Integer
는 Number
의 서브 타입이기 때문에 선언이 가능하지만, set()
메서드 인자로 문자열을 입력했기 때문에 컴파일 에러가 발생하게 됩니다.
추가적으로, Bounded Type
은 extends
를 사용하기 때문에 상위 바운드에 해당하는 클래스의 메서드를 코드에서 사용할 수 있다는 특징이 있습니다.
제네릭 타입 코드에서 와일드 카드
라고 하는 물음표(?
)는 알 수 없는 유형을 나타냅니다. 와일드 카드는 다음과 같은 다양한 상황에서 사용할 수 있습니다.
매개변수, 필드 또는 지역 변수의 타입 그리고 때로는 리텁 타입으로도 사용됩니다.
와일드 타드는 제네릭 메서드 호출, 제네릭 클래스 인스턴스 생성 또는 슈퍼 타입에 대한 유형 인수로는 사용되지 않습니다.
Upper Bounded Wildcards
를 사용하여 바운디드 타입의 상위 제한을 완화할 수 있습니다.
<? extends UpperBound>
이런 제네릭 타입은 UpperBound 클래스 또는 인터페이스의 하위 타입과 매칭될 수 있습니다.
예를 들어, List<Number>
은 List<Integer>
의 상위 클래스가 아닙니다. 따라서 List<Number>
를 파라미터로 가지는 메서드에 List<Integer>
를 인자로 호출하면 다음과 같은 Compile Error
가 발생하게 됩니다.
public class UpperBoundedWildCard {
public static double sumOfList(List<Number> list) {
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}
public static void main(String[] args) {
List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li)); // Compile Error
}
}
java.util.List<java.lang.Integer> cannot be converted to java.util.List<java.lang.Number>
위와 같은 제한을 완화하기 위해 Upper Bounded Wildcards
를 사용할 수 있습니다.
List<Integer>도 메서드 인자로 사용할 수 윘도록 와일드 카드를 추가하는 것인데,
List<? extends Number>는
List`보다 덜 제한적입니다.
public class UpperBoundedWildCard {
/**
Upper Bounded Wildcards
*/
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}
public static void main(String[] args) {
List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));
}
}
아래의 코드는 같은 기능을 하는 메서드를 와일드 카드 없이 구현한 것입니다.
public class UpperBoundedWildCard {
public static <T extends Number> double sumOfList(List<T> list) {
double s = 0.0;
for (T n : list)
s += n.doubleValue();
return s;
}
public static void main(String[] args) {
List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));
}
}
두 방식의 차이로
public static double sumOfList(List<? extends Number> list)
: 와일드 카드는 참조가 불가능하기 때문에 메서드에서 참조될 수 없습니다.
public static <T extends Number> double sumOfList(List<T> list)
: T elem;
처럼 메서드 내에서 타입을 참조하여 사용할 수 있습니다.
List<?>
와 같은 형태로 물음표<?>
만 가지고 정의됩니다. 내부적으로 Object
로 정의되어 사용되고 모든 타입의 인자를 받을 수 있습니다. 타입 파라미터에 의존하지 않는 메서드만 사용하거나 Object 메서드에서 제공하는 기능으로 충분할 경우에 사용합니다.
public class UnBoundedWildCard {
public static void printList(List<?> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
public static void main(String[] args) {
List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
}
}
다만, List<?>
가 List<Object>
와 동일하게 작동하는 것은 아닙니다.
List<Object>
에는 어떤 객체든 담을 수 있지만,
List<?>
에는 오직 null
만 담을 수 있습니다.
Lower Bounded Wildcards
를 사용하여 제네틱 타입을 특정 타입의 상위 클래스로 제한할 수 있습니다.
<? super LowerBound>
public static void addNumToList(List<Integer> list) {
for (int i = 1; i <= 10; i ++) {
list.add (i);
}
}
위의 메서드를 main에서 실행하게 되면, 다음과 같은 Compile Error
가 발생하게 됩니다.
java.util.List<java.lang.Number> cannot be converted to java.util.List<java.lang.Integer>
public class LowerBoundedWildCard {
public static void addNumToList(List<? super Integer> list) {
for (int i = 1; i <= 10; i ++) {
list.add (i);
}
}
public static void main(String[] args) {
List<Number> li = Arrays.asList(1, 2, 3);
addNumToList(li);
}
}
위와 같은 제한을 완화하기 위해 Lower Bounded Wildcards
를 사용할 수 있습니다.
List<Number>도 메서드 인자로 사용할 수 윘도록 와일드 카드를 추가하는 것인데,
List<? super Integer>는
List`보다 덜 제한적입니다.
Helper Methods
를 이용하여 컴파일러가 와일드 카드 타입을 유추할 수 있도록 도와주는 방식을 Wild Card Capture
라고 합니다.
컴파일러는 기본적으로 List<?>
에 대해 List<Object>
로 처리하려고 하며 set()
메서드에 엘리먼트 타입을 컴파일 타임에 확인할 수 없기 때문에 Compile Error
가 발생합니다.
public class WildCardCapture {
static void foo (List<?> i) {
// Compile Error
i.set(0, i.get(2));
}
public static void main(String[] args) {
List<Integer> li = Arrays.asList(1,2,3);
System.out.println(li);
foo(li);
System.out.println(li);
}
}
아래와 같이 헬퍼 메서드
를 추가하여 컴파일러가 와일드 카드 타입을 추론할 수 있게 만듭니다.
public class WildCardCapture {
static void foo (List<?> i) {
originalMethodNameHelper(i);
}
private static <T> void originalMethodNameHelper(List<T> i) {
i.set(0, i.get(2));
}
public static void main(String[] args) {
List<Integer> li = Arrays.asList(1,2,3);
System.out.println(li);
foo(li);
System.out.println(li);
}
}
null
을 추가할 수 있습니다.clear
를 호출할 수 있습니다.iterator
를 가져오고 remove
를 호출할 수 있습니다.List
에서 읽은 요소를 쓸 수 있습니다.컴파일러는 컴파일 시 타입 파라미터를 사용하는 대상의 타입을 컴파일러가 정하는 타입으로 대체하는 Type Erasure
를 실행하게 됩니다. 컴파일된 바이트코드에서는 T 대신 특정 타입으로 대체되어 있습니다.
Type Erasure의 규칙은 다음과 같습니다.
Object
로 바꿉니다. 따라서 생성된 바이트 코드에는 보통의 클래스, 인터페이스 및 메서드만 포함됩니다.type-safety
를 유지하기 위해 필요한 경우 타입 캐스팅
을 사용할 수 있습니다.다형성
을 유지하기 위해 브리지 메서드
를 생성합니다.Java 컴파일러는 type erasure 프로세스로서 모든 타입 파라미터를 지우고 타입 파라미터가 바인드 된 경우 첫 번째 바인드로 대체하고 타입 파라미터가 바인드 되지 않은 경우 Object
로 대체합니다.
다음은 타입 파라미터가 바인드 되지 않은 상태이므로 Object로 대체됩니다.
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; }
// ...
}
다음 예제에서 일반 Node 클래스는 bounded type parameter를 사용합니다.
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
Java 컴파일러는 바인딩 된 유형 파라미터 T
를 첫 번째 바인딩 된 클래스 인 Comparable
로 대체합니다.
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() { return data; }
// ...
}
Pair<int, char> p = new Pair<>(8, 'a'); // compile-time error
public static <E> void append(List<E> list) {
E elem = new E(); // compile-time error
list.add(elem);
}
public class MobileDevice<T> {
private static T os; // compile-time error
}
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // compile-time error
}
}
List<Integer>[] arrayOfLists = new List<Integer>[2]; // compile-time error
// Unbounded Wild card 사용 시 가능 (list가 ArrayList인 지 확인하기 위함)
// 런타임은 타입 파라미터를 추적하지 않기 때문에 ArrayList<Integer>인지 ArrayList<String>간의 차이를 알 수 없기 때문
public static void rtti(List<?> list) {
if (list instanceof ArrayList<?>) { // OK; instanceof requires a reifiable type
// ...
}
}
// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ } // compile-time error
// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // compile-time error
public class Example {
public void print(Set<String> strSet) { }
public void print(Set<Integer> intSet) { }
}
타입 체크
(Type Checking) 지원형변환
(Type Casting)을 하지 않다도 됨