본문 바로가기

Programming/Java

[Java] 김영한의 자바 중급 2편 #2 - 제네릭2

반응형

1. 타입 매개변수 제한

다음의 요구사항이 있다고 가정해본다.

요구 사항 : 개 병원은 개만 받을 수 있고, 고양이 병원은 고양이만 받을 수 있다.

개 병원과 고양이 병원은 검진(checkup)을 할 수 있고, 다른 개 혹은 고양이와의 크기를 비교할 수 있다.

1.1. 아무것도 모르는 상태일 경우

아무것도 모르는 상태라면 다음의 관점으로 접근할 수 있을 것이다.

  • Dog는 DogHospital 클래스를 만든다.
  • Cat은 CatHospital 클래스를 만든다.
  • main 메서드에서는 dogHospital과 catHospital에 각각 dog와 cat 객체를 생성한다음에 setter 메서드로 집어넣고, 각각 checkup과 bigger 메서드를 통해 검진과 대소비교를 할 수 있을 것이다.

1.1.1. 장단점

  • 장점으로는 CatHospital과 DogHospital을 각각 사용함으로써 타입 안정성이 명확하게 지켜진다.
  • 단점으로는 코드의 재사용성이 떨어진다.

1.2. 다형성을 시도

Dog, Cat은 Animal이라는 명확한 부모 타입이 있기 때문에 다형성을 사용해서 중복성을 제거할 수 있을 것으로 보인다.

다형성 활용한 AnimalHospital

package generic.ex3;

import generic.animal.Animal;

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

Main 코드

package generic.ex3;

import generic.animal.Cat;
import generic.animal.Dog;

public class AnimalHospitalMainV1 {

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

        Dog dog = new Dog("김멍멍", 100);
        Cat cat = new Cat("김냐옹", 200);

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

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

        // 문제 1 : 개 병원에 고양이가 전달된다.
        //         타입 안정성 X
        dogHospital.set(cat);

        // 문제 2 : 캐스팅 필요
        dogHospital.set(dog);
        Dog biggerDog = (Dog) dogHospital.getBigger(new Dog("박멍멍", 200));
        System.out.println("bigger dog is " + biggerDog);
    }
}

// 결과
동물 이름 : 김멍멍
동물 크기 : 100
멍멍
동물 이름 : 김냐옹
동물 크기 : 200
냐옹
bigger dog is Animal{name='박멍멍', size=200}

1.2.1. 장단점

  • 장점으로는 코드 재사용성이 높아진다.
  • 단점으로는 다운 캐스팅을 해야 하며, 타입 안정성이 없다는 점이다.

1.3. 제네릭 도입

앞서 배운 제네릭을 도입하여 코드 재사용성을 늘리고, 타입 안정성 문제도 해결해보도록 하는 것을 목표..

package generic.ex3;

public class AnimalHospitalV2<T> {

    private T animal;

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

    public void checkup() {
        // 단점 : T의 타입을 메서드를 정의하는 시점에 알 수 없으며 Object의 기능만 사용할 수 있다.
        // 즉 animal의 getName(), sound() 등은 사용할 수 없다.
        animal.toString();
    }
}

1.3.1. 단점

<T>를 사용해서 제네릭 타입을 선언했다.

하지만 자바 컴파일러 입장에서는 T에 어떤 값이 들어올지 예측할 수 없다. T에는 타입 인자로 Integer가 들어올 수도, Object가 들어올 수도 있다는 것이다.

  • 제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있다.
  • Object의 기능만 사용할 수 있다.

1.4. 제네릭에서의 타입 매개변수 제한

Type 매개변수 T를 Animal로 제한한 제네릭 클래스

package generic.ex3;

import generic.animal.Animal;

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

Main 클래스

package generic.ex3;

import generic.animal.Cat;
import generic.animal.Dog;

public class AnimalHospitalMainV3 {

