○ 제네릭이 필요한 이유
다양한 타입을 담는 박스가 필요하면 그때마다 각 박스 클래스를 새로 만들어야한다.
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);
}
}
Object는 모든 타입의 부모인점을 이용해 문제해결을 시도해볼 수 있다.
하지만 반환타입을 다운캐스팅해야하고, 잘못된 타입 인수를 전달시 꺼낼때 캐스팅예외가 발생할 수 있다.
ObjectBox integerBox = new ObjectBox();
integerBox.set(10);
Integer integer = (Integer) integerBox.get(); //Object -> Integer 캐스팅
BoxMain1 : 각각의 타입별로 IntegerBox , StringBox 와 같은 클래스를 모두 정의
- 코드 재사용X
- 타입 안전성O
BoxMain2 : ObjectBox 를 사용해서 다형성으로 하나의 클래스만 정의
- 코드 재사용O
- 타입 안전성X
○ 제네릭 적용
제네릭을 사용하면 코드 재사용과 타입 안전성이라는 두 마리 토끼를 한 번에 잡을 수 있다.
public class GenericBox<T> {
private T value;
public void set(T value){
this.value = value;
}
public T get() {
return value;
}
}
제네릭 클래스는 생성 시점에 <>사이에 원하는 타입을 지정한다.자바 컴파일러는 입력한 타입 정보를 기반으로 컴파일 과정에 타입 정보를반영한다.
new GenericBox<Integer>()
제네릭 클래스는 타입 추론이 가능하기때문에 타입정보를 생략할 수 있다.
GenericBox<Integer> integerBox = new GenericBox<Integer>() // 타입 직접 입력
GenericBox<Integer> integerBox2 = new GenericBox<>() // 타입 추론
○ 제네릭 용어와 관례
제네릭의 핵심은 사용할 타입을 미리 결정하지 않는다는 점이다.
메서드는 매개변수에 인자를 전달해서 사용할 값을 결정한다.
제네릭 클래스는 타입 매개변수에 타입 인자를 전달해서 사용할 타입을 결정한다.
🟢용어 정리
- 제네릭(Generic)
- 제네릭이라는 단어는 일반적인, 범용적인이라는 영어 단어 뜻이다. 풀어보면 특정 타입에 속한 것이 아니라 일반적으로, 범용적으로 사용할 수 있다는 뜻이다.
- 제네릭 타입 (Generic Type)
- 클래스나 인터페이스를 정의할 때 타입 매개변수를 사용하는 것을 말한다.
- 제네릭 클래스, 제네릭 인터페이스를 모두 합쳐서 제네릭 타입이라 한다.
- 타입은 클래스, 인터페이스, 기본형( int 등)을 모두 합쳐서 부르는 말이다.
- 예: class GenericBox<T> { private T t; } 여기에서 GenericBox<T> 를 제네릭 타입이라 한다.
- 타입 매개변수 (Type Parameter)
- 제네릭 타입이나 메서드에서 사용되는 변수로, 실제 타입으로 대체된다.
- 예: GenericBox<T> 여기에서 T 를 타입 매개변수라 한다.
- 타입 인자 (Type Argument)
- 제네릭 타입을 사용할 때 제공되는 실제 타입이다.
- 예: GenericBox 여기에서 Integer 를 타입 인자라 한다.
🟢제네릭 명명 관례
- E - Element
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
제네릭 타입<>을 지정하지않는것을, raw type 또는 원시타입이라고 한다. 이땐 타입 매개변수에 Object가 사용된다고 이해하면된다. (로 타입은 사용하지 않아야 한다!!!)
GenericBox integerBox = new GenericBox();
○ 타입 매개변수 제한
▪️요구사항: 동물병원을 만들자. 개 병원은 개만 받을 수 있고, 고양이 병원은 고양이만 받을 수 있어야 한다.
위를 위해 제네릭 타입을 선언하면 자바 컴파일러 입장에서 T 에 어떤 값이 들어올지 예측할 수 없다. 따라서 T를 모든 객체의 최종 부모인 Object 타입으로 가정한다. 따라서 Object 가 제공하는 메서드만 호출할 수 있고, Animal 타입 제공 기능들을 사용할 수 없다.
public class AnimalHospitalV2<T> {
private T animal;
public void set(T animal){
this.animal = animal;
}
public void checkup(){
//T의 타입을 메서드를 정의하는 시점에는 알수없다. Object 기능만 사용가능
animal.toString();
animal.equals(null);
// 컴파일 오류
//System.out.println("동물 이름: " + animal.getName());
//animal.sound();
}
public T getBigger(T target){
// 컴파일 오류
// return animal.getSize() > target.getSize() ? animal : target;
return null;
}
}
//문제: 제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있다.
AnimalHospitalV2<Dog> dogHospital = new AnimalHospitalV2<>();
AnimalHospitalV2<Cat> catHospital = new AnimalHospitalV2<>();
AnimalHospitalV2<Integer> integerHospital = new AnimalHospitalV2<>();
AnimalHospitalV2<Object> objectHospital = new AnimalHospitalV2<>();
따라서 아래처럼 타입 매개변수 T를 Animal과 그 자식만 받을 수있도록 제한할 수 있다. 즉 T의 상한이 Animal이 된다.
이제 자바 컴파일러는 T에 입력될 수 있는 값 범위를 예측할 수 있다.
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;
}
}
정리해보면, 제네릭에 타입 매개변수 상한을 사용해서 타입 안전성을 지키면서 상위 타입의 원하는 기능까지 사용할 수 있었다. 덕분
에 코드 재사용과 타입 안전성이라는 두 마리 토끼를 동시에 잡을 수 있었다.
○ 제네릭 메서드
제네릭 타입
- 정의: GenericClass<T>
- 타입 인자 전달: 객체를 생성하는 시점 ex) new GenericClass
- 클래스 전체 단위로 제네릭 도입
제네릭 메서드
- 정의: <T> T genericMethod(T t)
- 타입 인자 전달: 메서드를 호출하는 시점
- 예) GenericMethod.genericMethod(i)
- 메서드 단위로 제네릭 도입
public class GenericMethod {
public static Object objMethod(Object obj){
System.out.println("object print: " + obj);
return obj;
}
//제네릭 메서드를 정의할 때는 메서드의 반환 타입 왼쪽에 다이아몬드를 사용해서
// <T> 와 같이 타입 매개변수를 적어준다.
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;
}
}
제네릭 메서드는 인스턴스 메서드와 static메서드에 모두 적용할 수 있다.
class Box<T> { //제네릭 타입
static <V> V staticMethod2(V t) {} //static 메서드에 제네릭 메서드 도입
<Z> Z instanceMethod2(Z z) {} //인스턴스 메서드에 제네릭 메서드 도입 가능
}
제네릭 타입은 static 메서드에 타입 매개변수를 사용할 수 없다. 제네릭 타입은 객체를 생성하는 시점에 타입이 정해진 다. 그런데 static 메서드는 인스턴스 단위가 아니라 클래스 단위로 작동하기 때문에 제네릭 타입과는 무관하다. 따라서 static 메서드에 제네릭을 도입하려면 제네릭 메서드를 사용해야 한다.
제네릭 메서드도 타입 매개변수 제한이 가능하다.
public static <T extends Number> T numberMethod(T t) {}
제네릭 메서드는 타입을 추론할 수 있다. 타입을 추론해 컴파일러가 대신 처리해준다.
Integer i = 10;
Integer result = GenericMethod.<Integer>genericMethod(i);
//타입 추론, 타입 인자 생략
System.out.println("타입 추론");
Integer result2 = GenericMethod.genericMethod(i)
○ 제네릭 메서드 활용
AnimalHospitalV3 의 주요 기능을 제네릭 메서드로 만들어 사용해볼수있다.
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);
}
}
🟢 제네릭 타입과 제네릭 메서드 우선순위
정적 메서드는 제네릭 메서드만 적용할 수 있지만, 인스턴스 메서드는 제네릭 타입도 제네릭 메서드도 둘다 적용할 수 있다.
여기에 제네릭 타입과 제네릭 메서드의 타입 매개변수를 같은 이름으로 사용하면 어떻게될까.
제네릭 타입보다 제네릭 메서드가 높은 우선순위를 가진다.
따라서 printAndReturn() 은 제네릭 타입과는 무관하고 제네릭 메서드가 적용된다. (하지만 이름겹침권장X)
/**
*인스턴스 메서드 제네릭 타입과 제네릭 메서드의 타입 매개변수를 같은 이름으로 사용할때
* */
public class ComplexBox<T extends Animal> {
private T animal;
public void set(T animal) {
this.animal = animal;
}
public <T> T printAndReturn(T t) {
System.out.println("animal.className: " + animal.getClass().getName());
System.out.println("t.className: " + t.getClass().getName());
// t.getName(); // 호출 불가 메서드는 <T> 타입이다. <T extends Animal> 타입이 아니다.
return t;
}
}
public class MethodMain3 {
public static void main(String[] args) {
Dog dog = new Dog("멍멍이", 100);
Cat cat = new Cat("냐옹이", 50);
ComplexBox<Dog> hospital = new ComplexBox<>(); // 제네릭 타입매개변수 Dog
hospital.set(dog);
Cat returnCat = hospital.printAndReturn(cat); // 제네릭 메서드 타입매개변수 cat
System.out.println("returnCat = " + returnCat);
}
}
animal.className: generic.animal.Dog
t.className: generic.animal.Cat
returnCat = Animal{name='냐옹이', size=50}
○ 와일드카드1
이미 만들어진 제네릭 타입을 활용할 때 사용한다. (와일드카드는 제네릭 타입이나, 제네릭 메서드를 선언하는 것이아니다.)
와일드 카드인 ?는 모든 타입을 다 받을 수 있다는 뜻이다. ? == <? extends Object>
이렇게 ? 만 사용해서 제한 없이 모든 타입을 다 받을 수 있는 와일드카드를 비제한 와일드카드라 한다.
static <T> void printGenericV1(Box<T> box) { // 제네릭 메서드.타입추론으로 T가 Dog가 된다.
System.out.println("T = " + box.get());
}
static void printWildcardV1(Box<?> box) { // 일반 메서드. Box<Dog> dogBox 전달한다.
System.out.println("? = " + box.get());
}
🟢제네릭 메서드 vs 와일드카드
- 제네릭 메서드에는 특정 시점에 타입 매개변수에 타입 인자를 전달해서 타입을 결정해야 한다. 이런 과정은 매우 복잡하다.
- 와일드카드는 일반적인 메서드에 사용할 수 있고, 단순히 매개변수로 제네릭 타입을 받을 수 있는 것 뿐이다.
- 제네릭 타입이나 제네릭 메서드를 정의하는게 꼭 필요한 상황이 아니라면, 더 단순한 와일드카드 사용을 권장!
🔶상한 와일드카드
static <T extends Animal> T printAndReturnGeneric(Box<T> box) {
T t = box.get();
System.out.println("이름 = " + t.getName());
return t;
} // Dog dog = WildcardEx.printAndReturnGeneric(dogBox);
// 와일드카드에도 상한제한을 둘 수 있다.
static Animal printAndReturnWildcard(Box<? extends Animal> box) {
Animal animal = box.get();
System.out.println("이름 = " + animal.getName());
return animal;
}// Animal animal = WildcardEx.printAndReturnWildcard(dogBox);
위를보면, 첫번째 메소드는 전달타입을 명확하게 반환할 수 있다.
반면 두번째 메소드는 전달타입을 명확히 반환할 수 없다.
따라서 메서드 타입들을 특정 시점에 변경하려면 제네릭 타입이나 제너릭 메서드를 사용해야 한다.
🔶하한 와일드카드
static void writeBox(Box<? super Animal> box) { // Animal animal = animalBox.get();
box.set(new Dog("멍멍이", 100));
}
위를 보면, 하한은 Animal로 제한했기 때문에 하위타입 Box<Dog>는 전달할 수 없다.
○ 타입 이레이저 🌟
타입 이레이저: 제네릭은 자바 컴파일 단계에서만 사용되고, 컴파일 이후에는 제네릭 정보가 삭제된다.
컴파일 전인 .java 에는 제네릭의 타입 매개변수가 존재O, 컴파일 이후인 자바 바이트코드 .class 에는 존재X
컴파일 후, 상한 제한 없이 선언한 타입 매개변수 T 는 Object 로 변환된다.
// 컴파일 시점에 제네릭 정보를 이해
public class GenericBox<Integer> {
private Integer value;
public void set(Integer value) {
this.value = value;
}
public Integer get() {
return value;
}
}
// 컴파일 후 .class에는 Object로 변환
public class GenericBox {
private Object value;
public void set(Object value) {
this.value = value;
}
public Object get() {
return value;
}
예를들어 Integer 타입 매개변수 값을 받을때는, 아래처럼 타입 매개변수값으로 캐스팅하는 코드를 추가해준다.
void main() {
GenericBox box = new GenericBox();
box.set(10);
Integer result = (Integer) box.get(); //컴파일러가 캐스팅 추가
}
또한 <T extends Animal>같이 타입 매개변수를 제한했다면, 제한한 타입으로 코드를 변경한다.
🟢타입 이레이저 방식의 한계
컴파일 이후에는 지정한 T 타입정보가 모두 제거되기때문에, 런타임시 모두 Object가 된다.
따라서 아래처럼 런타임에 타입을 활용하는 코드는 작성할 수 없다.
class EraserBox<T> {
public boolean instanceCheck(Object param) {
return param instanceof T; // 오류.Object는 항상 참 반환
}
public void create() {
return new T(); // 오류.new Object는 개발자 의도와 다름
}
}
'Java' 카테고리의 다른 글
[Java] 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 섹션1 프로세스와 스레드 소개 (0) | 2024.07.16 |
---|---|
[Java] 김영한의 실전 자바 - 중급편2 섹션10 컬렉션 프레임워크 - 순회, 정렬, 전체 정리 (0) | 2024.07.08 |
[Java] 김영한의 실전 자바 - 중급편 섹션10 예외처리2 - 실습 (0) | 2024.05.23 |
[Java] 김영한의 실전 자바 - 중급편 섹션9 예외처리1 - 이론 (0) | 2024.05.22 |
[Java] 김영한의 실전 자바 - 중급편 섹션8 중첩 클래스,내부 클래스2 (0) | 2024.05.20 |