[김영한의 실전 자바 - 중급 2편] 02. 제네릭 - Generic2

Turtle·2024년 7월 6일
0
post-thumbnail

🏷️타입 매개변수 제한1 - 시작

public class AnimalHospitalMain {
	public static void main(String[] args) {
		DogHospital dogHospital = new DogHospital();
		CatHospital catHospital = new CatHospital();

		Dog dog = new Dog("멍멍이", 100);
		Cat cat = new Cat("야옹이", 300);

		dogHospital.setAnimal(dog);
		dogHospital.checkup();
		Dog findDog = dogHospital.bigger(new Dog("멍멍이2", 200));
		System.out.println(findDog);
		// dogHospital.setAnimal(cat);

		catHospital.setAnimal(cat);
		catHospital.checkup();
		Cat findCat = catHospital.bigger(new Cat("야옹이2", 100));
		System.out.println(findCat);
		// catHospital.setAnimal(dog);
	}
}
  • ✔️요구사항 : 개 병원은 개만 받을 수 있고 고양이 병원은 고양이만 받을 수 있다.
    • 개 병원과 고양이 병원을 별도의 클래스로 만들었다.
    • 각 클래스 별로 타입이 명확하기 때문에 개 병원은 개만 받을 수 있고 고양이 병원은 고양이만 받을 수 있다.
    • 타입 안전성은 보장할 수 있으나 코드의 재사용성 측면에서 보았을 때 중복이 많다.

🏷️타입 매개변수 제한2 - 다형성 시도

public class AnimalHospitalMainV2 {
	public static void main(String[] args) {
		AnimalHospital dogHospital = new AnimalHospital();
		AnimalHospital catHospital = new AnimalHospital();

		Dog dog = new Dog("멍멍이", 100);
		Cat cat = new Cat("야옹이", 100);


		dogHospital.setAnimal(dog);
		dogHospital.checkup();
		dogHospital.setAnimal(cat);

		catHospital.setAnimal(cat);
		catHospital.checkup();

		dogHospital.setAnimal(cat);

		dogHospital.setAnimal(dog);
		Dog findDog = (Dog) dogHospital.bigger(new Dog("멍멍이2", 500));
		System.out.println(findDog);
	}
}
  • ✔️정리
    • 다형성을 통해 코드 재사용이 가능하도록 했다.
    • 하지만 개 병원에 고양이를 전달이 가능하고 고양이 병원에 개를 전달하는 것이 가능한 문제가 발생한다.
    • 다형성을 적용했기 때문에 상위 타입인 Animal타입을 반환하게 되는데 Dog, Cat으로 반환하려면 다운캐스팅을 해야 한다.
    • 실수로 고양이를 입력했는데 개를 반환하는 상황이라면 캐스팅 예외가 발생한다.

🏷️타입 매개변수 제한3 - 제네릭 도입과 실패

public class AnimalHospitalV3<T> {
	private T animal;

	public T getAnimal() {
		return animal;
	}

	public void setAnimal(T animal) {
		this.animal = animal;
	}

	// 컴파일 오류
	/*
	public void checkup() {
		System.out.println("동물 이름 : " + T.getName());
		System.out.println("동물 크기 : " + T.getSize());
	} */


	// 컴파일 오류
	/*
	public T bigger(T target) {
		return animal.getSize() > target.getSize() ? animal : target;
	} */
}
  • ✔️정리
    • <T>를 사용해서 제네릭 타입을 선언했다.
    • 제네릭 타입을 선언하면 자바 컴파일러 입장에서 T에 어떤 값이 들어올지 예측할 수 없다.
    • <T>에는 타입 인자로 Integer가 들어올 수도 있고 Dog가 들어올 수도 있다. 물론 Object도 들어올 수 있다.
    • 자바 컴파일러는 어떤 타입이 들어올 지 알 수 없기 때문에 T를 어떤 타입이든 받을 수 있는 모든 객체의 최종 부모인 Object 타입으로 가정한다.
    • 따라서 Object가 제공하는 메서드만을 호출할 수 있는 것이다.
    • 원하는 기능을 사용하려면 Animal 타입이 제공하는 기능들이 필요한데 이 기능을 모두 사용할 수 없게 된 것이다.
    • 최종 목표는 동물과 전혀 관계 없는 타입이 들어올 우려가 있는 것인데 최소한 Animal이나 그 자식을 타입 인자로 제한하려고 하는 것이다.

🏷️타입 매개변수 제한4 - 타입 매개변수 제한

public class AnimalHospitalV4<T extends Animal> {
	private T animal;

	public T getAnimal() {
		return animal;
	}

	public void setAnimal(T animal) {
		this.animal = animal;
	}

	public void checkup() {
		System.out.println("동물 이름: " + animal.getName());
		System.out.println("동물 크기: " + animal.getSize());
	}