    public static void main(String[] args) {
        AnimalHospitalV3<Dog> dogHospital = new AnimalHospitalV3<>();
        AnimalHospitalV3<Cat> catHospital = new AnimalHospitalV3<>();

        Dog dog = new Dog("김멍멍", 100);
        Cat cat = new Cat("김냐옹", 200);

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

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

        // 문제1 해결 : 타입 안정성 O
//        dogHospital.set(cat);

        // 문제2 해결 : 타입 캐스팅 필요 X
        dogHospital.set(dog);
        Dog biggerDog = dogHospital.getBigger(new Dog("박멍멍", 200));
        System.out.println("bigger dog is " + biggerDog);
    }
}

// 결과
동물 이름: 김멍멍
동물 크기: 100
멍멍
동물 이름: 김냐옹
동물 크기: 200
냐옹
bigger dog is Animal{name='박멍멍', size=200}

1.4.1. 정리

  • 어떤 타입이든 사용할 수 있는 제네릭 클래스의 문제점을 해결할 수 있었다.
    • 타입 매개변수 상한을 사용해서 해당 타입의 기능을 활용할 수 있었다.
  • 제네릭을 사용함으로써 얻는 이점 또한 활용할 수 있었다.
    • 타입 안정성
    • 코드 재사용성

2. 제네릭 메서드

2.1. 예시

package generic.ex4;

public class GenericMethod {

    public static Object objMethod(Object obj) {
        System.out.println("object print: " + obj);
        return obj;
    }

    public static <T> T genericMethod(T t) {
        System.out.println("generic method : " + t);
        return t;
    }

    public static <T extends Number> T numberMethod(T t) {
        System.out.println("bound print: " + t);
        return t;
    }
}

package generic.ex4;

public class MethodMain1 {

    public static void main(String[] args) {
        Integer i = 10;
        Object object = GenericMethod.objMethod(i);

        // Type Argument 명시적 전달
        System.out.println("명시적 타입 인자 전달");
        Integer result = GenericMethod.<Integer>genericMethod(i);
        Integer integerValue = GenericMethod.<Integer>numberMethod(10);
        Double doubleValue = GenericMethod.<Double>numberMethod(20.0);
    }
}

// 결과
object print: 10
명시적 타입 인자 전달
generic method : 10
bound print: 10
bound print: 20.0

1.2 제네릭 타입과 제네릭 메서드

제네릭 타입

  • 정의 : GenericClass<T>
  • 타입 인자 전달 : 객체를 생성하는 시점
    • new GenericClass<String>

제네릭 메서드

  • 정의 : <T> T genericMethod(T t)
  • 타입 인자 전달 : 메서드를 호출하는 시점
    • GenericMethod.<Integer>genericMethod(i)
  • 특정 메서드 단위로 제네릭을 도입할 때 사용한다.
  • 타입을 지정하면서 메서드를 호출한다.

1.2.1. 인스턴스 메서드, static 메서드

제네릭 메서드는 인스턴스 메서드와 스태틱 메서드에 모두 적용할 수 있다.

class Box<T> {
	static <v> V staticMethod2(V t) {}
	
	<Z> Z instanceMethod2(Z z) {}
}

참고 제네릭 타입은 static 메서드에 타입 매개변수를 사용할 수 없다.

제네릭 타입은 객체를 생성하는 시점에 타입이 정해진다. 그런데 static 메서드는 인스턴스 단위가 아니라 클래스 단위로 작동하기 때문에 제네릭 타입과는 무관하다.

따라서 static 메서드에 제네릭을 도입하려면 제네릭 메서드를 사용해야 한다.

class Box<T> {
	T instanceMethod(T t) {} // 가능
	
	static T staticMethod(T t) {} // 제네릭 타입의 T 사용 불가능
}

1.2.2. 제네릭 메서드의 타입 매개변수 제한

public static <T extents Number> T numberMethod(T t) {}

1.2.3. 제네릭 메서드의 타입 추론

제네릭 메서드를 호출할 때 <Integer>와 같이 타입 인자를 계속 전달하지 않아도 자바 컴파일러는 타입 인자를 추론할 수 있다.

Integer result2 = GenericMethod.genericMethod(i);
Integer integerResult2 = GenericMethod.numberMethod(10);
Double doubleResult2 = GenericMethod.numberMethod(20.0);

