✅ 배열의 공변성
배열의 공변성(covariance)은 자바 배열의 특징 중 하나입니다. 공변성은 자식 타입의 배열이 부모 타입의 배열의 참조 변수에 대입될 수 있는 성질을 의미합니다. 이는 배열의 타입이 배열 요소의 타입의 하위 타입이면 형변환이 가능하다는 것을 의미합니다.
공변성을 통해 부모 타입의 배열 참조변수가 자식 타입 배열을 참조할 수 있습니다. 예를 들어, 자식 클래스가 부모 클래스의 모든 기능을 상속받은 경우, 부모 타입의 배열을 자식 타입의 배열로 취급할 수 있습니다. 이는 공변성의 예시입니다.
class Fruit { }
class Apple extends Fruit { }
class Orange extends Fruit { }
public class Main {
public static void main(String[] args) {
Apple[] apples = new Apple[2];
apples[0] = new Apple();
apples[1] = new Apple();
// Apple[] 배열을 Fruit[] 배열로 사용 (공변성)
Fruit[] fruits = apples;
fruits[0] = new Orange(); // 컴파일 에러가 발생하지 않음
Fruit firstFruit = fruits[0]; // 런타임에서 ClassCastException 발생
}
}
위의 코드에서 Apple
클래스는 Fruit
클래스의 자식 클래스이며, Orange
클래스도 Fruit
클래스의 자식 클래스입니다.
Apple[] apples
배열은 Apple
객체를 저장할 수 있는 배열입니다. 이 배열에 Apple
객체들을 저장한 후, 이 배열(Apple[] apples)을 Fruit[] fruits
배열에 할당할 수 있습니다. 이때, fruits
배열은 Fruit
객체들을 저장하는 배열입니다. 이처럼 부모 타입의 배열 참조 변수로 자식 타입의 배열 취급할 수 있는 것이 공변성입니다.
fruits[0] = new Orange();
코드에서 fruits
배열의 첫 번째 요소에 Orange
객체를 저장하려고 시도했습니다. 이는 컴파일러에서 에러를 발생시키지 않습니다. 왜냐하면 fruits
배열은 Fruit
타입을 저장하는 배열이며, Orange
는 Fruit
의 자식 클래스이기 때문에 공변성에 따라 할당이 허용되기 때문입니다.
fruits
배열은 Fruit[]
타입으로 선언되었지만, Apple[]
타입의 배열을 참조하고 있습니다. 이때 fruits[0] = new Orange();
와 같이 Orange
객체를 fruits
배열의 첫 번째 요소에 저장하려고 하면, 컴파일러는 이를 허용합니다. 그러나 런타임에는 문제가 발생합니다.
실제로 fruits
배열은 Apple[]
배열이기 때문에, fruits[0]
에는 Apple
객체만 저장 가능합니다. 하지만 fruits[0] = new Orange();
는 Orange
객체를 fruits[0]
에 저장하려는 시도입니다. 이때 런타임에서 ArrayStoreException
이 발생합니다. 이는 배열 요소의 실제 타입이 예상과 다른 객체를 저장하려고 할 때 발생하는 예외입니다.
이 예시에서 볼 수 있듯이, 배열의 공변성은 자식 타입의 배열을 부모 타입의 배열로 사용할 수 있다는 개념입니다. 또 다른 예를 들자면, Object
는 모든 클래스의 부모 클래스이므로 Object[]
는 모든 클래스의 배열을 할당할 수 있는 상위 타입입니다. 따라서, String[]
은 Object[]
에 할당될 수 있습니다. 이때, 배열의 타입은 그대로 유지되지만, 요소에 대한 일반적인 접근이 가능해집니다.
하지만 배열의 할당과 요소의 할당은 다르다는 점을 명심해야 합니다. 배열의 타입이 공변성을 가지기 때문에 배열 간의 할당이 가능하지만, 요소의 할당은 안전성을 보장하지 않습니다. 예를 들어, Object[]
배열에 String
을 요소로 할당하는 것은 안전하지만, Object[]
배열에 Integer
를 요소로 할당하는 것은 실행 시점에서 예외가 발생합니다.
요약하면, 배열의 타입과 요소의 타입 간의 관계를 이해하고, 배열이 자체적으로 타입 정보를 가지고 있다는 점을 인지하면 공변성을 이해하기 쉬워집니다. 배열의 타입은 컴파일 시점에서 결정되며, 공변성을 통해 배열의 할당이 가능해지지만, 요소의 할당은 실행 시점에서 예외가 발생할 수 있습니다.
✅ 힙 오염(Heap Pollution)
힙 오염(Heap Pollution)은 제네릭 타입 시스템에서 발생할 수 있는 타입 안전성 위반을 가리키는 용어입니다. 제네릭을 사용하여 타입을 매개변수화하면 컴파일러가 타입 체크를 수행하여 타입 안정성을 유지할 수 있지만, 몇 가지 상황에서는 힙 오염이 발생할 수 있습니다.
힙 오염은 주로 배열, 가변 인자(varargs), 원시 타입(raw type)과 같은 상황에서 발생할 수 있습니다. 힙 오염이 발생하면 제네릭을 사용하는 타입 안정성이 깨지고, 런타임 시 ClassCastException과 같은 예외가 발생할 수 있습니다.
List<String>[] stringLists = new List[2];
List<Integer> intList = List.of(1, 2, 3);
Object[] objArray = stringLists;
objArray[0] = intList;
String str = stringLists[0].get(0); // ClassCastException 발생
위의 코드에서 stringLists는 List<String> 타입의 배열로 선언되었습니다. 그러나 실제로는 List 타입의 배열을 할당했습니다. objArray는 Object 배열로 선언되고, stringLists를 참조하도록 했습니다. 그리고 intList를 objArray[0]에 할당했습니다.
이제 stringLists의 첫 번째 요소에는 List<Integer>가 들어가 있습니다. 이후 stringLists에서 값을 가져올 때 ClassCastException이 발생합니다. 왜냐하면 배열은 공변성을 가지기 때문에 컴파일 시에는 타입 안전성이 보장되지만, 런타임 시에는 타입 정보가 손실되어 제대로된 타입 체크를 할 수 없기 때문입니다.
✅ 제네릭 컬렉션을 배열로 바꾸는 방법
interface Collection<E>
//...생략
<T> T[] toArray(T[] a);
<T> T[] toArray(T[] a)
메서드는 자바 컬렉션 인터페이스인 java.util.Collection
인터페이스에 정의되어 있습니다. 따라서 Collection
인터페이스를 구현한 클래스들은 이 메서드를 상속받아 사용할 수 있습니다. 이 메서드는 컬렉션의 요소들을 특정 타입의 배열로 변환하여 반환합니다.
제네릭 메서드에서 <T> T[]라는 리턴 타입을 정의하는 것은 제네릭 배열을 리턴하겠다는 의미입니다. <T>는 메서드에 사용되는 타입 매개변수를 선언하는 부분이고, T[]는 해당 타입 매개변수 T를 요소로 갖는 배열을 의미합니다. 즉, 메서드가 호출되면 실제로는 T에 해당하는 타입으로 구체화된 배열을 리턴하게 됩니다.
따라서, T[]와 같은 배열을 리턴하는 것은 실제로는 컴파일러가 내부적으로 타입 캐스팅과 배열 생성을 수행하여 제네릭 배열의 한계를 우회하는 방식입니다.
import java.util.ArrayList;
import java.util.List;
public class ArrayConversionExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");
String[] stringArray = stringList.toArray(new String[stringList.size()]);
for (String str : stringArray) {
System.out.println(str);
}
}
}
위의 예시에서 stringList
는 List<String>
타입의 리스트입니다. toArray()
메서드를 사용하여 stringList
의 요소들을 String[]
배열로 변환하고, 이를 stringArray
에 저장합니다. 변환된 배열은 리스트의 크기에 맞게 생성되며, size()
메서드를 사용하여 크기를 얻어와 전달합니다.
마지막으로 stringArray
의 각 요소를 출력하여 확인할 수 있습니다.
toArray()
메서드는 컬렉션의 요소들을 배열로 변환할 때 유용하게 사용될 수 있습니다. 그러나 제네릭 타입의 배열을 생성하려면 컴파일 타임에서 타입 안전성을 보장하기 위해 몇 가지 주의사항을 지켜야 합니다.
- 배열 생성 시 제네릭 타입을 직접 사용할 수 없습니다.
T[] array = new T[size]; // 잘못된 방법
제네릭 타입은 컴파일 시에 타입 소거(Type Erasure)에 의해 실체화되지 않습니다. 따라서 컴파일러는 배열의 실제 타입을 알 수 없으므로 위와 같은 코드는 허용되지 않습니다.
- 배열 생성 시에는 제네릭 타입의 배열 대신 Object 배열을 생성한 후, 형변환을 해야 합니다.
T[] array = (T[]) new Object[size]; // 올바른 방법
제네릭 타입의 배열 대신 Object 배열을 생성한 후, 형변환을 통해 필요한 타입으로 변환해야 합니다. 이때 경고(unchecked warning)가 발생할 수 있습니다. 이러한 형변환은 컴파일러에게 제네릭 타입의 배열이 아닌 일반 객체 배열이라는 것을 알리는 역할을 합니다.
- 제네릭 타입의 배열을 사용할 때에는 배열의 실제 타입을 엄격히 체크해야 합니다.
T item = array[index]; // 올바른 방법
제네릭 타입의 배열을 사용할 때에는 배열의 실제 타입을 엄격히 체크하여 형변환 없이 사용해야 합니다. 이는 런타임 에러를 방지하고 타입 안전성을 유지하는데 도움을 줍니다.
👉 위의 주의사항을 따르면 제네릭 타입의 배열을 안전하게 사용할 수 있습니다. 그러나 제네릭 배열 관련 작업은 컴파일러 경고를 발생시킬 수 있으므로, 주의하고 해당 경고를 처리해야 합니다. 이를 경고 없이 처리하기 위해서는 @SuppressWarnings("unchecked") 어노테이션을 사용하여 경고를 억제할 수 있습니다.
🏷️이미지 출처와 참고한 사이트
Uploaded by N2T