1. Generics
먼저 자바에 제네릭이 도입되기 전 리스트를 어떻게 사용했는지 보면 제네릭의 유용함을 볼 수 있다.
List ints = Arrays.asList(
new Integer[] {
new Integer(1), new Integer(2), new Integer(3),
}
);
int s = 0;
for(Iterator it = ints.iterator(); it.hasNext();){
int n = ((Integer)it.next()).intValue();
s += n;
}
assert s == 6; 제네릭이 없을 떄는 List 안에 어떤 타입이 들어오는지 컴파일러는 알지 못하므로 프로그래머가 직접 작성해줘야 한다.
제네릭을 사용하면 다음과 같이 작성할 수 있다.
List<Integer> ints = Arrays.asList(1, 2, 3);
int s = 0;
for(int n: ints){
s += n;
}
assert s == 6; 자바 제네릭과 컬렉션을 사용하면 읽기도 편하고 컴파일러가 타입을 관리하기 때문에 프로그래머의 일이 줄어들었다.
자바 제네릭을 선언하는 방법은 다음과 같다.
class name<T1, T2, ..., Tn> { /* ... */ } < >안에 T1, T2, ...는 타입 파라미터 (타입 변수)라고 한다.
1.1 Erasure
자바의 제네릭은 타입 소거(Erasure) 방식을 사용한다. List<Integer>, List<Number>, List<List<String>>은 런타임엔 모두 같은 타입인 List로 변환된다. 따라서 자바에서는 List<Integer>같은 타입은 없기 때문에 a instanceof List<Integer> 와 같은 타입 검사는 불가능 하다.
그렇다면 컴파일러는 어떤식으로 List의 원소들의 타입을 알 수 있을까?
컴파일러는 컴파일할 때 타입 정보를 보고 필요한 곳에 캐스트를 하여 타입 안전성을 보장한다.
List<String> strs = new ArrayList<>();
String str = strs.get(0); 바이트 코드 관점에서는 위 코드와 아래 코드는 같다.
List strs = new ArrayList();
String str = (String)strs.get(0); 해당 코드의 .class를 확인해보면 checkcast로 String인지 확인하는 것을 볼 수 있다. 만약 실패한다면 ClassCastException이 발행한다.
이와 같이 제네릭을 구현한 이유는 만들 당시 기존 자바 코드의 호환성을 위해 이렇게 만들었다고 한다.
2. Boxing과 Unboxing
박싱은 primitive 타입을 reference 타입으로 변경하는 것을 말하고, 언박싱은 그 반대로 하는 것을 말한다. 여기서 primitive 타입은 int, double, char 과 같은 것이고 reference 타입은 class, interface, array와 같이 Object의 서브타입이다. 모든 primitive 타입은 해당되는 reference 타입이 있다.
자바는 필요할때 박싱과 언박싱을 자동으로 한다.
// 코드 1
List<Integer> ints = new ArrayList<Integer>();
ints.add(1);
int n = ints.get(0);
// 코드 2
List<Integer> ints = new ArrayList<Integer>();
ints.add(Integer.valueOf(1));
int n = ints.get(0).intValue();
// 코드 1과 코드 2는 같은 코드이다. 다음 코드는 정수 리스트의 합을 구하는 두 함수이다.
public static int sumPrimitive(List<Integer> ints) {
int s = 0;
for(int n: ints) {
s += n;
}
return s;
}
public static Integer sumInteger(List<Integer> ints){
Integer s = 0;
for(Integer n: ints) {
s += n;
}
return s;
} sumInteger 함수에서 반복문 내 s와 n에서 자동으로 박싱과 언박싱이 일어나므로 성능상으로 차이점이 있다. 어느정도 차이가 나는지 확인하기 위해서 다음과 같은 코드를 작성해서 테스트 해보았다.
import java.util.*;
public class Main {
static Integer sumInteger(List<Integer> ints) {
Integer s = 0;
for (Integer n : ints) {
s += n;
}
return s;
}
static int sumPrimitive(List<Integer> ints) {
int s = 0;
for (Integer n : ints) {
s += n;
}
return s;
}
static long time(Runnable r, int N) {
long best = Long.MAX_VALUE;
for (int i=0; i<N; i++) {
long t0 = System.nanoTime();
r.run();
long t1 = System.nanoTime();
best = Math.min(best, t1 - t0);
}
return best;
}
public static void main(String[] args) {
int N = 9000000;
List<Integer> xs = new ArrayList<>(N);
for (int i=0; i<N; i++) xs.add(i & 1234);
long tA = time(() -> { sumInteger(xs); }, 5);
long tB = time(() -> { sumPrimitive(xs); }, 5);
System.out.printf("sumInteger: %,d ms%n", tA/1000000);
System.out.printf("sumPrimitive: %,d ms%n", tB/1000000);
}
} 결과는 다음과 같이 약 7배 차이가 난다.
sumInteger: 21 ms
sumPrimitive: 3 ms 3. 제네릭 메소드
class List {
public static <T> List<T> toList(T... arr) {
List<T> list = new ArrayList<T>();
for(T e: list) {
list.add(e);
}
return list;
}
} 자바에서는 <T>로 타입 변수를 선언하고 이를 메소드에서도 사용할 수 있다. 또 필요한 캐스트를 컴파일러가 자동으로 해준다.
예시로, 아래와 같이 선언해서 사용할 수 있다.
List<Integer> ints = Arrays.toList(1,2,3); 컴파일러가 암묵적으로 해당 메소드에 타입을 추가하고 이를 타입 추론(Type inference)라고 한다.
List<Integer> ints = Arrays.<Integer>toList(1,2,3); 여기서는 컴파일러가 T를 Integer로 자동으로 추론하게 되면 위 코드와 유사하게 될 것이다.