본문 바로가기

Java

[Java 복습] 타입 매개변수 제한

목차

  • 요구사항에 맞는 코드
  • 다형성 시도
  • 제네릭 도입과 실패
  • 타입 매개변수 제한

 

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