제네릭에 대해 알아보자!
대부분의 최신 프로그래밍 언어는 제네릭 개념을 제공한다.
제네릭이 왜 필요한지 지금부터 코드로 알아보자.
public class IntegerBox { private Integer value; public void set(Integer value){ this.value = value; } public Integer get(){ return value; } }
- 숫자를 보관하고 꺼낼 수 있는 단순한 기능을 제공한다.
public class StringBox { private String value; public void set(String object) { this.value = object; } public String get() { return value; } }
- 문자열을 보관하고 꺼낼 수 있는 단순한 기능을 제공한다.
public class BoxMain1 { public static void main(String[] args) { IntegerBox integerBox = new IntegerBox(); integerBox.set(10); //오토 박싱 Integer integer = integerBox.get(); System.out.println("integer = " + integer); StringBox stringBox = new StringBox(); stringBox.set("hello"); String str = stringBox.get(); System.out.println("str = " + str); } }
- 코드를 보면 먼저 숫자를 보관하는
IntegerBox를 생성하고, 그곳에 숫자10을 보관하고, 꺼낸 다음에 출력했다. 참고로 오토 박싱에 의해int가Integer로 자동 변환된다.- 다음으로 문자열을 보관하는
StringBox를 생성하고 그곳에 문자열"hello"를 보관하고, 꺼낸 다음에 출력했다.
문제
이후에 Double , Boolean 을 포함한 다양한 타입을 담는 박스가 필요하다면 각각의 타입별로 DoubleBox ,BooleanBox 와 같이 클래스를 새로 만들어야 한다. 담는 타입이 수십개라면, 수십개의 XxxBox 클래스를 만들어야 한다.
Object는 모든 타입의 부모이다. 따라서, 다형성을 사용해서 이 문제를 간단히 해결할 수 있다.
public class ObjectBox { private Object value; public void set(Object object) { this.value = object; } public Object get() { return value; } }
- 내부에
Object value를 가지고 있다.Object는 모든 타입의 부모이다. 부모는 자식을 담을 수 있으므로 세상의 모든 타입을ObjectBox에 보관할 수 있다.public class BoxMain2 { public static void main(String[] args) { ObjectBox integerBox = new ObjectBox(); integerBox.set(10); Integer integer = (Integer) integerBox.get(); //Object -> Integer 캐스팅 System.out.println("integer = " + integer); ObjectBox stringBox = new ObjectBox(); stringBox.set("hello"); String str = (String) stringBox.get(); //Object -> Integer 캐스팅 System.out.println("str = " + str); //잘못된 타입의 인수 전달시 integerBox.set("문자100"); Integer result = (Integer) integerBox.get(); // String -> Integer 캐스팅 예외 System.out.println("result = " + result); } }
- 잘 동작하는 것 같지만 몇 가지 문제가 있다.
- 반환 타입이 맞지 않는 문제
먼저
IntegerBox를 만들어서 숫자10을 보관했다. 숫자를 입력하는 부분에는 문제가 없어 보이지만,integerBox.get()을 호출할 때 문제가 나타난다.
integerBox.get()의 반환 타입은Object이다.Integer = Object는 성립하지 않는다. 자식은 부모를 담을 수 없다. 따라서 다음과 같이(Integer)타입 캐스팅 코드를 넣어서Object타입을Integer타입으로 직접 다운 캐스팅해야 한다.stringBox의 경우도 마찬가지이다.stringBox.get()이Object를 반환하므로 다음과 같이 다운 캐스팅해야 한다.
정리
다형성을 활용한 덕분에 코드의 중복을 제거하고, 기존 코드를 재사용할 수 있게 되었다. 하지만, 입력할 때 실수로 원하지 않는 타입이 들어갈 수 있는 타입 안정성 문제가 발생한다. 예를 들어서integerBox에는 숫자만 넣어야 하고,stringBox에는 문자열만 입력할 수 있어야 한다. 하지만 박스에 값을 보관하는set()의 매개변수가Object이기 때문에 다른 타입의 값을 입력할 수 있다. 그리고 반환 시점에도Object를 반환하기 때문에 원하는 타입을 정확하게 받을 수 없고, 항상 위험한 다운 캐스팅을 시도해야한다. 결과적으로 이 방식은 타입 안전성이 떨어진다.
지금까지 개발한 프로그램은 코드 재사용과 타입 안전성이라는 2마리 토끼를 한번에 잡을 수 없다. 코드 재사용을 늘리기 위해 Object 와 다형성을 사용하면 타입 안전성이 떨어지는 문제가 발생한다.
BoxMain1 : 각각의 타입별로 IntegerBox , StringBox 와 같은 클래스를 모두 정의BoxMain2 : ObjectBox 를 사용해서 다형성으로 하나의 클래스만 정의제네릭을 사용하면 코드 재사용과 타입 안정성이라는 두 마리 토끼를 한 번에 잡을 수 있다.
public class GenericBox<T> { private T value; public void set(T value) { this.value = value; } public T get() { return value; } }
<>를 사용한 클래스를 제네릭 클래스라 한다. 이 기호(<>)를 보통 다이아몬드라 한다.- 제네릭 클래스를 사용할 때는
Integer,String같은 타입을 미리 결정하지 않는다.
대신에 클래스명 오른쪽에<T>와 같이 선언하면 제네릭 클래스가 된다. 여기서T를 타입 매개변수라 한다. 이 타입 매개변수는 이후에Integer,String으로 변할 수 있다.- 그리고 클래스 내부에
T타입이 필요한 곳에T value와 같이 타입 매개변수를 적어두면 된다.
public class BoxMain3 { public static void main(String[] args) { // 생성 시점에 T의 타입 결정 GenericBox<Integer> integerBox = new GenericBox<Integer>(); integerBox.set(10); //integerBox.set("문자100"); // Integer 타입만 허용, 컴파일 오류 Integer integer = integerBox.get(); // Integer 타입 반환 System.out.println("integer=" + integer); GenericBox<String> stringBox = new GenericBox<String>(); stringBox.set("hello"); // String 타입만 허용 String str = stringBox.get(); //String 타입만 반환 System.out.println("str = " + str); // 원하는 모든 타입 사용 가능 GenericBox<Double> doubleBox = new GenericBox<Double>(); doubleBox.set(10.5); Double doubleValue = doubleBox.get(); System.out.println("doubleValue = " + doubleValue); // 타입 추론 : 제네릭 타입 생략 가능 GenericBox<Integer> integerBox2 = new GenericBox<>(); } }실행 결과
integer = 10 str = hello doubleValue = 10.5
생성 시점에 원하는 타입 지정
- 제네릭 클래스는
<>(다이아몬드 기호) 안에 타입 매객변수를 정의하면 된다.class GenericBox<T>
- 제네릭 클래스는 생성하는 시점에
<>사이에 원하는 타입을 지정한다.new GenericBox<Integer>()
- 이렇게 하면 앞서 정의한
GenericBox의T가 다음과 같이 지정한 타입으로 변한 다음 생성된다.
T에 Integer를 적용한 GenericBox 클래스
public class GenericBox<Integer> { private Integer value; public void set(Integer value) { this.value = value; } public Integer get() { return value; } }
T가 모두Integer로 변한다. 따라서,Integer타입을 입력하고 조회할 수 있다.- 이렇게 지정하면
set(Integer value)이므로 이 메서드에는Integer숫자만 담을 수 있다.integerBox.set(10); //성공 integerBox.set("문자100"); // Integer 타입만 허용, 컴파일 오류
get()의 경우에도Integer를 반환하기 때문에 타입 캐스팅 없이 숫자 타입으로 조회할 수 있다.Integer integer = integerBox.get(); // Integer 타입 반환 (캐스팅 X)
원하는 모든 타입 사용 가능
제네릭 클래스를 사용하면 다음과 같이
GenericBox객체를 생성하는 시점에 원하는 타입을 마음껏 지정할 수 있다.new GenericBox<Double>() new GenericBox<Boolean>() new GenericBox<MyClass>()
- 참고로 제네릭을 도입한다고 해서 앞서 설명한
GenericBox<String>,GenericBox<Integer>와 같은 코드가 실제 만들어지는 것은 아니다.- 대신에 자바 컴파일러가 우리가 입력한 타입 정보를 기반으로 이런 코드가 있다고 가정하고 컴파일 과정에 타입 정보를 반영한다.
정리
제네릭을 사용한 덕분에 코드 재사용과 타입 안정성이라는 두 마리 토끼를 모두 잡을 수 있다.
제네릭의 핵심은 사용할 타입을 미리 결정하지 않는다는 점이다. 클래스 내부에서 사용하는 타입으로 클래스를 정의하는 시점에 결정하는 것이 아니라 실제 사용하는 생성 시점에 타입을 결정하는 것이다.
메서드의 매개변수와 인자
void method(String param) // 매개변수 void main() { String arg = "hello"; method(arg) // 인수 전달 }
- 메서드의 매개변수에 인자를 전달해서 메서드의 사용 값을 결정한다.
제네릭의 타입 매개변수와 타입 인자
- 제네릭도 앞서 설명한 메서드의 매개변수와 인자의 관계와 비슷하게 작동한다.
- 제네릭 클래스를 정의할 때 내부에서 사용할 타입을 미리 결정하는 것이 아니라, 해당 클래스를 실제 사용하는 생성 시점에 내부에서 사용할 타입을 결정하는 것이다.
- 차이가 있다면 메서드의 매개변수는 사용할 값에 대한 결정을 나중으로 미루는 것이고, 제네릭의 타입 매개변수는 사용할 타입에 대한 결정을 나중으로 미루는 것이다.
정리하면 다음과 같다.
- 메서드는 매개변수에 인자를 전달해서 사용할 값을 결정한다.
- 제네릭 클래스는 타입 매개변수에 타입 인자를 전달해서 사용할 타입을 결정한다.
용어 정리
- 제네릭 단어
- 제네릭이라는 단어는 일반적인, 범용적이라는 뜻
- 제네릭 타입
- 클래스나 인터페이스를 정의할 때 타입 매개변수를 사용하는 것
- 제네릭 클래스, 제네릭 인터페이스를 모두 합쳐서 제네릭 타입이라 한다.
- 예 :
class GenericBox<t>{private T t;}- 타입 매개변수
- 제네릭 타입이나 메서드에서 사용되는 변수로, 실제 타입으로 대체
- 예 :
GenericBox<T>- 여기에서
T를 타입 매개변수라 한다.- 타입 인자
- 제네릭 타입을 사용할 때 제공되는 실제 타입이다.
- 여기에서
Integer를 타입 인자라 한다.
제네릭 명명 관례
주로 사용하는 키워드는 다음과 같다.
- E - Element
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
제네릭 기타
class Data<K, V> {}타입 인자로 기본형은 사용할 수 없다.
제네릭의 타입 인자로 기본형(int,double...)은 사용할 수 없다. 대신에 래퍼 클래스(Integer,Double)을 사용하면 된다.
로 타입 - raw type( 이건 사용하면 안된다!! )
public class RawTypeMain { public static void main(String[] args) { GenericBox integerBox = new GenericBox(); //GenericBox<Object> integerBox = new GenericBox<>(); // 권장 integerBox.set(10); Integer result = (Integer) integerBox.get(); System.out.println("result = " + result); } }
- 제네릭 타입을 사용할 때는 항상
<>를 사용해서 사용시점에 타입을 지정해야 한다. 그런데 다음과 같이<>를 지정하지 않을 수 있는데, 이런 것을 로 타입(raw type), 또는 원시타입이라 한다.GenericBox integerBox = new GenericBox();원시 타입을 사용하면 내부의 타입 매개변수가
Object로 사용된다고 이해하면 된다.
- 제네릭 타입을 사용할 때는 항상
<>를 사용해서 사용시점에 타입을 지정해야 한다.- 그런데 왜 이런 로 타입을 지원하는 것일까?
- 자바의 제네릭이 자바가 처음 등장할 때부터 있었던 것이 아니라 자바가 오랜기간 사용된 이후에 등장했기 때문에 제네릭이 없던 시절의 과거 코드와의 하위 호환이 필요했다. 그래서 어쩔 수 없이 이런 로 타입을 지원한다
- 정리하면 로 타입을 사용하지 않아야 한다.
- 만약에
Object타입을 사용해야 한다면 다음과 같이 타입 인자로Object를 지정해서 사용하면 된다.
이번에는 직접 클래스를 만들고, 제네릭도 도입해보자.
지금부터 사용할 Animal 관련 클래스들은 이후 예제에서도 사용하므로generic.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 + '}'; } }public class Cat extends Animal{ public Cat(String name, int size){ super(name, size); } @Override public void sound() { System.out.println("냐옹"); } }public class Dog extends Animal{ public Dog(String name, int size) { super(name, size); } @Override public void sound() { System.out.println("멍멍"); } }
.super키워드를 사용하면 현재 클래스가 확장한 상위 클래스의 생성자를 호출할 수 있습니다.고양이 클래스인
Cat에서는Animal클래스의 생성자를 호출해야 합니다. 그렇지 않으면 상위 클래스인Animal클래스의 초기화가 제대로 이루어지지 않아 오류가 발생합니다.따라서
Cat클래스의 생성자에서는 다음과 같이super(name, size);를 사용하여Animal클래스의 생성자를 호출합니다.
여기서부터 패키지 경로 다름
public class Box<T>{ private T value; public void set(T value) { this.value = value; } public T get() { return value; } }public class AnimalMain1 { public static void main(String[] args) { Animal animal = new Animal("동물", 0); Dog dog = new Dog("멍멍이", 100); Cat cat = new Cat("냐옹이", 50); Box<Dog> dogBox = new Box<>(); dogBox.set(dog); Dog findDog = dogBox.get(); System.out.println("findDog = " + findDog); Box<Cat> catBox = new Box<>(); catBox.set(cat); Cat findCat = catBox.get(); System.out.println("findCat = " + findCat); Box<Animal> animalBox = new Box<>(); animalBox.set(animal); Animal findAnimal = animalBox.get(); System.out.println("findAnimal = " + findAnimal); } }실행 결과
findDog = Animal{name='멍멍이', size=100} findCat = Animal{name='냐옹이', size=50} findAnimal = Animal{name='동물', size=0}
Box제네릭 클래스에 각각의 타입에 맞는 동물을 보관하고 꺼낸다.Box<Dog> dogBox:Dog타입을 보관할 수 있다.Box<Cat> catBox:Cat타입을 보관할 수 있다.Box<Animal> animalBox:Animal타입을 보관할 수 있다.
여기서 Box<Animal> 의 경우 타입 매개변수 T 에 타입 인자 Animal 을 대입하면 다음 코드와 같다
public class Box<Animal> { private Animal value; public void set(Animal value) { this.value = value; } public Animal get() { return value; } }
- 따라서,
set이므로set()에Animal의 하위 타입인Dog,Cat도 전달할 수 있다.- 물론 이 경우 꺼낼 때는
Animal타입으로만 꺼낼 수 있다.public class AnimalMain2 { public static void main(String[] args) { Animal animal = new Animal("동물", 0); Dog dog = new Dog("멍멍이", 100); Cat cat = new Cat("냐옹이", 50); Box<Animal> animalBox = new Box<>(); animalBox.set(animal); animalBox.set(dog); // Animal = Dog animalBox.set(cat); // Animal = Cat Animal findAnimal = animalBox.get(); System.out.println("findAnimal = " + findAnimal); } }