1.3. 제네릭 메서드 활용

package generic.ex4;

public class GenericMethod {

    public static Object objMethod(Object obj) {
        System.out.println("object print: " + obj);
        return obj;
    }

    public static <T> T genericMethod(T t) {
        System.out.println("generic method : " + t);
        return t;
    }

    public static <T extends Number> T numberMethod(T t) {
        System.out.println("bound print: " + t);
        return t;
    }
}

Main 코드

package generic.ex4;

import generic.animal.Cat;
import generic.animal.Dog;

public class MethodMain2 {

    public static void main(String[] args) {
        Dog dog = new Dog("멍멍1", 100);
        Cat cat = new Cat("냐옹1", 200);

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

        Dog targetDog = new Dog("빅멍멍", 500);
        Dog biggerDog = AnimalMethod.getBigger(dog, targetDog);
        System.out.println("bigger dog is " + biggerDog);
    }
}

// 결과
동물 이름 : 멍멍1
동물 크기 : 100
멍멍
동물 이름 : 냐옹1
동물 크기 : 200
냐옹
bigger dog is Animal{name='빅멍멍', size=500}

1.4. 제네릭 타입과 제네릭 메서드의 우선순위

  • 정적 메서드 : 제네릭 메서드만 적용할 수 있다.
  • 인스턴스 메서드 : 제네릭 타입도, 제네릭 메서드도 둘 다 적용할 수 있다

그렇다면 제네릭 타입과 제네릭 메서드의 타입 매개변수를 같은 이름으로 사용한다면?

→ 제네릭 타입보다 제네릭 메서드가 높은 우선순위를 가진다.

2. 와일드카드(wildcard)

와일드카드라는 뜻은 컴퓨터 프로그래밍에서 *, ?와 같이 하나 이상의 문자들을 상징하는 특수 문자를 뜻한다.

와일드카드는 제네릭 타입이나, 제네릭 메서드를 선언하는 것이 아니다.

이미 만들어진 제네릭 타입을 활용할 때 사용한다.

2.1. 예시

Generic Box

package generic.ex5;

public class Box<T> {

    private T value;

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

    public T get() {
        return value;
    }
}

Generic과 Wildcard를 비교하기 위해 만든 클래스

package generic.ex5;

import generic.animal.Animal;

public class WildcardEx {

    static <T> void printGenericV1(Box<T> box) {
        System.out.println("T : " + box.get());
    }

    static void printWildcardV1(Box<?> box) {
        System.out.println("? : " + box.get());
    }

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

    static void printWildcardV2(Box<? extends Animal> box) {
        Animal animal = box.get();
        System.out.println("이름 : " + animal.getName());
    }

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

    static Animal printAndReturnWildcard(Box<? extends Animal> box) {
        Animal animal = box.get();
        System.out.println("이름 : " + animal.getName());
        return animal;
    }
}

Main 코드

package generic.ex5;

import generic.animal.Animal;
import generic.animal.Cat;
import generic.animal.Dog;

public class WildcardMain1 {

    public static void main(String[] args) {
        Box<Object> objBox = new Box<>();
        Box<Dog> dogBox = new Box<>();
        Box<Cat> catBox = new Box<>();

        dogBox.set(new Dog("멍멍이", 100));

        WildcardEx.printGenericV1(dogBox);
        WildcardEx.printWildcardV1(dogBox);

        WildcardEx.printGenericV2(dogBox);
        WildcardEx.printWildcardV2(dogBox);

        Dog dog = WildcardEx.printAndReturnGeneric(dogBox);
        Animal animal = WildcardEx.printAndReturnWildcard(dogBox);
    }
}

2.1.1. 설명

		// 제네릭 메서드 예시
    static <T> void printGenericV1(Box<T> box) {
        System.out.println("T : " + box.get());
    }

		// 와일드카드 예시
    static void printWildcardV1(Box<?> box) {
        System.out.println("? : " + box.get());
    }
  • 와일드카드는 제네릭 타입이나 제네릭 메서드를 정의할 때 사용하는 것이 아님
    • Box<Dog>, Box<Cat> 처럼 타입 인자가 정해진 제네릭 타입을 전달 받아서 활용할 때 사용
  • 와일드카드인 ?는 모든 타입을 다 받을 수 있다는 뜻
    • ? == <? extends Object>
  • ?만 사용해서 제한 없이 모든 타입을 다 받을 수 있는 와일드카드를 비제한 와일드카드라고 한다.

