제네릭이 탄생하게 된 이유
만약 프로그래밍 중에 타입 별로 값을 할당하고, 보관하고, 조회하는 기능이 필요하다고 가정해보자
개발자는 Integer타입
, String타입
, Double타입
등 먼저 기본형에 맞는 Wrapper
클래스를 먼저 만들어 줄 것이다.
하지만 Member
, Item
과 같이 프로그램 개발중에 만들어지는 인스턴스 타입도 모두 만들어야하는 안타까운 일이 발생하게 된다.
public class IntegerBox {
Integer value;
public void setValue(Integer value) {
this.value = value;
}
public Integer getValue() {
return value;
}
}
public class StringBox {
private String value;
public void setValue(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
그렇다면 다형성을 활용해서 문제를 해결해보자.Wrapper
클래스, Member
,Item
등 모든 클래스의 최상위 타입을 Object클래스이기 떄문에 Object클래스를 활용하게 된다면 값을 보관, 할당, 조회 하는 기능을 가진 수십 수백개의 모든 클래스를 단 하나의 클래스로 통합할 수 있게될 것이다.
public class ObjectBox {
private Object value;
public void setValue(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
setValue()
를 통해 값을 넣을 때는 저장하고자하는 값의 타입을 넣으면된다getValue()
로 값을 얻고자 할 때는 다운 캐스팅을 통해Object
타입의 값을 얻고자하는 타입으로 다운 캐스팅을하면 값을 얻을 수 있을것이다.
public class ObjectMain {
public static void main(String[] args) {
ObjectBox integerBox = new ObjectBox();
Integer intValue = 10;
integerBox.setValue(intValue);
Integer integerResult = (Integer) integerBox.getValue();
System.out.println("integerResult = " + integerResult);
ObjectBox stringBox = new ObjectBox();
String strValue = "hello";
stringBox.setValue(strValue);
String stringResult = (String)stringBox.getValue();
System.out.println("stringResult = " + stringResult);
}
}
이렇게 하나의 클래스로 어떤 값이든 저장할 수 있는 기능을 구현할 수 있게 되었다.
하지만 이런 설계는 타입의 안정성을 위협하게되는데 아래 코드를 살펴보자
ObjectBox error = new ObjectBox();
error.setValue("value100");
Integer value = (Integer) error.getValue();
다른 개발자가 ObjectBox
클래스를 사용하는데 안에 값을 문자열로 넣어두고 다른 개발자가 이 값을 꺼내 사용하려고 하면 ClassCastException
예외가 발생하게 된다.
여기서 개발자는 딜레마에 빠지게 되는 것이다.
- 타입 안정성을 포기하고 재사용성을 챙기느냐
- 재사용성을 포기하고 타입 안정성을 챙기느냐
- 이런 딜레마가 있을 때는 유지보수성, 재사용성을 챙기는 것보다는 프로그램의 안정성을 주기위해 타입안정성을 선택하는것이 좋다고 한다.*
===이런 딜레마를 모두 만족하면서 해결해주는 것이 제네릭이고 제네릭이 나타나게 된 이유이다.===
제네릭 사용
public class GenericBox<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
제네릭을 사용하기위해서는 사용되는 클래스이름 옆에 T
라는 기호를 붙혀주면된다T
가 아니라 어떤 문자가 와도 상관없긴하다
- 다이아몬드 기호를 사용하여 해당 클래스를 제네릭클래스로 정의하였다
- 이 클래스는 클래스를 선언할 때 타입이 결정되는 것이 아니라 인스턴스를 생성할 때 타입이 결정되게 된다
이제 이 클래스를 사용하여 여러타입의 값들을 저장하고 조회해보자
public class GenericMain {
public static void main(String[] args) {
GenericBox<Integer> integerBox = new GenericBox<>();
integerBox.setValue(10);
Integer integerValue = integerBox.getValue();
GenericBox<String> stringBox = new GenericBox<>();
stringBox.setValue("hello");
String strValue = stringBox.getValue();
System.out.println("integerValue = " + integerValue);
System.out.println("strValue = " + strValue);
}
}
GenericBox<Integer> integerBox = new GenericBox<>();
GenericBox<String> stringBox = new GenericBox<>();
===인스턴스가 생성되는 시점에 원하는 타입을 지정하여 해당 타입으로 인스턴스를 생성할 수 있게된다.===
[!NOTE] 타입 추론
GenericBox<String> stringBox = new GenericBox<String>();
원래는 위와 같이 인스턴스를 생성하는 곳에도String
타입을 지정해주어야 했지만 시간이 지남에 따라 편리성을 위해 굳이 넣어주지 않도록 바뀌었다.
이는 컴파일러가 타입추론을 통해 어떤 타입이 들어올지 왼쪽의 변수 타입을 보고 스스로 추론하여 생성하도록 하는것이다 이를 타입 추론이라고한다.
- 제네릭을 사용하여 코드의 재사용성과 타입 안정성을 모두 확보하였다
Object
타입을 활용할 때의 캐스팅 또한 필요없다
제네릭 이해하기
제네릭의 핵심은 사용할 타입을 미리 결정하지 않는다는 것이다.
'미리 결정하지 않는다'는 말은 메서드의 매개변수와 인수에도 적용이되는 개념이다
public void hello() {
System.out.println("hello");
}
print()
메서드는 반드시 "hello"라는 문자를 출력하는데만 사용할 수 있으므로 이는 "hello"라는 문자열이 이미 정해져있기 때문에 재사용성이 떨어진다.
이 메서드를 어떤 문자열이든 인수로 받아 출력할 수 있도록 변경하면
public void print(String str) {
System.out.println(str);
}
String str
매개변수로 출력하고자하는 문자열을 받아 원하는 문자열로 출력할 수 있다.
print("hello");
print("안녕하세요");
print메서드의 인자를 "hello", "안녕하세요"로 주고 원하는 문자열을 출력한다
===핵심은 매개변수와 인자이다===
- 메서드에 선언된 매개변수는 아직 정해지지않은 값으로 외부로부터 값을 입력받아서 동적으로 문자열을 출력한다
- 인자는 내가 사용하고자하는 값을 인자를 통해 전달해서 원하는 값을 동적으로 출력한다.
위 개념이 제네릭에서도 똑같이 적용되지만 메서드의 매개변수와 인자는 '값'을 동적으로 정하고,
제네릭의 타입 매개변수와 타입 인자는 '타입'을 동적으로 정하는 것이다.
정리
- 메서드는 매개변수에 인자를 전달해서 사용할 값을 결정한다
- 제네릭 클래스는 타입 매개변수에 타입 인자를 전달해서 사용할 타입을 결정한다
- 타입 매개변수: GenericBox<T>에서 T
- 타입 인자: GenericBox<Integer>에서 Integer
- 제네릭 타입의 타입매개변수 T는 타입 인자를 전달해서 제네릭의 사용 타입을 결정하게된다.
- 제네릭 단어
- 제네릭이라는 단어는 일반적인, 범용적인이라는 영어단어의 의미를 가졌다.
- 제네릭 타입
- 클래스나 인터페이스를 정의할 때 타입 매개변수를 사용하는 것을 말한다
- 타입 매개변수
- 제네릭 타입이나 메서드에서 사용되는 변수로 실제 타입으로 대체된다
- GenericBox<T> 의 T
- 타입 인자
- 제네릭 타입을 사용할 때 제공되는 실제 타입
- GenericBox<Integer>의 Integer
- 제네릭 타입 매개변수의 관례
- E - Element
- K - Key
- N - Number
- T - Type
- V - Value
제너릭 메서드
제네릭 메서드는 말그대로 메서드에서 제네릭을 사용하는 것이다
public class GenericMethod {
public static <T> T genericMethod(T t) {
System.out.println("generic print: " + t);
return t;
}
public static <T extends Number> T numberMethod(T t) {
System.out.println("bound print: " + t);
return t;
}
}
- 제네릭 메서드의 선언은 메서드 반환 타입 T 앞에 <T>를 붙혀 선언한다
- 제네릭타입과 마찬가지로 제네릭 메서드도
extends
키워드를 사용하여 타입 매개변수의 범위를 제한할 수 있다.numberMethod()
같은 경우에는 숫자만을 타입 인자로 입력받고 숫자만을 리턴할 수 있다.
public class MethodMain1 {
public static void main(String[] args) {
Integer i = 10;
System.out.println("명시적 타입 인자 전달");
Integer result = GenericMethod.<Integer>genericMethod(i);
Integer integerValue = GenericMethod.<Integer>numberMethod(10);
Double doubleValue = GenericMethod.<Double>numberMethod(20.0);
}
}
제네릭 타입과 제네릭 메서드 비교
제네릭 타입
- 정의: GenericeClass<T>
- 타입 인자 전달: 객체를 생성하는 시점
- Ex) new GenericClass<String>
제네릭 메서드
- 정의: <T> T genericMethod(T t)
- 타입 인자 전달: 메서드를 호출하는 시점
- Ex) GenericMethod.<Integer>genericMethod(i)
- 제네릭 메서드는 클래스 전체가 아니라 특정 메서드 단위로 제네릭을 도입할 때 사용한다
- 제네릭 메서드 정의는 메서드 반환타입 왼쪽에 다이아몬드를 사용해서 타입 매개변수를 적어준다
- 제네릭 메서드를 호출할때는 다이아몬드를 사용해서 타입을 정하고 호출한다
===제네릭 메서드의 핵심은 메서드를 호출하는 시점에 타입 인자를 전달해서 타입을 지정한다는 것이며 이는 다이아몬드를 통해 타입을 지정한다===
[!NOTE] 제네릭 타입과 static
제네릭 타입(클래스)은 인스턴스가 생성되는 시점에 타입이 결정된다고 공부했다.
하지만 static은 인스턴스의 생성여부 상관없이 클래스단위로 선언되어 사용할 수 있기때문에 제네릭 타입을 적용한 클래스에 선언된 static메서드에서는 제네릭 타입 매개변수를 사용할 수 없게된다.
만약 static메서드에서 제네릭을 사용해야한다면 제네릭 메서드를 사용하면 된다
public class TestMethod<T> {
public void testMethod1(T t) {
System.out.println("test");
}
//T t부분에서 컴파일 에러가 발생한다.
public static void testMethod2(T t) {
System.out.println("test");
}
}
제네릭 메서드 타입 추론
public class MethodMain1 {
public static void main(String[] args) {
Integer i = 10;
System.out.println("명시적 타입 인자 전달");
Integer result = GenericMethod.<Integer>genericMethod(i);
Integer integerValue = GenericMethod.<Integer>numberMethod(10);
Double doubleValue = GenericMethod.<Double>numberMethod(20.0);
}
}
자바 컴파일러는 genericMethod(i)
에서 전달되는 i의 타입이 Integer라는 것을 알 수 있기때문에 메서드 이름앞에 <Integer>는 생략할 수 있다.
타입을 생략한 메서드 호출
public class MethodMain1 {
public static void main(String[] args) {
Integer i = 10;
Integer result = GenericMethod.genericMethod(i);
Integer integerValue = GenericMethod.numberMethod(10);
Double doubleValue = GenericMethod.numberMethod(20.0);
}
}
와일드카드
- 와일드 카드는 제네릭 타입을 조금 더 편리하게 사용하도록 도와주는 개념이다
- ===와일드 카드는 제네릭 타입이나 제네릭 메서드를 선언하는 것이 아니라 이미 만들어진 제네릭 타입을 활용할 때 사용하는 것이다===
예제
데이터를 보관하고 반환하는 제네릭 타입
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
- 클래스이름 옆에 <T>를 선언해줌으로써 해당 클래스는 제네릭 타입임을 선언해주었다.
- T는 아직 정해지지 않은 타입을 의미하며 이 타입은 인스턴스 생성 시점에 결정된다
첫번째 예제 - 비제한 메서드
//제너릭 메서드
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>
처럼 타입 인자가 정해진 제네릭 타입을 전달받아서 활용하도록하는 역할을 수행한다. ?
만 사용해서 제한 없이 모든 타입을 다 받을 수 있는 와일드카드를 비제한 와일드카드라고 한다
두번째 예제 - 범위 지정
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());
}
- 와일드카드를 이용한 메서드도 제너릭을 사용하는 것과 동일하게 타입의 범위를 제한할 수 있다.
Box<? extends 제한할클래스 이름>
box.get()
메서드의 반환타입을 받을 때T t
가 아닌Animal
타입으로 바로 받을 수 있는 것을 확인할 수 있다. 이것 또한 와일드카드의 장점이다.
세번째 예제 - 리턴타입 지정
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;
}
- 제네릭 메서드 / 와일드카드를 이용한 메서드 모두 리턴타입을 지정할 수 있다
- 제네릭 메서드는 다형성을 고려하여 제한된 타입중 최상위타입을 리턴한다.
제네릭 메서드 vs 와일드 카드
제네릭 타입의 실행과정
//1. 전달
printGenericV1(dogBox)
//2. 제네릭 타입 결정 dogBox는 Box<Dog> 타입, 타입 추론 -> T의 타입은 Dog
static <T> void printGenericV1(Box<T> box) {
System.out.println("T = " + box.get());
}
//3. 타입 인자 결정
static <Dog> void printGenericV1(Box<Dog> box) {
System.out.println("T = " + box.get());
}
//4. 최종 실행 메서드
static void printGenericV1(Box<Dog> box) {
System.out.println("T = " + box.get());
}
제네릭 메서드 실행과정을 살펴보면 먼저, 타입인자를 전달받고 타입인자의 타입을 통해 타입추론이 일어나게된다.
추론결과로 나온 타입을 최종 타입으로 결정하고 메서드를 실행하게된다.
//1. 전달
printWildcardV1(dogBox)
//2. 최종 실행 메서드, 와일드카드 ?는 모든 타입을 받을 수 있다.
static void printWildcardV1(Box<?> box) {
System.out.println("? = " + box.get());
}
와일드카드를 사용한 메서드는 타입의 추론없이 모든타입을 받을 수 있기때문에 제네릭 메서드와 비교했을 때 실행과정이 매우 단순하다.
결론은
제네릭 메서드는 타입 매개변수가 존재해 이 타입을 결정하는 타입추론이 반드시 필요하며 이 과정은 매우 복잡하다
반면, 와일드카드 메서드는 일반적인 메서드에 와일드카드를 사용한 것 뿐이기때문에 코드의 외관도 유지할 수 있을 뿐더러 타입추론도 필요하지 않다
===따라서 제네릭 타입이나 제네릭 메서드를 정의하는게 꼭 필요한 상황이 아니라면 더 단순한 와일드카드의 사용을 권장하는편이라고 한다.===
[!NOTE] 제네릭 타입이나 제네릭 메서드가 꼭 필요한상황
와일드 카드를 사용하면 전달한 타입을 명확하게 반환할 수 없다.
만약 명확한 타입을 반환받고 싶다면 그 때는 제네릭 메서드를 사용해야한다.
와일드카드는 제네릭 메서드를 만드는 것이아니라 양념만 쳐주는 느낌?
타입 이레이저
제네릭은 자바 컴파일 단계에서만 사용되고 컴파일 이후에는 제네릭 정보가 삭제된다.
즉, 우리가 코딩하는 .java파일에는 제네릭의 타입 매개변수가 존재하지만, 컴파일러가 컴파일한 후의 파일인 자바 바이트코드 .class에는 타입 매개변수가 존재하지 않는다.
public class GenericBox<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
이 코드는 개발자가 작성한 코드로 .java파일 형식을 가지고 있다. 이 때는 제네릭 타입이 사용되었으며 타입 매개변수가 존재하는 것을 확인할 수 있다.
만약 이 코드에서 Integer
타입의 타입 인자를 전달했다고 가정해보자new GenericBox<Integer>()
그러면 컴파일러는 단순하게 아래와 같이 코드를 이해한다
public class GenericBox<Integer> {
private Integer value;
public void set(Integer value) {
this.value = value;
}
public Integer get() {
return value;
}
}
- 제네릭이 적용된, 타입 매개변수가 적용된 곳에 타입 인자로 들어온
<Integer>
로 이해한다.(치환하는 느낌)
만약 상한 제한이 없는 타입 매개변수라면(extends를 하지않은 제네릭)
컴파일 후
public class GenericBox {
private Object value;
public void set(Object value) {
this.value = value;
}
public Object get() {
return value;
}
}
컴파일 후에는 위와 같이 모든 타입을 Object로 치환한다.
===핵심===
void main() {
GenericBox box = new GenericBox();
box.set(10);
Integer result = (Integer) box.get(); //컴파일러가 캐스팅 추가
}
Object타입으로 반환받은 메서드의 결과를 Integer로 다운캐스팅하는 과정을 거친다.
이 결과를 통해 제네릭은 단순하게 말하면 개발자가 직접 캐스팅하는 코드를 컴파일러가 대신 처리해주는 것이다.
다운 캐스팅하는 과정에서 컴파일러가 완벽하게 검증을 하기때문에 다운캐스팅에서 문제가 발생하지 않는다.
이런 제네릭 타입은 컴파일 시점에만 존재하고 런타임 시에는 제네릭 정보가 지워지는데 이것을 타입 이레이저라고 한다
타입 매개변수 제한
DogHospital
에는 Dog
만 들어갈 수 있고 CatHospital에는
Cat
만 들어갈 수 있다고 가정한다.
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());
System.out.println(animal);
}
public Animal bigger(Animal target) {
return animal.getSize() > target.getSize() ? animal: target;
}
}
public class AnimalHospitalMainV1 {
public static void main(String[] args) {
AnimalHospital dogHospital = new AnimalHospital();
AnimalHospital catHospital = new AnimalHospital();
Dog dog = new Dog("멍멍이", 100);
Cat cat = new Cat("야옹이", 300);
dogHospital.set(dog);
dogHospital.checkup();
catHospital.set(cat);
catHospital.checkup();
dogHospital.set(cat);
Dog biggerDog = (Dog)dogHospital.bigger(new Dog("멍멍이2", 200));
System.out.println("biggerDog = " + biggerDog);
}
}
코드의 중복을 줄이기위해 AnimalHospital
이라는 병원 클래스를 하나로 통합하고 각각 강아지 병원, 고양이 병원을 만들어서 다형성으로 해결하려고하니 아래와 같은 문제가 발생한다
dogHospital.set(cat);
강아지 병원에 고양이가 들어갈 수 있게되었다.(컴파일 에러가 나지않음)Dog biggerDog = (Dog)dogHospital.bigger(new Dog("멍멍이2", 200));
더 큰 강아지를 조회하는 과정에서 다운캐스팅이 필요하게 되었다.
이제 앞서 공부한대로 제네릭 타입을 적용하여 타입안전성과 코드중복 문제를 모두 해결해보자
public class AnimalHospitalV2<T> {
private T animal;
public void set(T animal) {
this.animal = animal;
}
public void checkup() {
System.out.println("동물 이름: " + animal.getName()); //메서드 컴파일에러
System.out.println("동물 크기: " + animal.getSize()); //메서드 컴파일에러
System.out.println(animal);
}
public T bigger(T target) {
//메서드 컴파일에러
return animal.getSize() > target.getSize() ? animal: target;
}
}
다이아몬드 기호를 사용하여 AnimalHospital
을 제네릭 클래스로 선언해주니 다음 메서드들에 컴파일에러가 발생한다
animal.getName()
animal.getSize()
target.getSize()
===왜 컴파일 에러가 발생하는 것일까?===
생각을 해보니 제너릭타입의 T는 인스턴스가 생성되는 시점에 타입이 결정된다고 제네릭 개념을 공부하면서 배웠었다.
현재 이 코드는 AnimalHospital
을 제네릭으로 선언하는 과정이지 인스턴스가 생성되어있지 않은 상태이다. 즉 animal
변수와 target
변수는 현재 타입이 아직 정해지지 않은 상태이다.
이 변수들에는 Integer가
들어올수도있고, String이
들어올수도있고 Dog
,Cat
어떤 타입이 들어올지 모르는 상황이기때문에 해당 변수의 메서드를 사용할수가 없는것이다.
getName()
, getSize()
는 Animal
클래스에 정의되어있는 메서드이기 때문이다.
[!NOTE] animal변수는 Object타입
어떤 타입인지 정해지지않았지만, 모든 클래스의 상위 클래스는 Object이기때문에 Object에 관련된 메서드 즉, equlas(), toString()과 같은 메서드는 사용할 수 있다.
이제 해결해야할 문제는 두 가지가있다.
- 제네릭클래스를 선언하는 과정에서 사용하고자하는 메서드를 사용할 수 있도록해야한다
- 제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있게된다
-> 타입매개변수를 Animal로 제한하게 된다면 이러한 문제들을 해결할 수 있을 것이다
타입매개변수 제한으로 문제해결
public class ClassName<T extends 제한클래스>
제네릭 클래스를 위와같이 extends
키워드로 타입매개변수의 범위를 지정할 수 있다.
범위를 제한하면 제한한 클래스와 제한한 클래스의 하위타입으로만 타입 인자로 넣을 수 있다.
===이제 컴파일러는 T에 타입매개변수에는 Animal
, Dog
, Cat
만 들어올 수 있다는 것을 예측할 수 있기때문에 T 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());
System.out.println(animal);
}
public T bigger(T target) {
return animal.getSize() > target.getSize() ? animal: target;
}
}
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("야옹이", 300);
dogHospital.set(dog);
dogHospital.checkup();
catHospital.set(cat);
catHospital.checkup();
//dogHospital.set(cat); 컴파일 에러
Dog dog2 = new Dog("멍멍이2", 300);
Dog biggerDog = dogHospital.bigger(dog2); //다운 캐스팅 필요없음
System.out.println("biggerDog = " + biggerDog);
}
}
- 만약 DogHospital에 고양이를 넣으려 할 경우 컴파일 에러가 발생한다
- Object타입을 받는 것이 아니라 Dog타입 자체를 반환받기 때문에 다운캐스팅이 필요없다
'Language > Java' 카테고리의 다른 글
[JAVA] 래퍼 클래스 (0) | 2024.06.24 |
---|---|
[Java] Enum타입 (1) | 2024.06.18 |
[Java - 자료구조] Set인터페이스 (0) | 2024.06.12 |
[JAVA - 자료구조] ArrayList vs LinkedList (1) | 2024.06.09 |
[JAVA - 자료구조] LinkedList (1) | 2024.06.09 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!