목차
- 요구사항에 맞는 코드
- 다형성 시도
- 제네릭 도입과 실패
- 타입 매개변수 제한
public class Animal {
private String name ="";
private int size;
public Animal(String name, int size) {
this.name = name;
this.size = size;
}
public String getName() {
return name;
}
public int getSize() {
return size;
}
public void sound() {
System.out.println("동물 울음 소리.");
}
@Override
public String toString() {
return "Animal{" +
"name='" + name + '\'' +
", size=" + size +
'}';
}
}
public class Dog extends Animal{
public Dog(String name, int size) {
super(name, size);
}
@Override
public void sound() {
System.out.println("멍멍!");
}
}
public class DogHospital {
private Dog animal;
public void set(Dog animal) {
this.animal = animal;
}
public void checkUp() {
System.out.println("동물 이름 :" + animal.getName());
System.out.println("동물 크기 :" + animal.getSize());
}
public Dog bigger(Dog target) {
return animal.getSize() > target.getSize() ? animal : target;
}
}
public class AnimalHospitalV0 {
public static void main(String[] args) {
DogHospital dogHospital = new DogHospital();
CatHospital catHospital = new CatHospital();
Dog dog = new Dog("바둑이1", 100);
Cat cat = new Cat("야옹이1", 50);
// 개 병원
dogHospital.set(dog);
dogHospital.checkUp();
// 고양이 병원
catHospital.set(cat);
catHospital.checkUp();
// 문제1: 개 병원에 고양이 전달
// dogHospital.checkUp(cat); // 다른 타입을 입력: 컴파일 오류
// 문제2: 개 타입을 반환
dogHospital.set(dog);
Dog biggerDog = dogHospital.bigger(new Dog("멍멍이2", 200));
System.out.println("biggerDog = " + biggerDog);
요구사항: 개 병원은 개만 받을 수 있고, 고양이 병원은 고양이만 받을 수 있어야 한다.
- 여기서는 개 병원과 고양이 병원을 각각 별도의 클래스로 만들었다.
- 각 클래스 별로 타입이 명확하기 때문에 개 병원은 개만 받을 수 있고, 고양이 병원은 고양이만 받을 수 있다.
- 따라서 개 병원에 고양이를 전달하면 컴파일 오류가 발생한다.
- 그리고 개 병원에서 bigger() 로 다른 개를 비교하는 경우 더 큰 개를 Dog 타입으로 반환한다.
문제
- 코드 재사용X: 개 병원과 고양이 병원은 중복이 많이 보인다.
- 타입 안전성O: 타입 안전성이 명확하게 지켜진다.
다형성 시도
Dog, Cat은 Animal 이라는 명확한 부모 타입이 있다. 다형성을 사용해서 중복을 제거해보자!
public class AnimalHospital {
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;
}
}
public class AnimalHospitalMainV1 {
public static void main(String[] args) {
AnimalHospitalV1 dogHospital = new AnimalHospitalV1();
AnimalHospitalV1 catHospital = new AnimalHospitalV1();
Dog dog = new Dog("바둑이1", 100);
Cat cat = new Cat("야옹이1", 50);
// 개 병원
dogHospital.set(dog);
dogHospital.checkUp();
// 고양이 병원
catHospital.set(cat);
catHospital.checkUp();
// 문제1: 개 병원에 고양이 전달
dogHospital.set(cat); // 매개 변수 체크 실패: 컴파일 오류가 발생하지 않음
// 문제2: 개 타입을 반환
dogHospital.set(dog);
Dog biggerDog =(Dog)dogHospital.getBigger(new Dog("멍멍이2", 200));
System.out.println("biggerDog = " + biggerDog);
}
}
동물 이름: 바둑이1
동물 크기: 100
멍멍!
동물 이름: 야옹이1
동물 크기: 50
냐용먀용!
biggerDog = Animal{name='멍멍이2', size=200}
문제
- 코드 재사용O: 다형성을 통해 AnimalHospitalV1 하나로 개와 고양이를 모두 처리한다.
- 타입 안전성X
- 개 병원에 고양이를 전달하는 문제가 발생한다.
- 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);
}
public T getBigger(T target) {
// 컴파일 오류
//return animal.getSize() > target.getSize() ? animal : target;
return null;
}
}
- 제네릭 타입을 선언하면 자바 컴파일러 입장에서 T 에 어떤 값이 들어올지 예측할 수 없다.
- 우리는 Animal 타입의 자식이 들어오기를 기대했지만, 여기 코드 어디에도 Animal 에 대한 정보는 없다.
- T 에는 타입 인자로 Integer 가 들어올 수 도 있고, Dog 가 들어올 수도 있다. 물론 Object 가 들어올 수도 있다.
다양한 타입 인자
AnimalHospitalV2<Dog> dogHospital = new AnimalHospitalV2<>();
AnimalHospitalV2<Cat> catHospital = new AnimalHospitalV2<>();
AnimalHospitalV2<Integer> integerHospital = new AnimalHospitalV2<>();
AnimalHospitalV2<Object> objectHospital = new AnimalHospitalV2<>();
- 자바 컴파일러는 어떤 타입이 들어올 지 알 수 없기 때문에 T 를 어떤 타입이든 받을 수 있는 모든 객체의 최종 부모인
Object 타입으로 가정한다. - 따라서 Object 가 제공하는 메서드만 호출할 수 있다.
- 원하는 기능을 사용하려면 Animal 타입이 제공하는 기능들이 필요한데, 이 기능을 모두 사용할 수 없다.
- 여기에 추가로 한가지 문제가 더 있다. 바로 동물 병원에 Integer , Object 같은 동물과 전혀 관계 없는 타입을 타입
인자로 전달 할 수 있다는 점이다. - 우리는 최소한 Animal 이나 그 자식을 타입 인자로 제한하고 싶다.
public class AnimalHospitalMainV2 {
public static void main(String[] args) {
AnimalHospitalV2<Dog> dogHospital = new AnimalHospitalV2<>();
AnimalHospitalV2<Cat> catHospital = new AnimalHospitalV2<>();
AnimalHospitalV2<Animal> animalHospital = new AnimalHospitalV2<>();
AnimalHospitalV2<Integer> integerHospital = new AnimalHospitalV2<>();
}
}
문제
- 제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있다.
- 따라서 타입 매개변수를 어떤 타입이든 수용할 수 있는 Object 로 가정하고, Object 의 기능만 사용할 수 있다.
- 발생한 문제들을 생각해보면 타입 매개변수를 Animal 로 제한하지 않았기 때문이다.
- 만약 타입 인자가 모두 Animal과 그 자식만 들어올 수 있게 제한한다면 어떨까?
타입 매개변수 제한
public class AnimalHospitalV3<T extends Animal> { // Animal ~ 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> 이다. 타입 매개변수 T 를 Animal 과 그 자식만 받을 수 있도록 제한을 두는 것이다.
즉 T 의 상한이 Animal 이 되는 것이다. 이렇게 하면 타입 인자로 들어올 수 있는 값이 Animal 과 그 자식으로 제한된다.
AnimalHospitalV3<Animal>
AnimalHospitalV3<Dog>
AnimalHospitalV3<Cat>
이제 자바 컴파일러는 T 에 입력될 수 있는 값의 범위를 예측할 수 있다.
타입 매개변수 T 에는 타입 인자로 Animal , Dog , Cat 만 들어올 수 있다.
따라서 이를 모두 수용할 수 있는 Animal 을 T 의 타입으로 가정해도 문제가 없다.
따라서 Animal 이 제공하는 getName() , getSize() 같은 기능을 사용할 수 있다.
public class AnimalHospitalMainV3 {
public static void main(String[] args) {
AnimalHospitalV3<Dog> dogHospital = new AnimalHospitalV3<>();
AnimalHospitalV3<Cat> catHospital = new AnimalHospitalV3<>();
Dog dog = new Dog("바둑이1", 100);
Cat cat = new Cat("야옹이1", 50);
// 개 병원
dogHospital.set(dog);
dogHospital.checkUp();
// 고양이 병원
catHospital.set(cat);
catHospital.checkUp();
// 문제1: 개 병원에 고양이 전달
// dogHospital.set(cat); // 다른 타입 입력: 컴파일 오류
// 문제2: 개 타입을 반환
dogHospital.set(dog);
Dog biggerDog = dogHospital.getBigger(new Dog("멍멍이2", 200));
System.out.println("biggerDog = " + biggerDog);
}
}
기존 문제와 해결
타입 안전성X 문제
- 개 병원에 고양이를 전달하는 문제가 발생한다. 해결
- Animal 타입을 반환하기 때문에 다운 캐스팅을 해야 한다. 해결
- 실수로 고양이를 입력했는데, 개를 반환하는 상황이라면 캐스팅 예외가 발생한다. 해결
제네릭 도입 문제
- 제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있다. 해결
- 그리고 어떤 타입이든 수용할 수 있는 Object 로 가정하고, Object 의 기능만 사용할 수 있다. 해결
- 여기서는 Animal 을 상한으로 두어서 Animal 의 기능을 사용할 수 있다.
정리
제네릭에 타입 매개변수 상한을 사용해서 타입 안전성을 지키면서 상위 타입의 원하는 기능까지 사용할 수 있었다.
덕분에 코드 재사용과 타입 안전성이라는 두 마리 토끼를 동시에 잡을 수 있었다.
'Java' 카테고리의 다른 글
[Java] JVM 메모리 구조 탐구 (0) | 2024.10.09 |
---|---|
[Java] Jshell 을 아시나요? (1) | 2024.09.26 |
[Java] 지네릭 타입의 형변환 (0) | 2024.05.10 |
[Java] 와일드 카드, 지네릭 메서드 (0) | 2024.05.10 |
[Java] 지네릭 클래스의 제약 (0) | 2024.05.10 |