2.2. 상한 와일드카드

    static void printWildcardV2(Box<? extends Animal> box) {
        Animal animal = box.get();
        System.out.println("이름 : " + animal.getName());
    }

  • 제네릭 메서드와 마찬가지로 와일드카드에 상한 제한을 둘 수 있다.
  • ? extends Animal 처럼 말이다.
  • 이를 통해 Animal 기능을 호출할 수 있다.

2.2.1. 상한 와일드카드와 제네릭의 타입 매개변수 제한

와일드 카드는 Box<Dog>, Box<Cat>처럼 타입 인자가 전달된 제네릭 타입을 활용할 때 사용한다.

따라서 상한 와일드 카드를 써야할 때, 제네릭으로 타입 매개변수를 제한해서 써야할때를 구분할 줄 알아야 한다.

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

    static Animal printAndReturnWildcard(Box<? extends Animal> box) {
        Animal animal = box.get();
        System.out.println("이름 : " + animal.getName());
        return animal;
    }

  • printAndReturnGeneric의 리턴 타입은 T이다. 즉 전달된 타입을 명확하게 반환할 수 있다.
  • printAndReturnWildcard의 리턴 타입은 Animal이다. 즉 전달된 타입이 무엇이든간에 상한 와일드카드 타입으로 반환할 수 밖에 없다.

2.2.2. 정리

2.2.1처럼 제네릭 타입이나 제네릭 메서드가 꼭 필요한 상황이면 <T>를 쓰고,

그렇지 않은 상황이라면 와일드카드를 사용하는 것을 권장한다고 한다.


3. 타입 이레이저

타입 이레이저를 통해 제네릭의 동작 방식을 이해할 수 있다.

제네릭은 컴파일 단계에서만 사용되고, 컴파일 이후에는 제네릭 정보가 삭제된다.

즉, 제네릭에 사용한 타입 매개변수가 모두 사라지는 것이다.

컴파일 전인 .java에는 제네릭의 타입 매개변수가 존재하지만 컴파일 이후 자바 바이트코드인 .class에는 타입 매개변수가 존재하지 않는다.

3.1. 변환 과정

3.1.1. 코드

선언

public class GenericBox<T> {
	private T value;
	
	public void set(T value) {
		this.value = value;
	}
	
	public T get() {
		return value;
	}
}

제네릭 타입에 Integer 타입 인자 전달

void main() {
	GenericBox<Integer> box = new GenericBox<Integer>();
	box.set(10);
	Integer result = box.get();
}

이렇게 하면 자바 컴파일러는 아래와 같이 new GenericBox<Integer>에 대해 다음과 같이 이해함

public class GenericBox<Integer> {
	private Integer value;
	
	public void set(Integer value) {
		this.value = value;
	}
	
	public Integer get() {
		return value;
	}
}

3.1.2. 컴파일 후

상한 제한이 없다는 가정 하에,

타입 매개변수 T는 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(); //컴파일러가 캐스팅 추가
}

3.2. 타입 이레이저 방식의 한계

class EraserBox<T> {
	public boolean instanceCheck(Object param) {
		return param instanceof T; // 오류
	}
	
	public void create() {
		return new T(); // 오류
	}
}
  • T는 런타임에 모두 Object가 되어버린다.
  • instanceof는 항상 Object와 비교하게 되는데 이 때 항상 참이 반환되는 문제가 발생한다.
    • 이 때문에 자바는 타입 매개변수에 instanceof를 허용하지 않는다.
  • new T는 항상 new Object가 되어버린다.
    • 개발자는 타입 인자로 들어온 String이나 Integer를 의도했겠지만 의도한 바와 달라진다. 따라서 자바는 타입 매개변수에 new를 허용하지 않는다.
반응형