-
[JAVA] 자바 mid2 1 - 2 강 요약자바 2024. 12. 30. 12:47
1. 제네릭 Generic 1
제네릭이 필요한 이유
제네릭을 사용하면 코드 재사용과 타입 안전성을 모두 지킬 수 있다
EX1.
// 숫자를 보관하고 꺼낼 수 있는 단순한 기능을 제공 public class IntegerBox { private Integer value; public void set(Integer value) { this.value = value; } public Integer get() { return value; } } // 문자열을 보관하고 꺼낼 수 있는 단순한 기능을 제공 public class StringBox { private String value; public void set(String object) { this.value = object; } public String get() { return value; } }
public class BoxMain1 { public static void main(String[] args) { IntegerBox integerBox = new IntegerBox(); integerBox.set(10); //오토 박싱 Integer integer = integerBox.get(); System.out.println("integer = " + integer); StringBox stringBox = new StringBox(); stringBox.set("hello"); String str = stringBox.get(); System.out.println("str = " + str); } }
문제 1 :
이후에 Double, Boolean을 포함한 다양한 타입을 담는 박스가 필요하다면 각각의 타입별로 DoubleBox와 같은 클래스를 새로 만들어야 한다
EX2. 다형성을 통한 중복 해결 시도 (Object 활용)
public class ObjectBox { private Object value; public void set(Object object) { this.value = object; } public Object get() { return value; } }
- 내부에 Object value를 가지고 있다 Object는 모든 타입의 부모이다 부모는 자식을 담을 수 있으므로 세상의 모든 타입을 ObjectBox에 보관할 수 있다
public class BoxMain2 { public static void main(String[] args) { ObjectBox integerBox = new ObjectBox(); integerBox.set(10); Integer integer = (Integer) integerBox.get(); // Object -> Integer 캐스팅 System.out.println("integer = " + integer); ObjectBox stringBox = new ObjectBox(); stringBox.set("hello"); String str = (String) stringBox.get(); // Object -> String 캐스팅 System.out.println("str = " + str); // 잘못된 타입의 인수 전달시 integerBox.set("문자100"); Integer result = (Integer) integerBox.get(); // String -> Integer 캐스팅 예외 System.out.println("result = " + result); } }
문제 1 : 반환 타입이 맞지 않는다
integerBox를 만들어서 숫자 10을 보관했고 숫자 입력 부분에서는 문제가 없지만 integerBox.get()을 호출할 때 문제가 된다integerBox.get()의 반환 타입은 Object이다
Object obj = integerBox.get();
Integer = Object 는 성립하지 않는다
자식은 부모를 담을 수 없다 따라서 아래와 같이 직접 다운 캐스팅해야 한다
Integer integer = (Integer) integerBox.get() // 1 Integer integer = (Integer) (Object)value // 2 Integer integer = (Integer)value // 3
문제 2 : 잘못된 타입의 인수 전달 문제
integerBox.set("문자100");
개발자의 의도는 integerBox 에는 변수 이름과 같이 숫자 타입이 입력되기를 기대했다
하지만 set(Object ..) 메서드는 모든 타입의 부모인 Object를 매개변수로 받기 때문에 모든 데이터를 입력받을 수 있다
잘못된 타입의 값을 전달하면 값을 꺼낼 때 문제가 발생한다
Integer result = (Integer) integerBox.get(); // 1 Integer result = (Integer) "문자100"; // 2 Integer result = (Integer) "문자100"; // 3. 예외 발생 String을 Integer로 캐스팅할 수 없다
숫자가 들어가 있을 것으로 예상한 박스에는 문자열이 들어가 있었다
결과적으로 다운 캐스팅시 String을 Integer로 캐스팅 할 수 없다는 예외가 발생한다
EX3. 제네릭 적용
public class GenericBox<T> { private T value; public void set(T value) { this.value = value; } public T get() { return value; } }
- <> 를 사용한 클래스를 제네릭 클래스라 한다
- 제네릭 클래스를 사용할 때는 Integer, String 과 같은 타입을 미리 결정하지 않는다
- 대신 클래스명 오른쪽에 <T>와 같이 선언하면 제네릭 클래스가 된다 여기서 T를 타입 매개변수라고 한다
- 클래스 내부에 T 타입이 필요한 곳에 T value와 같이 타입 매개변수를 적어두면 된다
public class BoxMain3 { public static void main(String[] args) { GenericBox<Integer> integerBox = new GenericBox<Integer>(); // 생성 시점에 T의 타입 결정 integerBox.set(10); // integerBox.set("문자100"); // Integer 타입만 허용, 컴파일 오류 Integer integer = integerBox.get(); // Integer 타입 반환 (캐스팅 X) System.out.println("integer = " + integer); GenericBox<String> stringBox = new GenericBox<String>(); stringBox.set("hello"); // String 타입만 허용 String str = stringBox.get(); // String 타입만 반환 System.out.println("str = " + str); // 원하는 모든 타입 사용 가능 GenericBox<Double> doubleBox = new GenericBox<Double>(); doubleBox.set(10.5); Double doubleValue = doubleBox.get(); System.out.println("doubleValue = " + doubleValue); // 타입 추론: 생성하는 제네릭 타입 생략 가능 GenericBox<Integer> integerBox2 = new GenericBox<>(); } }
참고로 제네릭을 도입한다고 해서 앞서 설명한 GenericBox<String>, GenericBox<Integer> 와 같은 코드가 실제 만들어지는 것은 아니다 대신에 자바 컴파일러가 우리가 입력한 타입 정보를 기반으로 이런 코드가 있다고 가정하 고 컴파일 과정에 타입 정보를 반영한다 이 과정에서 타입이 맞지 않으면 컴파일 오류가 발생한다
타입 추론
GenericBox<Integer> integerBox = new GenericBox<Integer>() // 타입 직접 입력 GenericBox<Integer> integerBox2 = new GenericBox<>() // 타입 추론
타입 직접 입력 코드를 보면 변수를 선언할 때와 객체를 생성할 때 <Integer>가 두 번 나온다
자바는 왼쪽에 있는 변수를 선언할 때의 <Integer>를 보고 오른쪽에 있는 객체를 생성할 때 필요한 타입 정보를 얻는다
따라서 타입 추론 코드의 오른쪽 코드에서 new GenricBox<>()와 같이 타입 정보를 생략할 수 있다
이렇게 자바가 스스로 추론해서 개발자가 타입 정보를 생략할 수 있는 것을 타입 추론이라 한다
제네릭 용어와 관례
제네릭의 핵심은 사용할 타입을 미리 결정하지 않는다는 점이다
클래스 내부에서 사용하는 타입을 클래스를 정의하는 시점에 결정하는 것이 아니라
실제 사용하는 생성 시점에 타입을 결정하는 것이다
이것을 쉽게 비유하자면 메서드의 매개변수와 인자의 관계와 비슷하다
1. 메서드에 필요한 값을 메서드 정의 시점에 미리 결정
void method1() { println("hello"); }
- 메서드에 필요한 값을 이렇게 메서드 정의 시점에 미리 결정하게 되면 이 메서드는 오직 "hello" 라는 값만 출력할 수 있다 따라서 재사용성이 떨어진다
2. 메서드에 필요한 값을 인자를 통해 매개변수로 전달해서 결정
void method2(String param) { println(param); } void main() { method2("hello"); method2("hi"); }
- 메서드에 필요한 값을 메서드를 정의하는 시점에 미리 결정하는 것이 아니라, 메서드를 실제 사용하는 시점으로 미룰 수 있다
- 메서드에 매개변수(String param)를 지정하고, 메서드를 사용할 때 원하는 값을 인자(hello, hi)로 전달하면 된다
- 매개변수를 정의하고, 실행 시점에 인자를 통해 원하는 값을 매개변수에 전달했다 이렇게 하면 이 메서드는 실행 시점에 얼마든지 다른 값을 받아서 처리할 수 있다 따라서 재사용성이 크게 증가한다
메서드의 매개변수와 인자
void method(String param) // 매개변수 void main() { String arg = "hello"; method(arg) // 인수 전달 }
- 매개변수(Parameter) : String param
- 인자, 인수(Argument) : arg
제네릭의 타입 매개변수와 타입 인자
제네릭도 앞서 설명한 메서드의 매개변수와 인자의 관계와 비슷하게 작동한다
제네릭 클래스를 정의할 때 내부에서 사용할 타입을 미리 결정하는 것이 아니라,
해당 클래스를 실제 사용하는 생성 시 점에 내부에서 사용할 타입을 결정하는 것이다
차이가 있다면 메서드의 매개변수는 사용할 값에 대한 결정을 나중으로 미루는 것이고,
제네릭의 타입 매개변수는 사용할 타입에 대한 결정을 나중으로 미루는 것이다
- 메서드는 매개변수에 인자를 전달해서 사용할 값을 결정한다
- 제네릭 클래스는 타입 매개변수에 타입 인자를 전달해서 사용할 타입을 결정한다
제네릭에서 사용하는 용어
- 타입 매개변수 : GenericBox<T>에서 T
- 타입 인자 : GenericBox<Integer>에서 Integer
- 제네릭 타입의 타입 매개변수 <T>에 타입 인자를 전달해서 제네릭의 사용 타입을 결정한다
- 제네릭(Generic) 단어
- 제네릭이라는 단어는 일반적인, 범용적인이라는 영어 단어 뜻
- 특정 타입에 속한 것이 아니라 일반적으로, 범용적으로 사용할 수 있다는 뜻
- 제네릭 타입 (Generic Type)
- 클래스나 인터페이스를 정의할 때 타입 매개변수를 사용하는 것
- 제네릭 클래스, 제네릭 인터페이스를 모두 합쳐서 제네릭 타입이라 한다
- 타입은 클래스, 인터페이스, 기본형등을 모두 합쳐서 부르는 말이다
- 예: class GenericBox<T> { private T t; } / 여기에서 GenericBox<T>를 제네릭 타입이라 한다
- 타입 매개변수 (Type Parameter)
- 제네릭 타입이나 메서드에서 사용되는 변수로, 실제 타입으로 대체된다
- 예: GenericBox<T> / 여기에서 T를 타입 매개변수라고 한다
- 타입 인자 (Type Argument)
- 제 제네릭 타입을 사용할 때 제공되는 실제 타입
- 예: GenericBox<Integer> / 여기에서 Integer를 타입 인자라고 한다
제네릭 명명 관례
타입 매개변수는 일반적인 변수명처럼 소문자로 사용해도 문제는 없다
하지만 일반적으로 대문자를 사용하고 용도에 맞는 단어의 첫글자를 사용하는 관례를 따른다
주로 사용하는 키워드는 다음과 같다
- E - Element
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
제네릭 기타
class Data<K, V> {}
- 한번에 여러 타입 매개변수를 선언할 수 있다
- 타입 인자로 기본형은 사용할 수 없다 (대신에 래퍼 클래스 사용)
로 타입 - raw type
public class RawTypeMain { public static void main(String[] args) { GenericBox integerBox = new GenericBox(); // GenericBox<Object> integerBox = new GenericBox<>(); // 권장 integerBox.set(10); Integer result = (Integer) integerBox.get(); System.out.println("result = " + result); } }
- <>을 지정하지 않을 수 있는데 이것을 로타입(raw type) 또는 원시타입이라 한다
- 원시 타입을 사용하면 내부의 타입 매개변수가 Object를 사용한다
- 결론적으로 raw type은 사용하지 말아야 한다
2. 제네릭 Genric 2
타입 매개변수 제한 예제
public class AnimalHospitalV3<T extends Animal> { private T animal; public void set(T animal) { this.animal = animal; } public void checkup() { System.out.println("동물 이름: " + animal.getName()); System.out.println("동물 크기: " + animal.getSize()); animal.sound(); } public T getBigger(T target) { return animal.getSize() > target.getSize() ? animal : target; } }
- 여기서 핵심은 <T extends Animal>, 타입 인자로 들어올 수 있는 값이 Animal과 그 자식으로 제한된다
- 이제 자바 컴파일러는 T에 입력될 수 있는 값의 범위를 예측할 수 있다
제네릭 메서드
public class GenericMethod { public static Object objMethod(Object obj) { System.out.println("object print: " + obj); return obj; } public static <T> T genericMethod(T t) { System.out.println("generic print: " + t); return t; } public static <T extends Number> T numberMethod(T t) { System.out.println("bound print: " + t); return t; } }
public class MethodMain1 { public static void main(String[] args) { Integer i = 10; Object object = GenericMethod.objMethod(i); // 타입 인자(Type Argument) 명시적 전달 System.out.println("명시적 타입 인자 전달"); Integer result = GenericMethod.<Integer>genericMethod(i); Integer integerValue = GenericMethod.<Integer>numberMethod(10); Double doubleValue = GenericMethod.<Double>numberMethod(20.0); } }
- 제네릭 타입
- 정의 : GenericClass<T>
- 타입 인자 전달 : 객체를 생성하는 시점
- 예: new GenericClass<String>
- 제네릭 메서드
- 정의 : <T> T genericMethod(T t)
- 타입 인자 전달 : 메서드를 호출하는 시점
- 예: GenericMethod.<Integer>genericMethod(i)
- 제네릭 메서드는 클래스 전체가 아니라 특정 메서드 단위로 제네릭을 도입할 때 사용한다
- 제네릭 메서드를 정의할 때는 메서드의 반환 타입 왼쪽에 다이아몬드를 사용해서 <T>와 같이 타입 매개변수를 적어준다
- 제네릭 메서드는 메서드를 실제 호출하는 시점에 다이아몬드를 사용해서 <Integer>와 같이 타입을 정하고 호출한다
- 제네릭 메서드의 핵심은 메서드를 호출하는 시점에 타입 인자를 전달해서 타입을 지정하는 것이다 따라서 타입을 지정하면서 메서드를 호출한다
인스턴스 메서드, static 메서드
제네릭 메서드는 인스턴스 메서드와 static 메서드에 모두 적용할 수 있다
// 제네릭 타입 class Box<T> { static <V> V staticMethod2(V t) {} // static 메서드에 제네릭 메서드 도입 <Z> Z instanceMethod2(Z z) {} // 인스턴스 메서드에 제네릭 메서드 도입 가능 }
- 참고 : 제네릭 타입은 static 메서드에 타입 매개변수를 사용할 수 없다 제네릭 타입은 객체를 생성하는 시점에 타입이 정해진다 그런데 static 메서드는 인스턴스 단위가 아니라 클래스 단위로 작동하기 때문에 제네릭 타입과는 무관하다 따라서 static 메서드에 제네릭을 도입하려면 제네릭 메서드를 사용해야 한다
class Box<T> { T instanceMethod(T t) {} // 가능 static T staticMethod1(T t) {} // 제네릭 타입의 T 사용 불가능 }
타입 매개변수 제한
제네릭 메서드도 제네릭 타입과 마찬가지로 타입 매개변수를 제한할 수 있다
public static <T extends Number> T numberMethod(T t) {} //GenericMethod.numberMethod("Hello"); // 컴파일 오류 Number의 자식만 입력 가능
제네릭 메서드 타입 추론
// 타입 인자(Type Argument) 명시적 전달 System.out.println("명시적 타입 인자 전달"); Integer result = GenericMethod.<Integer>genericMethod(i); Integer integerValue = GenericMethod.<Integer>numberMethod(10); Double doubleValue = GenericMethod.<Double>numberMethod(20.0); // 타입 추론, 타입 인자 생략 System.out.println("타입 추론"); Integer result2 = GenericMethod.genericMethod(i); Integer integerValue2 = GenericMethod.numberMethod(10); Double doubleValue2 = GenericMethod.numberMethod(20.0);
타입을 추론해서 컴파일러가 대신 처리하기 때문에 타입을 전달하지 않는 것 처럼 보이지만 실제로는 타입 인자가 전달된다
제네릭 메서드 활용
public class AnimalMethod { public static <T extends Animal> void checkup(T t) { System.out.println("동물 이름: " + t.getName()); System.out.println("동물 크기: " + t.getSize()); t.sound(); } public static <T extends Animal> T getBigger(T t1, T t2) { return t1.getSize() > t2.getSize() ? t1 : t2; } }
public class MethodMain2 { public static void main(String[] args) { Dog dog = new Dog("멍멍이", 100); Cat cat = new Cat("냐옹이", 100); AnimalMethod.checkup(dog); AnimalMethod.checkup(cat); Dog targetDog = new Dog("큰 멍멍이", 200); Dog bigger = AnimalMethod.getBigger(dog, targetDog); System.out.println("bigger = " + bigger); } }
cf. 제네릭 타입과 제네릭 메서드의 우선순위
제네릭 타입보다 제네릭 메서드가 높은 우선순위를 가진다
와일드카드 1
제네릭 타입을 조금 더 편리하게 사용할 수 있는 와일드카드(wildcard)
참고로 와일드카드라는 뜻은 컴퓨터 프로그래밍에서 *, ? 와 같이 하나 이상의 문자들을 상징하는 특수 문자를 뜻한다
즉 여러 타입이 들어올 수 있다는 뜻이다
public class Box<T> { private T value; public void set(T value) { this.value = value; } public T get() { return value; } }
public class WildcardEx { static <T> void printGenericV1(Box<T> box) { System.out.println("T = " + box.get()); } static void printWildcardV1(Box<?> box) { System.out.println("? = " + box.get()); } static <T extends Animal> void printGenericV2(Box<T> box) { T t = box.get(); System.out.println("이름 = " + t.getName()); } static void printWildcardV2(Box<? extends Animal> box) { Animal animal = box.get(); System.out.println("이름 = " + animal.getName()); } static <T extends Animal> T printAndReturnGeneric(Box<T> box) { T t = box.get(); System.out.println("이름 = " + t.getName()); return t; } static Animal printAndReturnWildcard(Box<? extends Animal> box) { Animal animal = box.get(); System.out.println("이름 = " + animal.getName()); return animal; } }
public class WildcardMain1 { public static void main(String[] args) { Box<Object> objBox = new Box<>(); Box<Dog> dogBox = new Box<>(); Box<Cat> catBox = new Box<>(); dogBox.set(new Dog("멍멍이", 100)); WildcardEx.printGenericV1(dogBox); WildcardEx.printWildcardV1(dogBox); WildcardEx.printGenericV2(dogBox); WildcardEx.printWildcardV2(dogBox); Dog dog = WildcardEx.printAndReturnGeneric(dogBox); Animal animal = WildcardEx.printAndReturnWildcard(dogBox); } } // 실행결과 // T = Animal{name='멍멍이', size=100} // ? = Animal{name='멍멍이', size=100} // 이름 = 멍멍이 // 이름 = 멍멍이 // 이름 = 멍멍이 // 이름 = 멍멍이
비제한 와일드카드
// 이것은 제네릭 메서드이다 // Box<Dog> dogBox를 전달한다 타입 추론에 의해 타입 T가 Dog가 된다 static <T> void printGenericV1(Box<T> box) { System.out.println("T = " + box.get()); } // 이것은 제네릭 메서드가 아니다 일반적인 메서드이다 // Box<Dog> dogBox를 전달한다 와일드카드 ?는 모든 타입을 받을 수 있다 static void printWildcardV1(Box<?> box) { System.out.println("? = " + box.get()); }
- 두 메서드는 비슷한 기능을 하는 코드이지만 하나는 제네릭 메서드를 사용하고 하나는 일반적인 메서드에 와일드카 드를 사용했다
- 와일드카드는 제네릭 타입이나 제네릭 메서드를 정의할 때 사용하는 것이 아니다 Box<Dog>처럼 타입 인자가 정해지 제너릭 타입을 전달 받아서 활용할 때 사용한다
- 와일드카드인 ?는 모든 타입을 받을 수 있다는 뜻이다
- ? == <? extends Object>
- 이렇게 ?만 사용해서 제한 없이 모든 타입을 다 받을 수 있는 와일드카드를 비제한 와일드카드라고 한다
제네릭 메서드 실행 예시
// 1. 전달 printGenericV1(dogBox) // 2. 제네릭 타입 결정 dogBox는 Box<Dog> 타입, 타입 추론 -> T의 타입은 Dog static <T> void printGenericV1(Box<T> box) { System.out.println("T = " + box.get()); } // 3. 타입 인자 결정 static <Dog> void printGenericV1(Box<Dog> box) { System.out.println("T = " + box.get()); } // 4. 최종 실행 메서드 static void printGenericV1(Box<Dog> box) { System.out.println("T = " + box.get()); }
와일드 카드 실행 예시
// 1. 전달 printWildcardV1(dogBox) // 이것은 제네릭 메서드가 아니다. 일반적인 메서드이다 // 2. 최종 실행 메서드, 와일드카드 ?는 모든 타입을 받을 수 있다 static void printWildcardV1(Box<?> box) { System.out.println("? = " + box.get()); }
제네릭 메서드 vs 와일드카드
제네릭 타입이나 제네릭 메서드를 정의하는게 꼭 필요한 상황이 아니라면, 더 단순한 와일드카드 사용을 권장한다
와일드카드 2
상한 와일드카드
static <T extends Animal> void printGenericV2(Box<T> box) { T t = box.get(); System.out.println("이름 = " + t.getName()); } static void printWildcardV2(Box<? extends Animal> box) { Animal animal = box.get(); System.out.println("이름 = " + animal.getName()); }
- 제네릭 메서드와 마찬가지로 와일드카드에도 상한 제한을 둘 수 있다
- ? extends Animal 을 통해 Animal과 그 하위 타입만 입력 받는다
- box.get()을 통해 꺼낼 수 있는 타입의 최대 부모는 Animal이다 따라서 Animal 타입으로 조회할 수 있다
- 결과적으로 Animal 타입의 기능을 호출할 수 있다
타입 매개변수가 꼭 필요한 경우
와일드카드는 제네릭을 정의할 때 사용하는 것이 아니다
Box<Dog>, Box<Cat>처럼 타입 인자가 전달된 제네릭 타입을 활용할 때 사용한다
다음과 같은 경우에는 제네릭 타입이나 제네릭 메서드를 사용해야 문제를 해결할 수 있다
static <T extends Animal> T printAndReturnGeneric(Box<T> box) { T t = box.get(); System.out.println("이름 = " + t.getName()); return t; } static Animal printAndReturnWildcard(Box<? extends Animal> box) { Animal animal = box.get(); System.out.println("이름 = " + animal.getName()); return animal; }
- printAndReturnGeneric()은 다음과 같이 전달한 타입을 명확하게 반환할 수 있다
- Dog dog = WildcardEx.printAndReturnGeneric(dogBox)
- printAndReturnWildcard()의 경우 전달한 타입을 명확하게 반환할 수 없다 (Animal 타입으로 반환)
- Animal animal = WildcardEx.printAndReturnWildcard(dogBox)
- 메서드의 타입들을 특정 시점에 변경하려면 제네릭 타입이나, 제네릭 메서드를 사용해야 한다
- 와일드카드는 이미 만들어진 제네릭 타입을 전달 받아서 활용할 때 사용한다 따라서 메서드의 타입들을 타입 인자를 통 해 변경할 수 없다
- 정리하면 제네릭 타입이나 제네릭 메서드가 꼭 필요한 상황이면 <T>를 사용하고, 그렇지 않은 상황이면 와일드카드를 사용하는 것을 권장한다
하한 와일드 카드
public class WildcardMain2 { public static void main(String[] args) { Box<Object> objBox = new Box<>(); Box<Animal> animalBox = new Box<>(); Box<Dog> dogBox = new Box<>(); Box<Cat> catBox = new Box<>(); // Animal 포함 상위 타입 전달 가능 writeBox(objBox); writeBox(animalBox); // writeBox(dogBox); // 하한이 Animal // writeBox(catBox); // 하한이 Animal Animal animal = animalBox.get(); System.out.println("animal = " + animal); } static void writeBox(Box<? super Animal> box) { box.set(new Dog("멍멍이", 100)); } }
Box<? super Animal> box
?가 Animal 타입을 포함한 Animal 타입의 상위 타입만 입력 받을 수 있다는 뜻이다
하한을 Animal로 제한했기 때문에 Animal의 하위 타입인 Box<Dog>는 전달할 수 없다
타입 이레이저
제네릭은 자바 컴파일 단계에서만 사용되고, 컴파일 이후에는 제네릭 정보가 삭제된다
제네릭에 사용한 타입 매개변수 가 모두 사라지는 것이다
쉽게 이야기해서 컴파일 전인 .java에는 제네릭의 타입 매개변수가 존재하지만, 컴파일 이후인 자바 바이트코드 .class 에는 타입 매개변수가 존재하지 않는 것이다
제네릭 타입 선언
public class GenericBox<T> { private T value; public void set(T value) { this.value = value; } public T get() { return value; } }
제네릭 타입에 Integer 타입 인자 전달
void main() { GenericBox<Integer> box = new GenericBox<Integer>(); box.set(10); Integer result = box.get(); }
이렇게 하면 자바 컴파일러는 컴파일 시점에 타입 매개변수와 타입 인자를 포함한 제네릭 정보를 활용해서 new GenericBox<Integer>()에 대해 다음과 같이 이해한다
public class GenericBox<Integer> { private Integer value; public void set(Integer value) { this.value = value; } public Integer get() { return value; } }
컴파일이 모두 끝나면 자바는 제네릭과 관련된 정보를 삭제한다
이때 .class에 생성된 정보는 다음과 같다
컴파일 후 GenericBox.class
public class GenericBox { private Object value; public void set(Object value) { this.value = value; } public Object get() { return value; } }
상한 제한 없이 선언한 타입 매개변수 T는 Object로 변환된다
void main() { GenericBox box = new GenericBox(); box.set(10); Integer result = (Integer) box.get(); // 컴파일러가 캐스팅 추가 }
- 값을 반환 받는 부분을 Object로 받으면 안된다 자바 컴파일러는 제네릭에서 타입 인자로 지정한 Integer로 캐스팅하는 코드를 추가해준다
- 이렇게 추가된 코드는 자바 컴파일러가 이미 검증하고 추가했기 때문에 문제가 발생하지 않는다
타입 매개변수 제한의 경우
컴파일 전
public class AnimalHospitalV3<T extends Animal> { private T animal; public void set(T animal) { this.animal = animal; } public void checkup() { System.out.println("동물 이름: " + animal.getName()); System.out.println("동물 크기: " + animal.getSize()); animal.sound(); } public T getBigger(T target) { return animal.getSize() > target.getSize() ? animal : target; } } // 사용 코드 예시 AnimalHospitalV3<Dog> hospital = new AnimalHospitalV3<>(); ... Dog dog = animalHospitalV3.getBigger(new Dog());
컴파일 후
public class AnimalHospitalV3 { private Animal animal; public void set(Animal animal) { this.animal = animal; } public void checkup() { System.out.println("동물 이름: " + animal.getName()); System.out.println("동물 크기: " + animal.getSize()); animal.sound(); } public Animal getBigger(Animal target) { return animal.getSize() > target.getSize() ? animal : target; } } // 사용 코드 예시 AnimalHospitalV3 hospital = new AnimalHospitalV3(); ... Dog dog = (Dog) animalHospitalV3.getBigger(new Dog());
- T의 타입 정보가 제거되어도 상한으로 지정한 Animal 타입으로 대체되기 때문에 Animal 타입의 메서드를 사용하는데는 아무런 문제가 없다
- 반환 받는 부분을 Animal로 받으면 안되기 때문에 자바 컴파일러가 타입 인자로 지정한 Dog로 캐스팅하는 코드를 넣어준다
자바의 제네릭 타입은 컴파일 시점에만 존재하고, 런타임 시에는 제네릭 정보가 지워지는데, 이것을 타입 이레이저라 한다
타입 이레이저 방식의 한계
컴파일 이후에는 제네릭의 타입 정보가 존재하지 않는다
.class로 자바를 실행하는 런타임에는 지정한 Box<Integer>, Box<String>의 타입 정보가 모두 제거된다
따라서 런타임에 타입을 활용하는 다음과 같은 코드는 작성할 수 없다
소스 코드
class EraserBox<T> { public boolean instanceCheck(Object param) { return param instanceof T; // 오류 } public void create() { return new T(); // 오류 } }
런타임
class EraserBox { public boolean instanceCheck(Object param) { return param instanceof Object; // 오류 } public void create() { return new Object(); // 오류 } }
- 여기서 T는 런타임에 모두 Object가 된다
- instanceof는 항상 Object와 비교하게 된다 따라서 항상 참이 반환되는 문제가 발생한다
- 자바는 이런 문제 때문에 타입 매개변수에 instanceof 허용하지 않는다
- new T는 항상 new Object가 된다
- 따라서 자바는 타입 매개변수에 new를 허용하지 않는다
김영한의 실전자바 -중급 2편 ( 1 강 - 2 강)
728x90'자바' 카테고리의 다른 글
[JAVA] 자바 mid1 4 - 6 강 요약 (0) 2024.12.29 [JAVA] 자바 mid1 7 - 8 강 요약 (0) 2024.12.29 [JAVA / SPRING] @SuppressWarnings 어노테이션 (0) 2024.11.28 [JAVA] 자바 mid1 4 - 6 강 요약 (0) 2024.11.23 [JAVA] 자바 mid1 1 - 3 강 요약 (0) 2024.11.21