Java/김영한의 실전자바

제네릭

슈코 2024. 8. 15. 12:42

제네릭이 필요한 이유( Box 예제 )

타입 안전성을 높이면서, 코드를 재사용하기 위해

 

Integer 변수 value를 set, get을 할 수 있는 IntegerBox 클래스 생성

public class IntegerBox {

    private Integer value;

    public void set(Integer value) {
        this.value = value;
    }

    public Integer get() {
        return value;
    }
}

 

 

String 변수 value를 set, get을 할 수 있는 StringBox 클래스 생성

public class StringBox {

    private String value;

    public void set(String value) {
        this.value = value;
    }

    public String get() {
        return value;
    }
}

 

한가지 타입에 대한 같은 기능이지만, 타입을 추가할떄마다 매번 클래스를 새로 만들어야하는 불편함

 

모든 타입의 부모인 Object 타입으로 생성하여, 문제 해결 시도

public class ObjectBox {

    private Object value;

    public Object get() {
        return value;
    }

    public void set(Object value) {
        this.value = value;
    }
}

 

get을 할때 Object 타입으로 리턴을 해주기 때문에 맞는 타입을 리턴받으려면 캐스팅을 매번 해줘야 한다.

잘못된 타입(Integer만 set 하려고 했는데, String을 set)을 저장하면, 캐스팅 시 에러가 발생할 수 있다.

코드 재사용은 가능했지만, 에러가 발생할 이슈 존재

 

  • 타입별로 클래스를 생성 - 타입의 안전성 ↑  코드 재사용성 ↓
  • Object -  코드 재사용성 ↑  타입 안전성 

 

 

제네릭 클래스

  • <>를 사용한 클래스
  • <> = 다이아몬드
  • 클래스 오른쪽에 <T> 표현 = 제네릭 클래스
  • T = 타입 매개변수
  • 생성 시점에 타입을 적어 클래스 생성

 

제네릭 타입 Box로 이슈해결

public class GenericBox<T> {

    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

 

 

GenericBox<T>를 선언, Integer로 사용하고 싶으면

GenericBox<Integer> integerBox = new GenericBox<Integer>();

다음과 같이 '생성' 시점에 Integer를 적어주면 된다.

( 코드 재사용성 ↑ )

 

여기서 오른쪽에 Integer는 생략이 가능하다

( 왼쪽에 생성할때, Integer를 적어주기 때문에, 추론이 가능하다 )

GenericBox<> integerBox = new GenericBox<Integer>() ;

 

Integer로 생성하면, String으로 값을 set 하는 순간 컴파일 에러가 발생한다.

( 타입 안전성 ↑ )

 

public static void main(String[] args) {

    GenericBox<Integer> integerBox = new GenericBox<Integer>(); // 생성 시점에 T의 타입 결정(Integer로)
    integerBox.set(10);
    Integer integer = integerBox.get(); // Integer 타입만 허용, 컴파일 오류
    System.out.println("integer = " + integer);

    GenericBox<String> stringBox = new GenericBox<String>();
    stringBox.set("hello");
    String str = stringBox.get();
    System.out.println("str = " + str);

    // 원하는 모든 타입 사용 가능
    GenericBox<Double> doubleBox = new GenericBox<>();
    doubleBox.set(10.0);
    Double doubleValue = doubleBox.get();
    System.out.println("doubleValue = " + doubleValue);
    
    // 타입 추론 : 생성하는 제네릭 타입 생략 가능
    GenericBox<Integer> integerBox2 = new GenericBox<>();
}

 

제네릭의 핵심

사용할 타입을 미리 결정하지 않는다. 생성 시점에 지정한다.

 

클래스 내부에서 사용하는 타입을 정의하는 시점이 아닌, 생성하는 시점에 결정

 

 

용어정리

  • 제네릭 - 일반적인, 범용적인 뜻( 특정 타입에 속한 것 이 아니라, 범용적으로 사용할 수 있다! )
  • 제네릭 타입 - 클래스, 인터페이스 정의 시 타입 매개변수를 사용하는 것
  • 타입 매개변수 - 제네릭 타입, 메서드에서 사용하는 변수로 실제 타입으로 대체된다
  • 타입 인자 - 제네릭 타입을 사용할때, 제공되는 실제 타입
GenericBox<T>
String - Generic<String>
Integer - Generic<Integer>

T를 타입 매개변수,
String, Integer를 타입인자

 

 

메소드 매개변수 & 제네릭 매개변수

  • 메소드 매개변수 = 사용할 값에 대한 결정을 나중으로 미룬다.
  • 제네릭 매개변수 = 사용할 타입에 대한 결정을 나중으로 미룬다.

 

제네릭 명명 관례

  • E - Element
  • K - Key
  • N - Number
  • T - Type
  • V - Value

 

제네릭 기타

  • 한번에 여러 타입을 지정할 수 있다( <K, T> )
  • 항상 <>을 사용해서 사용시점에 원하는 타입을 지정해야 한다
  • <>을 생략하면, 원시타입(매개변수가 Object로 사용)
  • 원시타입은 제네릭이 생기기 전의 코드와 호환성을 위해

 


 

 

타입 매개변수 제한

  • 제네릭 사용 시 특정 타입으로 타입 매개변수를 제한할때 사용
  • <T extend 클래스명>
  • extend 옆에 붙어있는 클래스명 자신, 혹은 그 자식들만 사용 가능

 

 

Animal 객체 예시를 통하여 타입 매개변수를 제한하는 것의 장점을 파악할 수 있다.

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 +
                '}';
    }
}

