varargs
(가변인자)는 JDK 5에서 새로 도입된 기능이다.
단어에서 추측할 수 있듯 인자의 수, 매개변수의 수를 가변적으로 조절할 수 있게 해주는 기능이다.
인자와 매개변수의 차이
- 매개변수: 메소드를 선언하는 부분에 있는 변수
- 인자: 메소드를 호출할 때 전달되는 값
사용법은 간단하다. 매개변수를 여러개 정의하고 싶은 메소드를 선언할 때, 매개변수 부분에 … 을 붙여주면 된다.
public int getSum(int... numbers) {
return Arrays.stream(numbers).sum();
}
varargs
는 내부적으로 배열을 반환하기 때문에Arrays.stream()
을 사용할 수 있다.
사실 위에서 짧게 설명한 것이 전부라고 할 수 있다.
varargs
를 사용하면 컴파일타임에 Object
배열이 만들어지고 varargs
에 할당된 인자들이 이 배열의 원소로 들어간다.
위 코드를 보자마자 이미 어떤 장점이 있는지 눈치채셨을 분들도 있을 것이다.
가장 큰 장점은 메소드의 오버로딩 필요를 줄여준다는 것이다.
public int getSum(int number1, int number2) {
return number1 + number2;
}
public int getSum(int number1, int number2, int number3) {
return number1 + number2 + number3;
}
...
varags
를 사용하지 않았다면 위와 같이 여러개의 메소드를 새로 정의해주어야한다.
성능
위 동작 과정에서 알 수 있듯 varargs
가 사용된 메소드들은 함수 실행 직전에 항상 배열이 생성된다.
이 점 때문에 여러번 실행되는 경우 매번 배열을 만드는 오버헤드가 생기기때문에 성능 문제가 발생할 수도 있다.
타입 안정성
varargs
는 제네릭과 같이 사용할 때 주의해야 한다.
제네릭 또한 컴파일 타임에 타입 데이터가 제거되고 Object
로 변환된다. 그리고 데이터가 사용될 때 컴파일러가 지정된 타입으로 변환시켜 사용하게 되는 방식이다.
(제네릭이 없던 이전 버전 호환성을 위해)
<T> void function(T… data) { … }
때문에 다음과 같이 사용한다면 varargs
의 동작에 의해 Obejct
형식이 제네릭에 할당되어 컴파일러에게 타입 변환의 도움을 받지 못하게 된다.
public static void main(String[] args) {
int number1 = 10;
int number2 = 20;
int number3 = 30;
}
private static <T> void unsafeFunction(T... numbers) {
Object[] data = numbers;
data[2] = "stringData";
}
위와 같은 코드가 컴파일러의 경고도 받지 못하고 실행되어 RuntimeException
이 발생한다.
제네릭에 타입제한을 걸어 위와 같은 문제를 해결할 수 있다.
private static <T extends Integer> void safeFunction(T... numbers) {
Integer[] data = numbers;
data[2] = "stringData";
}
리스트를 사용할 때는 조금 더 복잡해지지만 기본적으로 문제가 발생하는 원인은 동일하다.
다음과 같은 코드는 힙 오염을 발생시킨다.
void run() {
List<String> strings = new ArrayList<>();
List<String> strings2 = new ArrayList<>();
strings.add("stringData");
dangerousFunction(strings, strings2);
}
void dangerousFunction(List<String>... listOfStrings) {
List<Integer> ints = List.of(1,2);
Object[] datas = listOfStrings;
datas[0] = ints;
String stringData = listOfStrings[0].get(0);
System.out.println(stringData);
}
대신 아래와 같이 varargs
대신 List
를 사용하면 컴파일러의 도움을 받을 수 있다. 다만 이렇게 되면 인자를 전달해 줄 때 List.of
같은 팩터리 메소드를 사용하여 전달해주어야 하는 불편함을 감수해야 한다.
void run() {
List<String> strings = new ArrayList<>();
List<String> strings2 = new ArrayList<>();
strings.add("stringData");
safeFunction(List.of(strings, strings2));
}
<T> void safeFunction(List<List<T>> listOfStrings) {
List<Integer> ints = List.of(1,2);
List<List<T>> datas = listOfStrings;
datas.add(0, ints);
String stringData = listOfStrings.get(0).get(0);
System.out.println(stringData);
}
varargs
는 잘 사용하면 정말 편리한 기능이 될 수 있지만, 내부적인 동작과정 때문에 사용하기 전 고려해야 할 부분이 꽤 있다.
특히 제네릭과 함께 사용할 때는 타입 안정성에 대해 한 번 더 생각하고 사용하도록 하자.
JDK 7 이후 부터는 제네릭과 가변인자를 함께 사용하면 발생하는 경고를 @SafeVarargs
어노테이션으로 보이지 않게 할 수 있는데, 당연히 이 어노테이션이 붙은 메서드는 타입 안정성이 보장되어야 한다.
이에 대한 사용 가이드는 밑 링크를 참고하면 좋을 것 같다.
Improved Compiler Warnings When Using Non-Reifiable Formal Parameters with Varargs Methods