🧐 Varargs가 없었을 때
Varargs(이하 가변인수)가 없었을 때에는, 임의의 수의 값을 파라미터로 사용하는 메서드를 사용하기 위해서는 배열을 사용하여야 했습니다.
예를 들어 다음과 같습니다.
public static void main(String[] args) {
final Object[] objects = {
new Date(),
"안녕하세요",
24,
"살 말랑이입니다."
};
printSequence(objects);
}
public static void printSequence(Object[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println("[" + i + "] : " + args[i]);
}
}
그러나 매번 배열을 만들고 이를 전달해 주는 것은 귀찮은 작업입니다.
🧐 Varargs를 사용하면?
이를 해결하기 위해 가변인수가 등장했는데요, 가변인수를 사용하면 다음과 같이 코드가 바뀝니다.
public static void main(String[] args) {
printSequence(
new Date(),
"안녕하세요",
24,
"살 말랑이입니다."
);
}
public static void printSequence(Object... args) {
for (int i = 0; i < args.length; i++) {
System.out.println("[" + i + "] : " + args[i]);
}
}
가변인수를 사용하더라도 결국 내부적으로는 배열에 여러 인수를 전달해야 하지만, 가변인수는 이를 배열로 만들어주는 과정을 자동화하고 숨겨줍니다.
또한 가변인수는 기존 배열을 사용하는 API를 가변인수를 사용하도록 변경하더라도 완벽하게 호환됩니다.
🧐 가변인수 사용 방법
가변인수는 마침표(.) 3개를 통해 사용할 수 있습니다.
public static void sample(Object... others);
가변인수는 파라미터의 마지막에만 사용할 수 있습니다. 그렇지 않으면 컴파일되지 않습니다.
즉 다음과 같이 사용할 수 없습니다.
public static void sample(String first, Object... others, Integer second);
🧐 가변인수의 장점
임의의 수의 파라미터를 넘길 때 배열을 만들어서 넘길 필요가 없기 때문에 코드의 가독성이 좋아지며,
무엇보다 사용하는 입장에서 편하게 사용할 수 있다는 장점이 있습니다.
🧐 가변인수의 단점
가변인수를 사용하는 메서드를 호출하는 경우, 내부적으로 배열을 생성하고 초기화해야 하기 때문에 고정된 매개변수를 사용하는 메서드에 비해 성능 저하가 발생할 수 있습니다.
(그러나 그렇게 큰 차이는 발생하지 않습니다.)
또한 오버로딩 시 문제가 발생할 수 있는데요, 예를 들어 다음과 같이 오버로딩된 메서드가 있는 경우, 무엇을 호출해야 할 지 모르기 때문에 문제가 발생할 수 있습니다.
public static void main(String[] args) {
printSequence(
new Date() /* 컴파일 에러! 무엇을 호출해야 할 지 모름! */
);
}
public static void printSequence(Object... args) {
for (int i = 0; i < args.length; i++) {
System.out.println("[" + i + "] : " + args[i]);
}
}
public static void printSequence(Object a, Object... args) {
for (int i = 0; i < args.length; i++) {
System.out.println("[" + i + "] : " + args[i]);
}
}
성능 저하 문제를 해결하기 위해서는 다음과 같이 구현할 수 있습니다.
public static void main(String[] args) {
printSequence(
new Date()
); // (1) 호출
printSequence(
new Date(),
new Date()
); // (2) 호출
printSequence(
new Date(),
new Date(),
new Date()
); // (3) 호출
}
public static void printSequence(Object first) {} // (1)
public static void printSequence(Object first, Object second) {} // (2)
public static void printSequence(Object... args) {} // (3)
특정 개수의 파라미터는 미리 메서드로 정의해둔 후, 이후 임의의 개수의 파라미터에 대해서만 가변 인자를 받도록 사용하면,
오버로딩 문제를 회피하면서 성능 문제를 해결할 수 있습니다.
🧐 가변인수와 Heap Pollution
Heap Pollution에 대한 설명을 살펴보면 Non-Reifiable type 이라는 용어가 등장합니다.
📕 Non-Reifiable type
Reifiable Type은 런타임에 타입 정보를 완전히 사용할 수 있는 타입을 의미합니다.
예를 들어 int, double 등의 primitive type과, String, Integer등의 비 제네릭 유형 등이 있습니다.
반대로 Non-Reifiable Type은 컴파일 타임에 타입 이레이저에 의해 정보가 제거된 타입을 의미하는데요,
제네릭을 사용하는 경우가 해당됩니다.
예를 들어 List<String>, List<Integer> 등이 Non-Reifiable Type 에 속합니다.
📙 Heap Pollution
매개변수화된 타입의 변수가 해당 매개변수화된 타입이 아닌 객체를 참조하는 경우 힙 오염이 발생합니다.
예를 들어 아래와 같은 상황이 있습니다.
List l = new ArrayList<Number>();
List<String> ls = l; // (1) unchecked warning
l.add(0, 42); // (2) another unchecked warning
String s = ls.get(0); // (3) ClassCastException is thrown
타입이 List<Number>인 변수 l 이, 타입이 List<String>에 할당되는 경우 힙 오염이 발생합니다.
이러한 것이 가능한 이유는 Non-Reifiable Type인 List<Number>와 List<String>은 모두 타입 이레이저에 의하여 컴파일 이후 List가 되기 때문입니다.
또한 l.add()를 호출한 경우에도 힙 오염이 발생합니다.
add 메서드의 정적 타입 파라미터는 String입니다.
그러나 이 메서드는 String이 아닌 Integer 타입의 파라미터로 호출됩니다.
제네릭 타입(혹은 매개변수화된 타입)을 가변인수로 사용하는 경우에도 힙 오염이 발생할 수 있습니다.
예시는 다음과 같습니다.
public static void main(String[] args) {
String[] a = new String[12];
a[0] = "123";
a[1] = "123";
some(a);
}
public static <T> void some(T... p) {
p[0] = (T) Integer.valueOf(123); // Heap Pollution
}
public static void some(List<String>... l) {
Object[] objectArray = l;
objectArray[0] = Arrays.asList(Integer.valueOf(100)); // Heap Pollution
String s = l[0].get(0); // ClassCastException thrown here
}
📒 Heap Pollution을 발생시키지 않고 제네릭과 가변인수를 함께 쓰는 방법
다음 규칙을 지킨다면, 힙 오염을 발생시키지 않고 타입에 안전하게 제네릭 가변인수를 사용했음을 확신할 수 있습니다.
- 제네릭 가변인수를 통해 만들어진 제네릭 배열에 아무것도 저장하지 않아야 합니다.
- 해당 배열의 참조가 밖으로 노출되지 않아야 합니다.
제네릭 가변인수를 통해 만들어진 제네릭 배열에 무언가를 저장하는 예시는 바로 위에서 오류가 발생했던 두 예시를 확있했으므로, 이번에는 해당 배열의 참조가 외부로 노출되는 경우를 살펴보겠습니다.
public static void main(String[] args) {
final String[] strings = toArray("123", "123"); // ClassCastException
System.out.println();
}
static <T> T[] toArray(T a, T b) {
final T[] tarr = toArray2(a, b);
return tarr; // tarr은 Object[] 타입이 된다.
}
// 자신의 제네릭 매개변수 배열의 참조를 노출하는 메서드
static <T> T[] toArray2(T... args) {
return args;
}
위 코드는 컴파일 시점에는 문제가 없다가 실행 시 오류가 발생하는 코드입니다.
오류가 발생하는 이유는 아래와 같습니다.
toArray() 내부에서 toArray2()를 호출합니다.
이때 toArray2()에서 제네릭 타입의 가변인수로 생성되는 배열은 항상 Object[]가 되는데, 이는 toArray()에 어떠한 타입의 객체를 넘기더라도 담을 수 있는 가장 구체적인 타입이기 때문입니다.
그러나 이를 strings에 할당하는 과정에서 String[] 로 형변환이 자동으로 발생하고, Object[] 는 String[]의 하위 타입이 아니므로 캐스팅 할 수 없기에 위와 같은 문제가 발생합니다.
📗 @SafeVarargs
위에서 살펴본 규칙을 잘 지켜서 타입에 안전하게 제네릭 가변인수를 사용한 경우라면 @SafeVarags를 통해 컴파일러가 발생시켜주는 경고를 숨길 수 있습니다.
@SafeVarargs를 사용할 때를 정하는 규칙은 간단합니다.
제네릭이나 매개변수화된 타입을 가변인수로 쓰는 모든 메서드에 @SafeVarargs를 달아야 하는데요, 이는 결국 안전하지 않은 가변인수 메서드를 절대 만들어서는 안된다는 뜻이기도 합니다.
만약 작성한 메서드 중에서 제네릭 가변인수를 사용하여 힙 오염 경고가 뜨는 메서드가 있다면, 해당 메서드가 정말 안전한지 점건한 후, @SafeVarargs를 달아주어야 합니다.
'☕️ Java > 기본' 카테고리의 다른 글
[Java] Lombok Getter를 Recored Style로 만들기 (0) | 2023.08.11 |
---|---|
[Java] Collectors.toMap() 의 여러 가지 사용법 (7) | 2023.03.14 |
[Java] 얕은 복사, 방어적 복사, 깊은 복사 (12) | 2023.02.23 |
[Java] EnumMap 에 대하여 (0) | 2023.02.18 |
[Java] groupingBy를 통해 동일한 자료의 개수 구하기 (0) | 2022.11.12 |