	public T bigger(T target) {
		return animal.getSize() > target.getSize() ? animal : target;
	}
}
  • ✔️정리
    • 타입 매개변수에 입력될 수 있는 상한선(extends)을 지정해서 문제를 해결했다.
    • 제네릭 클래스 안에서 Animal의 기능을 사용할 수 있다.
    • 타입 안전성X 문제 해결
      • 개 병원에 고양이를 전달하는 문제가 발생한다. → 해결
      • Animal 타입을 반환하기 때문에 다운캐스팅을 해야 한다. → 해결
      • 실수로 고양이를 입력했는데 개를 반환하는 상황이라면 캐스팅 예외가 발생한다. → 해결
    • 제네릭 도입 문제 해결
      • 제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있다. → 해결
      • 어떤 타입이든 수용할 수 있는 Object로 가정하고 Object의 기능만 사용할 수 있다. → 해결

🏷️제네릭 메서드

public class GenericMethod {
	public static Object objectMethod(Object object) {
		System.out.println("object = " + object);
		return object;
	}

	public static <T> T genericMethod(T obj) {
		System.out.println("obj = " + obj);
		return obj;
	}

	public static <T extends Number> T numberMethod(T t) {
		System.out.println("t = " + t);
		return t;
	}
}
  • ✔️제네릭 타입
    • 정의 : GenericClass<T>
    • 타입 인자 전달 : 객체를 생성하는 시점
  • ✔️제네릭 메서드
    • 정의 : <T> T genericMethod(T t)
    • 타입 인자 전달 : 메서드를 호출하는 시점
    • 제네릭 메서드는 클래스 전체가 아니라 특정 단위로 메서드를 도입할 때 사용한다.
    • 제네릭 메서드를 정의할 때는 메서드 반환 타입 왼쪽에 <T>와 같이 타입 매개변수를 사용한다.
    • 제네릭 메서드는 실제 호출하는 시점에 다이아몬드를 사용해서 <Integer>와 같이 타입을 정하고 호출한다.
  • ✔️제네릭 메서드 - 인스턴스 메서드, static 메서드
    • 제네릭 메서드의 타입 인자 전달 시점은 메서드를 호출하는 시점이다.
    • 따라서 둘 다 적용이 가능하다.
  • ✔️제네릭 타입 - 인스턴스 메서드, static 메서드
    • 제네릭 타입의 타입 인자 전달 시점은 객체를 생성하는 시점이다.
    • 하지만 static 메서드는 클래스 단위로 작동하기 때문에 제네릭 타입과는 무관하다.
    • 따라서 static 메서드에 제네릭을 도입하려면 제네릭 메서드를 사용해야 한다.

🏷️제네릭 메서드 활용

public class AnimalMethod {
	// checkup 메서드
	public static <T extends Animal> void checkup(T t) {
		System.out.println("동물 이름: " + t.getName());
		System.out.println("동물 크기: " + t.getSize());
	}

	// bigger 메서드
	public static <T extends Animal> T bigger(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("야옹이", 50);

		AnimalMethod.checkup(dog);
		AnimalMethod.checkup(cat);

		Dog targetDog = new Dog("멍멍이2", 300);
		Cat targetCat = new Cat("야옹이2", 300);

		System.out.println(AnimalMethod.bigger(dog, targetDog));
		System.out.println(AnimalMethod.bigger(cat, targetCat));
	}
}

🏷️와일드카드1

public class WildCardEx {
	static <T> void printGenericV1(Box<T> box) {
		System.out.println("T = " + box.getValue());
	}

	static void printWildCardV1(Box<?> box) {
		System.out.println("? = " + box.getValue());
	}

	static <T extends Animal> void printGenericV2(Box<T> box) {
		T t = box.getValue();
		System.out.println("T = " + box.getValue());
		System.out.println("이름: " + t.getName());
		System.out.println("크기: " + t.getSize());
	}

	static void printWildCardV2(Box<? extends Animal> box) {
		Animal value = box.getValue();
		System.out.println("value = " + value);
	}

	static <T extends Animal> T printGenericV3(Box<T> box) {
		T t = box.getValue();
		System.out.println("이름: " + t.getName());
		return t;
	}

