-
Notifications
You must be signed in to change notification settings - Fork 2
Open
Labels
🐭 05 Generics5장 제네릭5장 제네릭
Description
Chapter : 5. 제네릭
Item : 28. 배열보다는 리스트를 사용하라
Assignee : jseok0917
🍑 서론
제네릭
- 한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능
- 타입안정성을 높여줌. 사용될 타입을 미리 지정하지 않고 클래스나 메서드를 정의할 수 있다.
- 형변환의 번거로움을 줄여줌. 클래스나 메서드에서 사용될 수 있는 데이터 타입을 파라미터화할 수 있다.
- 타입에 의존하는 클래스나 메서드는 문제가 발생할 수 있음.
//제네릭을 사용하지 않는 경우
//컴파일러는 Box에 어떤 타입의 객체가 저장되는지 알지 못한다.
public class Box {
private Object item;
public void setItem(Object item) {
this.item = item;
}
public Object getItem() {
return item;
}
}
//제네릭을 사용하는 경우
//컴파일러는 Box에 T라는 타입의 객체가 저장됨을 알 수 있다.
public class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}제네릭을 사용하는 이유
- 컴파일러가 컴파일 시점에 형변환 오류와 같은 변수 타입과 관련된 오류를 잡아낼 수 있다.
- 프로젝트 규모가 커질수록, 타입과 관련하여 발생하는 런타임 시점의 오류를 잡아내기가 어려워진다.
- 변수 타입을 명시함으로써 코드가독성과 유지보수가 용이하다.
- 컴파일러가 변수 타입을 알기 때문에 코드가 더 효율적으로 최적화될 수 있고, 실행시간 단축 및 성능이 향상된다.
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
// 제네릭을 명시하지 않은 경우
ArrayList my_list = new ArrayList();
my_list.add("Hello");
my_list.add(123);
// 아래 코드는 문법상 허용된다.
// 그러나 런타임 시점에 오류가 발생한다.
for (Object item : my_list) {
String str = (String) item; // 형변환 시도
System.out.println(str.toUpperCase()); // 런타임에 ClassCastException 발생
}
}
}🍑 본론
1. 배열은 공변이고, 리스트는 불공변이다.
-
불공변성 : 자료형 간의 대체가 없음. 즉, 하위 자료형이나 상위 자료형으로의 대체가 허용되지 않는다.
-
공변성 : 자료형 간의 대체가 가능. 즉, 하위 자료형을 상위 자료형으로 대체할 수 있다.
-
간단하게 얘기해서
- T가 T'의 부모이면 C<T>도 C<T'>의 부모
import java.util.ArrayList;
import java.util.List;
class Animal {}
class Cat extends Animal {}
//공변성과 불공변성의 차이
Cat cat = new Cat();
Animal animal = cat;
Cat[] cat_array = new Cat[5];
Animal[] animal_array = cat_array //할당 가능
List<Cat> cat_list = new List<Cat>();
List<Animal> animal_list = cat_list //할당 불가능- 공변성으로 발생하는 문제의 예시
public class test04 {
public static void main(String[] args) {
Cat[] cats = new Cat[5];
cats[0] = new Cat();
// 배열을 Animal[]로 캐스팅
Animal[] animals = cats;
// 다른 종류의 동물 객체를 추가
animals[1] = new Animal();
// cats 배열을 순회하면서 각 요소를 출력
for (Cat cat : cats) {
System.out.println(cat); // 런타임 시점에서 Unchecked Error인 ArrayStoreException 발생한다.
}
}
}- 제네릭을 사용할 경우 다음과 같이 타입 안정성을 보장할 수 있다.
public class test03 {
public static void main(String[] args) {
List<Cat> cats = new ArrayList<>();
cats.add(new Cat());
// 불공변성으로 인해 List<Animal>으로 캐스팅 불가능
List<Animal> animals = cats; // 컴파일 시점에서 Check Error인 Type Mismatch 에러가 발생한다.
// 동일한 리스트를 사용하여 다른 동물을 추가
animals.add(new Animal());
for (Cat cat : cats) {
System.out.println(cat);
}
}
}- 배열과 리스트를 섞어서 사용할 경우, 공변성으로 인해 다음과 같은 문제가 발생할 수 있다.
List<String>[] stringLists = new List<String>[1];
List<Integer> intList = List.of(42);
Object[] objects = stringLists; // Object는 String의 부모클래스 이므로 할당가능(공변성)
objects[0] = intList; //object에 원소를 할당하면, stringLists 참조했으므로 stringLists[0] 도 바뀜
String s = stringLists[0].get(0); // 2. 배열은 실체화(reify)된다. 리스트는...
- 실체화 : 자신의 타입 정보를 런타임에도 알고 있는 것(정확히는 런타임시에도 타입을 지속적으로 체크)
- 비실체화 : 자신의 타입정보를 런타임 시점에 소거(erasure)하여 컴파일 타임보다 정보를 적게 가지는 것
- 제네릭 Type Erasure : 컴파일 타입에만 타입 제약 조건을 정의하고, 런타임에는 타입을 제거
- 타입소거란?
ArrayList<Integer> integerList;
ArrayList<String> stringList;
//이 코드는 컴파일하면,
ArrayList integerList;
ArrayList stringList;
//이렇게 같은 타입으로 변한다. 따라서, 다음과 같은 오버로딩은 불가능하다.
public void overload(List<Integer> integerList) {}
public void overload(List<String> stringList) {}- 타입 소거(erasure)로 인한 문제점
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");
// Object 배열 생성
Object[] objArray = new Object[2];
// 제네릭 리스트의 내용을 배열로 복사
copyListToArray(stringList, objArray);
// 배열 요소 출력
for (Object obj : objArray) {
// ClassCastException 발생 가능
String str = (String) obj;
System.out.println(str);
}
}
// 제네릭 리스트를 배열로 복사하는 메서드
public static <T> void copyListToArray(List<T> list, T[] array) {
for (int i = 0; i < list.size(); i++) {
array[i] = list.get(i);
}
}
}
//위의 예제에서는 List<String>을 Object[]로 복사하려고 시도했습니다.
//그러나 자바에서는 제네릭의 타입 정보는 런타임에 소거되기 때문에,
//copyListToArray 메서드는 컴파일 시에는 제대로 동작하지만 런타임에는 제네릭 타입 정보가 소거되어
// Object 배열에 실제로는 String이 아닌 Object로 채워지게 됩니다.
//이렇게 되면 배열에서 값을 가져올 때 ClassCastException이 발생할 수 있습니다.
//따라서, reify한 제네릭과 reify하지 않은 배열을 혼합해서 사용할 때는 주의해야 합니다.
//가능하다면 제네릭을 사용한 컬렉션을 배열로 변환하는 것은 피하는 것이 좋습니다.배열로 형변환 시, 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분 E[]대신 List를 사용하면 해결된다.
public class Chooser {
private final Object[] choiceArray;
public Chooser(Collection choices) {
choiceArray = choices.toArray();
}
//실제로 이 메서드를 써먹으려면
//반환된 Object를 다시 형변환해주는 과정이 필요하다.
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices){
choiceArray = choices.toArray();
}
//choose 메서드는 그대로
}
//근데 이건 왼쪽이 T[]인데 오른쪽은 object[]이므로 타입이 서로 호환되는지 알 수 없으므로 컴파일에러 발생public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices){
//형변환하면 컴파일에러가 생기지 않는다.
choiceArray = (T[]) choices.toArray();
}
//choose 메서드는 그대로
}
//컴파일 에러가 이제 발생하지 않지만, 그대신 경고가 뜸
//why? 명시적 캐스팅은 오류의 원인을 프로그래머가 책임지는것이므로//리스트를 써서 제네릭을 쓰면 깔끔하다
public class Chooser<T> {
private final List<T> choiceList;
public Chooser(Collection<T> choices){
choiceList = new ArrayList<>(choices);
}
public T choose(){
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceList.size()));
}
}🍑 결론
- 배열보다 리스트를 사용해라. 둘의 가장 큰 차이는 제네릭을 사용할 수 있냐 없냐. 제네릭의 사용하면 타입안정성, 즉 컴파일 시 타입으로 인한 오류를 잡아낼 수 있다. 런타임 에러는 항상 치명적일 수 있음을 인지하자!
번외
Java 컴파일러의 타입 소거 규칙
- unbounded Type(<?>, )는 Object로 변환
- bound type()의 경우는 Object가 아닌 Comprarable로 변환
- 제네릭 타입을 사용할 수 있는 일반 클래스, 인터페이스, 메소드에만 소거 규칙을 적용
- 타입 안정성 보존을 위해 필요하다면 type casting
- 확장된 제네릭 타입에서 다형성을 보존하기 위해 bridge method를 생성
bound type의 타입 소거
// 컴파일 할 때 (타입 소거 전)
public class Test<T> {
public void test(T test) {
System.out.println(test.toString());
}
}
// 런타임 때 (타입 소거 후)
public class Test {
public void test(Object test) {
System.out.println(test.toString());
}
}타입이 컴파일 타임에만 유효하고, 런타임엔 전부 Object로 치환되는 희한한 특성 때문에, 자바 제네릭에는 몇 가지 제약이 존재한다.
- 원시 타입의 제네릭 타입 생성 불가
- 제네릭 타입 인스턴스화 불가
- 제네릭 타입의 static 필드 선언 불가
- 제네릭 타입으로의 형변환이나 instanceof 사용 불가
- 제네릭 타입의 배열 생성 불가
- 제네릭 타입이 포함된 클래스 생성, catch/throw 불가
- 타입 소거가 됐을 때 동일한 메서드 오버로딩 불가
- 자바의 제네릭은 각종 제약을 가질 뿐더러 타입 안정성을 완벽하게 보장하지 않으며 잘못된 사용 시 "힙 오염 (Heap pollution)"을 유발하는 주범이 된다.
그럼에도 왜 이런 방식을 사용하나?
C#에서는 제네릭을 조금 다른 방식으로 구현한다.
다음과 같은 C# 코드가 있다고 가정해보자.
List<int> integerList;
List<string> stringList;
//컴파일러는 코드 내에서 List가 사용하는 제네릭 타입 (int, string)을 전부 확인한다. 그리고 List의 코드에서 제네릭 타입 파라미터를 int, string으로 치환한 특수 버전 List를 2개 생성하여 각각 List<int>, List<string>에 대입한다. 이러한 방식은 자바와 달리 완벽한 타입 검사를 지원하고, 타입이 실체화 되어있기 때문에 타입의 동적인 사용이 가능하다.// 자바와 달리 제네릭 타입의 동적인 생성 가능
new T();
//초창기 자바에는 제네릭이 존재하지 않았다.
//따라서 현재 제네릭을 지원하는 컬렉션 프레임워크 등도 제네릭 없이, Object 타입으로 구현이 되어 있었다.
// 제네릭이 지원되기 전 ArrayList
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
//그러다 2004년 JDK 1.5 출시와 함께 제네릭이 도입되었지만 이미 수많은 코드들이 Object를 구현하는 방식으로 짜여져 있었고,
//이들과 호환성을 유지하기 위해 자바는 제네릭 타입을 런타임에 소거하는 방식을 택했다.
//제네릭을 사용하더라도 런타임엔 Object로 치환되기 때문에 기존 코드들과 호환성을 유지할 수 있는 것이다.Referenced by https://velog.io/@bedshanty/%EC%9E%90%EB%B0%94%EC%9D%98-%ED%83%80%EC%9E%85-%EC%86%8C%EA%B1%B0-Type-erasure
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
🐭 05 Generics5장 제네릭5장 제네릭