name과 size를 가지는 Animal 클래스, get() 메소드와 sound(): 동물 울음소리 출력 메소드를 가진다.

 

public class Dog extends Animal {


    public Dog(String name, int size) {
        super(name, size);
    }

    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}

Animal 클래스를 상속받은 Dog 클래스

 

public class Cat extends Animal {

    public Cat(String name, int size) {
        super(name, size);
    }

    @Override
    public void sound() {
        System.out.println("야옹");
    }
}

역시 Animal 클래스를 상속받은 Cat 클래스

 

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());
        animal.sound();
    }

    public Dog bigger(Dog target) {
        return animal.getSize() > target.getSize() ? animal : target;
    }
}

Dog 클래스를 활용하여 만든 DogHospital 클래스, checkup()과 bigger() 메소드를 가진다

 

public class CatHospital {

    private Cat animal;

    public void set(Cat animal) {
        this.animal = animal;
    }

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

    public Cat bigger(Cat target) {
        return animal.getSize() > target.getSize() ? animal : target;
    }
}

Cat 클래스를 활용하여 만든 CatHospital 클래스, 역시 checkup()과 bigger() 메소드를 가진다

 

 

public class AnimalHospitalMainV0 {

    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", 300);

        // 개 병원
        dogHospital.set(dog);
        dogHospital.checkup();

        // 고양이 병원
        catHospital.set(cat);
        catHospital.checkup();

        // 문제 1: 개 병원에 고양이 전달
        //dogHospital.set(cat); 다른타입을 입력하면 컴파일 오류

        // 문제 2: 개 타입 반환
        dogHospital.set(dog);
        Dog biggerDog = dogHospital.bigger(new Dog("멍멍이2", 200));
        System.out.println("biggerDog = " + biggerDog);
    }
}

DogHospital, CatHospital 클래스를 만들어 Dog, Cat을 넣어준다.

각각의 클래스는 그에 맞는 동물 클래스만 들어갈 수 있어서 DogHospital에 Cat 객체를 넣으면 컴파일 오류가 발생한다

하지만, 같은 기능을 수행하는 중복되는 클래스를 만든 이슈가 있다.

 

 

public class AnimalHospitalV1 {

    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 bigger(Animal target) {
        return animal.getSize() > target.getSize() ? animal : target;
    }
}

Dog, Cat 클래스의 부모 클래스인 Animal 클래스를 활용한 AnimalHospital 클래스를 만들어준다.

각각 만들 필요 없이 Dog, Cat 클래스를 모두 넣을 수 있다

 

 

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", 300);

        // 개 병원
        dogHospital.set(dog);
        dogHospital.checkup();

        // 고양이 병원
        catHospital.set(cat);
        catHospital.checkup();

        // 문제 1: 개 병원에 고양이 전달
        dogHospital.set(cat);  // 매개변수 체크 실패 : 컴파일 오류가 발생하지 않는다.

        // 문제 2: 개 타입 반환
        dogHospital.set(dog);
        Dog biggerDog = (Dog) dogHospital.bigger(new Dog("멍멍이2", 200));
        System.out.println("biggerDog = " + biggerDog);
    }
}

 Dog, Cat 클래스를 활용한 Hospital 클래스 없이도 모든 동물을 넣을 수 있다.

하지만, DogHospital로 만들어도 Cat 클래스를 넣을 수 있다(매개변수 체크 실패: 컴파일 오류가 발생하지 않음)

bigger() 메소드 사용 시 형 변환을 사용해야 하는 경우가 발생한다

 

매개변수 제한 없이 제네릭을 활용하면, 모든 타입이 들어갈 수 있다
또한, Animal에서 만든 메소드를 사용하지 못한다
(어떤 타입이 들어갈지 알 수 없기 때문에, Object의 기본 메소드만 활용가능)

 

 

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 bigger(T target) {
        return animal.getSize() > target.getSize() ? animal : target;
    }
}

제네릭 선언 시, 매개변수 타입 제한을 활용하면, Animal 클래스 자신과 자식 클래스만 제네릭 타입으로 선언할 수 있다.

Animal 클래스의 메소드를 활용할 수 있다

자식 클래스는, 오버라이딩 되어서 자식 클래스의 메소드를 활용할 수 있다

 

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", 300);

        // 개 병원
        dogHospital.set(dog);
        dogHospital.checkup();

        // 고양이 병원
        catHospital.set(cat);
        catHospital.checkup();

        // 문제 1: 개 병원에 고양이 전달
        //dogHospital.set(cat);  // 다른 타입을 입력: 컴파일 오류

        // 문제 2: 개 타입 반환
        dogHospital.set(dog);
        Dog biggerDog = dogHospital.bigger(new Dog("멍멍이2", 200));
        System.out.println("biggerDog = " + biggerDog);
    }
}

동물 클래스 별로 만들 필요가 없어 중복을 제거하고, 다른 타입이 들어오는 것을 방지할 수 있다(컴파일 오류 발생)

'Java > 김영한의 실전자바' 카테고리의 다른 글

스레드 - join()  (0) 2024.08.26
스레드 제어  (0) 2024.08.22
스레드의 생성과 실행  (0) 2024.08.15
프로세스와 스레드  (0) 2024.08.13
[김영한의 실전 자바 - 기본편] 기본형과 참조형  (0) 2024.01.21