	static Animal printAndReturnGeneric(Box<? extends Animal> box) {
		Animal value = box.getValue();
		System.out.println("value = " + value);
		return value;
	}
}
public class WildCardMain {
	public static void main(String[] args) {
		Box<Object> objectBox = new Box<>();
		Box<Dog> dogBox = new Box<>();
		Box<Cat> catBox = new Box<>();

		dogBox.setValue(new Dog("멍멍이", 100));
		WildCardEx.printGenericV1(dogBox);
		WildCardEx.printGenericV2(dogBox);
		Dog dog = WildCardEx.printGenericV3(dogBox);
		System.out.println(dog);

		catBox.setValue(new Cat("야옹이", 100));
		WildCardEx.printWildCardV1(catBox);
		WildCardEx.printWildCardV2(catBox);
		Animal animal = WildCardEx.printAndReturnGeneric(catBox);
		System.out.println("animal = " + animal);
	}
}
  • ✔️와일드카드
    • 와일드카드라는 뜻은 컴퓨터 프로그래밍에서 *, ?와 같이 하나 이상의 문자들을 상징하는 특수 문자를 뜻한다.
    • 쉽게 이야기해서 여러 타입이 들어올 수 있다.
    • 와일드카드는 이미 만들어진 제네릭 타입을 활용할 때 사용하는 것이다.
    • 와일드카드인 ?는 모든 타입을 다 받을 수 있다는 뜻이다.
      • 다음과 같이 해석할 수 있다. → ? == <? extends Object>
    • 이렇게 ?만 사용해서 제한 없이 모든 타입을 다 받을 수 있는 와일드카드를 비제한 와일드카드라 한다.
  • ✔️제네릭 메서드 vs 와일드카드
    • 제네릭 메서드에는 타입 매개변수가 존재한다.
    • 특정 시점에 타입 매개변수에 타입 인자를 전달해서 타입을 결정해야 한다.
    • 와일드카드는 일반적인 메서드에 사용할 수 있고 단순히 매개변수로 제네릭 타입을 받을 수 있다는 것 뿐이다.
    • 제네릭 메서드처럼 타입을 결정하거나 복잡하게 작동하지 않는다.
    • 단순히 일반 메서드에 제네릭 타입을 받을 수 있는 매개변수가 하나 있는 것 뿐이다.
    • 제네릭 타입이나 제네릭 메서드를 정의하는 꼭 필요한 상황이 아니라면 단순한 와일드카드 사용을 권장한다.

🏷️와일드카드2

public class WildCardEx {
	// 제네릭 사용
	static <T> void printGenericV1(Box<T> box) {
		System.out.println("T = " + box.getValue());
	}

	// 와일드카드 사용
	static void printWildCardV1(Box<?> box) {
		System.out.println("? = " + box.getValue());
	}

	static <T extends Animal> void printGenericV2(Box<T> box) {
		T t = box.getValue();
		System.out.println("T = " + box.getValue());
		System.out.println("이름: " + t.getName());
		System.out.println("크기: " + t.getSize());
	}

	static void printWildCardV2(Box<? extends Animal> box) {
		Animal value = box.getValue();
		System.out.println("value = " + value);
	}

	static <T extends Animal> T printGenericV3(Box<T> box) {
		T t = box.getValue();
		System.out.println("이름: " + t.getName());
		return t;
	}

	static Animal printAndReturnGeneric(Box<? extends Animal> box) {
		Animal value = box.getValue();
		System.out.println("value = " + value);
		return value;
	}
}
  • ✔️상한 와일드카드
    • 제네릭 메서드와 마찬가지로 와일드카드에도 상한 제한을 둘 수 있다.
    • <? extends Animal>을 지정했다.
    • Animal과 같이 그 하위 타입만 입력을 받는다. 만약 다른 타입을 입력하면 컴파일 오류가 발생한다.
    • box.getValue()을 통해 꺼낼 수 있는 타입의 최대 부모는 Animal이 된다. 따라서 Animal타입으로 조회할 수 있다.
    • 결과적으로 Animal 타입의 기능을 호출할 수 있다.
  • ✔️타입 매개변수가 꼭 필요한 경우
    • 제네릭 타입이나 제네릭 메서드가 꼭 필요한 상황이면 <T>를 사용하고, 그렇지 않은 상황이라면 와일드카드를 사용하는 것을 권장한다.
  • ✔️하한 와일드카드
    • 하한을 Animal로 제한했기 때문에 Animal을 상속받는 자식들은 전달할 수 없다.
public class WildCardMain1 {
	public static void main(String[] args) {
		Box<Object> objectBox = new Box<>();
		Box<Animal> animalBox = new Box<>();
		Box<Cat> catBox = new Box<>();
		Box<Dog> dogBox = new Box<>();

		writeBox(objectBox);
		writeBox(animalBox);
		// writeBox(catBox);  → 고양이 박스
		// writeBox(dogBox);  → 개 박스
	}

	static void writeBox(Box<? super Animal> box) {
		box.setValue(new Dog("멍멍이", 100));
		System.out.println("box.value = " + box.getValue());

		box.setValue(new Cat("고양이", 100));
		System.out.println("box.value = " + box.getValue());
	}
}

🏷️타입 이레이저

  • ✔️타입 이레이저
    • 제네릭은 자바 컴파일 단계에서만 사용되고, 컴파일 이후에는 제네릭 정보가 삭제된다.
    • 제네릭에 사용한 타입 매개변수가 모두 사라지는 것이다.
    • 컴파일 전인 .java에서는 제네릭의 타입 매개변수가 존재하지만, 컴파일 이후인 자바 바이트코드 .class에는 타입 매개변수가 존재하지 않는다.

0개의 댓글