제네릭이 필요한 이유( 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 |