728x90
12장 컬렉션 프레임워크 들어가기
- 우리가 사용하는 프로그램은 대부분 데이터를 사용하여 구현합니다.
- 메일 시스템은 메일을, 채팅 앱은 친구 목록과 채팅 내용 등을 관리합니다.
- 프로그램을 실행할 때 데이터를 효율적으로 관리하기 위해 자료 구조를 사용합니다.
- 이 장에서는 자료 구조를 구현한 다양한 인터페이스와 클래스를 소개합니다.
- 각 특성을 잘 이해하여 프로그램을 만들 때 활용해 보세요.
제네릭이란?
- 프로그램에서 변수를 선언할 때 모든 변수는 자료형이 있습니다.
- 메서드에서 매개변수를 사용할 때도 자료형을 가지고 있습니다.
- 대부분은 하나의 자료형으로 구현하지만, 변수나 메서드의 자료형을 필요에 따라 여러 자료형으로 바꿀 수 있다면 프로그램이 훨씬 유연할 것입니다.
- 이와 같이 어떤 값이 하나의 참조 자료형이 아닌 여러 참조 자료형을 사용할 수 있도록 프로그래밍하는 것을 '제네릭(Generic) 프로그래밍' 이라고 합니다.
- 제네릭 프로그램은 참조 자료형이 변환될 때 이에 대한 검증을 컴파일러가 하므로 안정적입니다.
- 우리가 앞으로 학습할 '컬렉션 프레임워크'도 많은 부분이 제네릭으로 구현되어 있습니다.
- TCP school 홈페이지에서는 제네릭에 대해서 다음과 같이 설명합니다.
:
자바에서 제네릭(generic)이란 데이터의 타입(data type)을 일반화(generalize)한다는 것을 의미합니다. 제네릭은 클래스나 메서드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법입니다. 이렇게 컴파일 시에 미리 타입 검사(type check)를 수행하면 다음과 같은 장점을 가집니다.
1. 클래스나 메소드 내부에서 사용되는 객체의 타입 안정성을 높일 수 있습니다.
2. 반환값에 대한 타입 변환 및 타입 검사에 들어가는 노력을 줄일 수 있습니다.
제네릭의 선언 및 생성
- 자바에서 제네릭은 클래스와 메소드에만 다음과 같은 방법으로 선언할 수 있습니다.
class MyArray<T> {
T element;
void setElement(T element) { this.element = element; }
T getElement() { return element; }
}
제네릭의 필요성
- 3D 프린터를 예로 들어 제네릭에 대해 이해해 봅시다.
- 3D 프린터에 쓰이는 재료는 여러가지가 있는데, 하나의 참조 자료형만 사용 가능하다면 여러 개의 프린터를 만들어야 한다.

- 예시 코드를 보면서 더 이해해 보겠습니다.
package generics;
public class Powder {
public void doPrinting() {
System.out.println("Powder 재료로 출력합니다");
}
public String toString() {
return "자료는 Powder 입니다";
}
}
package generics;
public class Plastic {
public void doPrinting() {
System.out.println("Plastic 재료로 출력합니다");
}
@Override
public String toString() {
return "자료는 Plastic 입니다";
}
}
- 우선 3D 프린터 클래스에서 사용할 Powder 클래스와 Plastic 클래스를 만들고 doPrinting 메서드 정의했고 toString 메서드를 재정의했습니다.
- 이번에는 파우더를 사용하는 3D 프린터 클래스 코드를 먼저 살펴보겠습니다.
package generics;
public class ThreeDPrinter {
private Powder material;
public Powder getMaterial() {
return material;
}
public void setMaterial(Powder material) {
this.material = material;
}
}
- 이번에는 Plastic 을 재료로 쓰는 3D 프린터 클래스를 만들어보겠습니다.
package generics;
public class ThreeDPrinter {
private Plastic material;
public Plastic getMaterial() {
return material;
}
public void setMaterial(Plastic material) {
this.material = material;
}
}
- 그런데 재료만 바뀌었을 뿐 프린터 기능이 동일하다면 프린터 클래스를 두 개 만드는 것은 비효율적입니다.
- 이런 경우에 어떤 재료든 쓸 수 있도록 material 변수의 자료형을 Object로 사용할 수 있습니다.
- Object 클래스는 모든 클래스의 최상위 클래스이므로 모든 클래스는 Object 로 변환할 수 있기 때문입니다.
- Object 로 변환한 코드는 다음과 같습니다.
package generics;
public class ThreeDPrinter {
private Object material;
public Object getMaterial() {
return material;
}
public void setMaterial(Object material) {
this.material = material;
}
}
- material 변수의 자료형을 Object 로 선언한 ThreeDPrinter 에 파우더를 재료로 사용하면 다음과 같은 테스트 코드를 구현할 수 있습니다.
package generics;
public class ThreeDPrinterTest {
public static void main(String[] args) {
ThreeDPrinter printer = new ThreeDPrinter();
Powder p1 = new Powder();
printer.setMaterial(p1); // Object material = Powder p1; 자동형변환
Powder p2 = (Powder)printer.getMaterial(); // Object 형으로 자동형변환 된 값을 다운캐스팅 하여 대입해야 하기 때문에 직접 형 변환을 해야 함.
}
}
- setMaterial( ) 메서드를 활용하여 Powder를 재료로 선택할 때는 매개변수 자료형이 Object 이므로 자동으로 형 변환이 됩니다.
- 하지만 반환형이 Object 클래스인 getMaterial( ) 메서드로 Powder 자료형 변수를 반환받을 때는 반드시 형 변환을 해줘야 합니다.
- 즉, 어떤 변수가 여러 참조 자료형을 사용할 수 있도록 Object 클래스를 사용하면 다시 원래 자료형으로 반환해(다운캐스팅) 주기 위해 매번 형 변환을 직접 해야 하는 번거로움이 있습니다.
- 이러한 경우에 사용하는 프로그래밍 방식이 제네릭입니다.
- 여러 참조 자료형이 쓰일 수 있는 곳에 특정한 자료형을 지정하지 않고, 클래스나 메서드를 정의한 후 사용하는 시점에 어떤 자료형을 사용할 것인지 지정하는 방식입니다.
- 하나씩 이해해보겠습니다.
generic 정리해보기
- generic 을 사용하는 이유는 여러 참조 자료형이 쓰일 수 있도록 Object 클래스를 사용하면 다시 원래 자료형으로 반환해(다운캐스팅) 주기 위해 매번 형 변환을 직접 해야 하는 번거로움을 해결해주기 때문이다.
- generic 은 여러 참조 자료형이 쓰일 수 있는 곳에 특정한 자료형(Object)을 지정하지 않고, 클래스나 메서드를 정의한 후 사용하는 시점에 어떤 자료형을 사용할 것인지 지정하는 방식입니다.
제네릭 클래스 정의하기
- 제네릭에서는 여러 참조 자료형을 사용해야 하는 부분에 Object 가 아닌 하나의 문자로 표현합니다.
- 앞에서 예를 든 ThreeDPrinter 를 제네릭 클래스로 정의하면 다음과 같습니다.
package generics;
public class GenericPrinter<T> { // 제네릭 클래스를 만들기 위해 다이아몬드 사용, T 는 type 의 약자로 자료형 매개변수로 사용한다는 것을 의미함
private T material;
public T getMaterial() {
return material;
}
public void setMaterial(T material) {
this.material = material;
}
}
- 코드를 보면 여러 자료형으로 바꾸어 사용할 material 변수의 자료형을 T라고 썼습니다.
- 이때 T를 자료형 매개변수(type parameter)라고 부릅니다.
- 클래스 이름을 GenericPrinter<T> 라고 정의하고 나중에 클래스를 사용할 때 T 위치에 실제 사용할 자료형을 지정합니다.
- 클래스의 각 메서드에서 해당 자료형이 필요한 부분에는 모두 T 문자를 사용하여 구분합니다.

다이아몬드 연산자 < >
- 자바 7부터는 제네릭 자료형의 클래스를 생성할 때 생성자에 사용하는 자료형을 명시하지 않을 수 있습니다.
- 우리가 많이 사용했던 ArrayList 를 보겠습니다.
ArrayList<String> List = new ArrayList< >( ); //생략가능
- 여기에서 < > 를 다이아몬드 연산자라고 합니다.
- 선언된 자료형을 보고 생략된 부분이 String임을 컴파일러가 유추할 수 있기 때문에 생성 부분에서는 생략할 수 있습니다.
자료형 매개변수 T 와 static
- static 변수나 메서드는 인스턴스를 생성하지 않아도 클래스 이름으로 호출할 수 있습니다.
- static 변수는 인스턴스 변수가 생서되기 이전에 생성됩니다.
- 또한 static 메서드에서는 인스턴스 변수를 사용할 수 없습니다.
- 그런데 T의 자료형이 결정되는 순간은 제네릭 클래스의 인스턴스가 생성되는 순간입니다.
- 따라서 T의 자료형이 결정되는 시점보다 빠르기 때문에 static 변수의 자료형이나 static 메서드 내부 변수의 자료형으로 T를 사용할 수 없습니다.
- 자료형 매개변수로 T 외에 다른 문자도 사용할 수 있습니다. E 는 element, K는 key, V는 value를 의미합니다.
- 의미가 그렇다는 것이지 꼭 이런 문자를 사용해야하는 것은 아니며 다른 문자를 사용해도 되지만 일반적으로 이렇게 사용합니다.
제네릭에서 자료형 추론하기
- 02장 변수와 자료형에서 잠깐 소개했듯이 자바 10부터는 지역 변수에 한해서 자료형을 추론할 수 있습니다.
- 이는 제네릭에도 적용됩니다.
ArrayList<String> list = new ArrayList<String>( );
//->
var list = new ArrayList<String>( );
- 생성되는 인스턴스를 바탕으로 list의 자료형이 ArrayList<String>임을 추론할 수 있기 때문입니다.
- 물론 list 가 지역 변수로 선언되는 경우만 가능합니다.
제네릭 클래스 사용하기
- 파우더가 재료인 프린터는 다음과 같이 선언하여 생성합니다.
package gamelevel;
import generics.GenericPrinter;
import generics.Powder;
public class GenericPrinterTest {
public static void main(String[] args) {
GenericPrinter<Powder> powderPrinter = new GenericPrinter<Powder>();
powderPrinter.setMaterial(new Powder());
Powder powder = powderPrinter.getMaterial(); // object 를 사용했을 때와는 달리 명시적 형 변환을 하지 안항도 괜찮음
}
}
- 위 코드를 보면 GenericPrinter 를 만들 때 < > 다이아몬드 연산자 안에 어떤 자료형을 가질지 표시했습니다.
- 또 매개변수로 T 를 넣어주어야 할 때 자료형 생성자를 넣어 주었습니다. (new Powder)
- 마지막으로 Object 를 자료형으로 사용했을 때와는 달리 제네릭을 사용하면 인스턴스를 만들 때 자료형을 정해주기 때문에 자료를 꺼내 대입하기 위해 다운 캐스팅을 할 때 명시적 형 변환을 해주지 않아도 되는 것을 알 수 있습니다.
- 이렇게 실제 제네릭 클래스를 사용할 때 T 위치에 사용한 Powder 형을 '대입된 자료형' 이라고 합니다.
- Powder 를 대입해 만든 GenericPrinter<Powder> 를 '제네릭 자료형'이라고 합니다.
제네릭에서 대입된 자료형을 명시하지 않은 경우
- 제네릭 클래스를 사용할 때는 GenericPrinter<Powder> 의 Powder 와 같이 대입된 자료형을 명시해야 합니다.
- 그런데 다음과 같이 자료형을 명시하지 않고도 사용할 수 있습니다.
- 이 문법은 이전 버전과의 호환을 위해 제공됩니다.
GenericPrinter powderPrinter2 = new GenericPrinter( );
powderPrinter2.setMaterial(new Powder( ));
Powder powder = (Powder)powderPrinter.getMaterial( );
- 이렇게 클래스에 대입된 자료형을 명시하지 않는 경우 컴파일 오류는 아니지만, 사용할 자료형을 명시하라는 의미로 노란색 경고 줄이 나타납니다.
- 또한 컴파일러가 어떤 자료형을 사용할 것인지 알 수 없으므로 getMaterial( ) 메서드에서 강제로 형 변환을 해야 합니다.
- 따라서 제네릭 클래스를 사용하는 경우에는 되도록이면 대입된 자료형으로 사용할 참조 자료형을 지정하는 것이 좋습니다.
T 자료형에 사용할 자료형을 제한하는 <T extends 클래스>
- 제네릭 클래스에서 T 자료형에 사용할 자료형에 제한을 둘 수 있습니다.
- 예를 들어 우리가 구현한 GenericPrinter<T> 클래스는 사용할 수 있는 재료가 한정되어 있습니다.
- 무슨 말이냐면 우리가 이전에 만든 GenericPrinter<T> 클래스에서는 Plastic 과 Powder 클래스를 재료로 사용했습니다.
- 그런데, 해당 클래스에서 Water 를 재료로 써도 될까요?
- 안 됩니다.
- 이런 일을 방지하기 위해 사용할 클래스에 자료형 제한을 두는 방식으로 extends 예약어를 사용할 수 있습니다.
- 이 말이 무슨 말이냐면, 우리가 이전에 만든 Plastic 과 Powder 클래스가 Material 이라는 추상 클래스를 상속받도록 만들고, T 자료형에 사용할 자료형을 제한하는 방법으로 <T extends Material> 이런 식으로 작성한다는 겁니다.
- 실제로 코드를 구성해보면서 살펴보겠습니다.
package generics;
public abstract class Material {
public abstract void doPrinting();
}
- Material 클래스는 위의 코드처럼 추상 클래스로 정의하였습니다.
- 상속받은 클래스는 doPrinting( ) 추상 메서드를 반드시 구현해야 합니다.
package generics;
public class Powder extends Material {
public void doPrinting() {
System.out.println("Powder 재료로 출력합니다");
}
@Override
public String toString() {
return "자료는 Powder 입니다";
}
}
package generics;
public class Plastic extends Material {
public void doPrinting() {
System.out.println("Plastic 재료로 출력합니다");
}
@Override
public String toString() {
return "자료는 Plastic 입니다";
}
}
- Material 을 상속받은 Powder 와 Plastic 클래스 코드는 위와 같습니다.
package generics;
public class GenericPrinter<T extends Material> { // 제네릭 클래스를 만들기 위해 다이아몬드 연산자 사용, T 는 type 의 약자로 자료형 매개변수로 사용한다는 것을 의미함.
// extends 예약어로 사용할 수 있는 자료형에 제한을 둠
private T material;
public T getMaterial() {
return material;
}
public void setMaterial(T material) {
this.material = material;
}
}
- <T extends Material> 을 사용한 코드는 위와 같습니다.
- 클래스 이름에 <T extends Material> 이라고 명시하여 사용할 수 있는 자료형에 제한을 두었습니다.
<T extends 클래스>로 상위 클래스 메서드 사용하기
- <T extends Material> 로 선언하면 제네릭 클래스를 사용할 때 상위 클래스 Material에서 선언한 메서드를 사용할 수도 있습니다.
- 우선 <T extends Material> 를 하지 않은 상태부터 확인해 보겠습니다.
public class GenericPrinter<T> {
private T material;
}
- T 는 컴파일할 때 Object 클래스로 변환됩니다.
- 따라서 이 경우에 Object 클래스가 기본으로 제공하는 메서드만 사용할 수 있습니다.
- 왜냐하면 자료형을 알 수 없기 때문입니다. (자료형은 나중에 인스턴스가 생성될 때 지정되기 때문입니다)
- 만약 <T extends Material> 을 사용하면 Material 클래스에서 선언한 메서드들을 사용할 수 있습니다.
- 이렇게 되는 이유는 <T extends Material> 을 사용하면 컴파일할 때 내부적으로 T 자료형이 Object 가 아닌 Material 로 변환되기 때문입니다.
제네릭 메서드 generic method
- 제네릭 메서드란 메서드의 선언부에 타입 변수를 사용한 메서드를 의미합니다.
- 이때 타입 변수의 선언은 반환 타입 바로 앞에 위치합니다.
public static <T> void sort(...) {...}
- 이때 주의할 점은 제네릭 클래스에서 정의된 타입 변수 T와 제네릭 메소드에서 사용된 타입 변수 T는 전혀 다른 별개의 것입니다.
class AnimalList<T> {
...
public static <T> void sort() {
...
}
...
}
와일드카드의 사용
- 와일드카드(wild card)란 이름에 제한을 두지 않음을 표현하는 데 사용되는 기호를 의미합니다.
- 자바의 제네릭에서는 물음표(?) 기호를 사용하여 이러한 와일드카드를 사용할 수 있습니다.
<?> // 타입 변수에 모든 타입을 사용할 수 있음
<? extends T> // T 타입과 T 타입을 상속받는 자손 클래스 타입만을 사용할 수 있음.
<? super T> // T 타입과 T 타입이 상속받은 조상 클래스 타입만을 사용할 수 있음.
- 와일드 카드 사용 예시는 다음과 같습니다. (TCP School 출처)
package genericsex1;
import java.util.*;
class LandAnimal { public void crying() { System.out.println("육지동물"); } }
class Cat extends LandAnimal { public void crying() { System.out.println("냐옹냐옹"); } }
class Dog extends LandAnimal { public void crying() { System.out.println("멍멍"); } }
class Sparrow { public void crying() { System.out.println("짹짹"); } }
class AnimalList<T> {
ArrayList<T> al = new ArrayList<T>();
public static void cryingAnimalList(AnimalList<? extends LandAnimal> al) {
LandAnimal la = al.get(0);
la.crying();
}
void add(T animal) { al.add(animal); }
T get(int index) { return al.get(index); }
boolean remove(T animal) { return al.remove(animal); }
int size() { return al.size(); }
}
public class Frog {
public static void main(String[] args) {
AnimalList<Cat> catList = new AnimalList<Cat>();
catList.add(new Cat());
AnimalList<Dog> dogList = new AnimalList<Dog>();
dogList.add(new Dog());
AnimalList.cryingAnimalList(catList);
AnimalList.cryingAnimalList(dogList);
}
}
- 여기서 궁금한 점이 하나 있지 않으세요?
- 저는, cryingAnimalList 메서드에서 ? 와일드카드 대신 T 를 사용해도 동일하게 작동하지 않을까 생각했는데, 여러분의 생각은 어떠신가요?
- 정답은 작동하지 않는다입니다.
- 왜냐하면 클래스에서 사용한 T 자료형 매개변수와 클래스 내부 메서드에서 사용되는 T 는 전혀 다른 것이기 때문입니다.
- 따라서 인스턴스가 생성될 때 자료형이 정해지는 클래스 자료형 매개변수와 다르게 클래스 내부 메서드에 있는 T 는 자료형이 정해지지 않기 때문에 오류가 발생하게 됩니다.
제네릭 메서드 활용하기
- 메서드의 매개변수를 자료형 매개변수로 사용하는 경우에 대해 알아봅시다.
- 또한 자료형 매개 변수가 하나 이상인 경우도 살펴보겠습니다.
- 제네릭 메서드의 일반 형식은 다음과 같습니다.
public <자료형매개변수> 반환형 메서드이름(자료형매개변수) {}
- 반환형 앞에 사용하는 <자료형 매개변수>는 여러 개일 수 있으며, 이는 메서드 내에서만 유효합니다.
- 자료형 매개변수를 여러 개 사용하는 제네릭 메서드 예제를 살펴보겠습니다.
package generics;
public class Point<T, V> {
T x;
V y;
public Point(T x, V y) {
super();
this.x = x;
this.y = y;
}
public T getX() { // 제네릭 메서드, 자료형 매개변수를 반환한다
return x;
}
public V getY() {
return y;
}
}
- 위와 같은 Point 클래스가 있습니다.
- 이 클래스는 한 점을 나타내기 위해 x, y 두 멤버 변수를 사용하는데 이는 모두 자료형 매개변수로 선언합니다.
- 한 점을 나타내는 Point 클래스의 두 좌표 x, y 는 정수일 수도 있고 실수일 수도 있습니다.
- 그래서 T와 V 라는 자료형 매개변수로 표현했습니다.
- 그리고 이 변수들을 위한 메서드 getX( ), getY( ) 는 T와 V를 반환하고 있으므로 제네릭 메서드입니다.
- 이제 Point 클래스를 활용하여 다음과 같이 두 점을 생성합니다.
Point<Integer, Double> p1 = new Point<>(0, 0.0);
Point<Integer, Double> p2 = new Point<>(10, 10.0);
- 두 점의 위치를 표현할 때 X 좌표는 Integer를 사용하였고 y 좌표는 Double을 사용하였습니다.
- 컴파일러는 선언된 자료형을 보고 생성되는 인스턴스의 자료형을 유추할 수 있으므로 < > 다이아몬드 연산자에는 자료형을 명시하지 않아도 됩니다.
- 그러면 두 점을 매개변수로 받아 만들어지는 사각형의 넓이를 계산하는 makeRectangle( ) 메서드를 만들어 보겠습니다.
- 우리가 방금 두 점을 만들었을 때 정수와 실수를 마음대로 사용했던 것처럼 만드려는 사각형의 두 점도 정수와 실수를 가질 수 있기 때문에 넓이를 계산하는 makeRectangle( ) 역시 제네릭 메서드로 만들어야 합니다.
package generics;
public class GenericMethod {
public static <T, V> double makeRectangle(Point<T, V> p1, Point<T, V> p2) {
double left = ((Number)p1.getX()).doubleValue();
double right = ((Number)p2.getX()).doubleValue();
double top = ((Number)p1.getY()).doubleValue();
double bottom = ((Number)p2.getY()).doubleValue();
double width = right - left;
double height = bottom - top;
return width * height;
}
public static void main(String[] args) {
Point<Integer, Double> p1 = new Point<Integer, Double>(0, 0.0);
Point<Integer, Double> p2 = new Point<Integer, Double>(10, 10.0);
double rect = GenericMethod.<Integer, Double>makeRectangle(p1, p2);
System.out.println("두 점으로 만들어진 사각형의 넓이는 " + rect + "입니다.");
}
}
- 여기서 한 가지 주의할 점이 있습니다.
public static double makeRectangle ... 이라고 코드를 작성했다면 어떻게 되었을까요? - 오류가 발생하게 됩니다. 왜냐하면 매개변수로 사용된 T, V 의 정의가 없기 때문인데요.
- 클래스 혹은 메서드에서 T와 V를 정의해주어야 오류가 사라지게 됩니다.

- GenericMethod 클래스는 제네릭 클래스가 아닙니다.
- 제네릭 클래스가 아니라도 내부에 제네릭 메서드를 구현할 수 있습니다.
- 또한 makeRectangle( ) 메서드에서 사용하는 T와 V는 makeRectangle( ) 메서드 내부에서만 유효하게 사용할 수 있습니다.
- 제네릭 클래스 안에 제네릭 메서드를 선언했다고 가정해볼까요?
class Shape<T> {
public static <T, V> double makeRectangle(Point<T, V> p1, Point<T, V> p2) {
...
}
}
- 이때 Shape<T> 에서 사용한 T 와 makeRectangle( ) 에서 사용한 T는 전혀 다른 의미입니다.
- 앞에서 설명했듯인 makeRectangle( ) 메서드에서 사용한 T는 메서드 내에서만 유효합니다.
- 또한
double rect = GenericMethod.<Integer, Double>makeRectangle(p1, p2);
double rect = GenericMethod.>makeRectangle(p1, p2); // 컴파일러가 유추 가능하여 생략
- 구현한 제네릭 메서드를 호출할 때 사용할 자료형을 대입하는 것을 생략할 수 있습니다.
제네릭의 형 제한과 형 제한 푼 클래스 만들기
package genericsex2;
public class GenericMethodTest1 {
public static <T, V> double makeRectangle(Point<T, V> p1, Point<T, V> p2) {
double left = ((Number)p1.getX()).doubleValue();
double bottom = ((Number)p1.getY()).doubleValue();
double right = ((Number)p2.getX()).doubleValue();
double top = ((Number)p2.getY()).doubleValue();
double width = right - left;
double height = top - bottom;
return width * height;
}
public static void main(String[] args) {
Point<Integer, Integer> p1 = new Point<Integer, Integer>(0, 0);
Point<Integer, Integer> p2 = new Point<Integer, Integer>(10, 10);
// Point<Double, Double> p3 = new Point<Double, Double>(0.0, 0.0);
// Point<Double, Double> p4 = new Point<Double, Double>(10.0, 10.0);
// double result1 = GenericMethodTest1.<Integer, Integer>makeRectangle(p1, p2);
// double result2 = GenericMethodTest1.<Double, Double>makeRectangle(p3, p4);
double result3 = GenericMethodTest1.<Number, Number>makeRectangle(p1,p2);
// System.out.println(result1);
// System.out.println(result2);
}
}
- 앞선 예제에서는 <Integer, Integer>makeRectangle 처럼 메서드를 사용할 때 제네릭을 통해 형 제한을 시켰습니다.
- 이 메서드에서 사용되는 형이 Integer, Integer 라는 것을 확신할 수 있었지만, Point의 자료형이 제한되면서 형에 맞는 Point 들만 묶어야 코드가 잘 작동하는 것을 볼 수 있습니다.
- 무슨 말이냐면 Integer, Integer 라고 형 제한을 시킨 makeRectangle 함수에서 Integer, Double 형태로 생성된 Point 를 매개변수로 쓸 수 없었다는 말을 의미합니다.
- 이런 불편함이 싫다면 다음과 같이 제네릭을 해제하고 이전에 Object를 썼던 것처럼 Number 를 사용할 수 있습니다.
package genericsex2;
public class PointNumber {
Number x;
Number y;
public PointNumber(Number x, Number y) {
super();
this.x = x;
this.y = y;
}
public Number getX() {
return x;
}
public Number getY() {
return y;
}
}
package genericsex2;
public class GenericMethodNumberTest1 {
public static double makeRectangle(PointNumber p1, PointNumber p2) {
double left = ((Number)p1.getX()).doubleValue();
double bottom = ((Number)p1.getY()).doubleValue();
double right = ((Number)p2.getX()).doubleValue();
double top = ((Number)p2.getY()).doubleValue();
double width = right - left;
double height = top - bottom;
return width * height;
}
public static void main(String[] args) {
PointNumber p1 = new PointNumber(0, 0);
PointNumber p2 = new PointNumber(10.0, 10.0);
double result1 = GenericMethodNumberTest1.makeRectangle(p1,p2);
System.out.println(result1);
}
}
- 이렇게 만들면 PointNumber 가 어떤 자료형을 갖던 Number 클래스를 상속하는 자료형이라면 오류 없이 코드가 잘 작동한다.
컬렉션 프레임워크에서 사용하는 제네릭
- 앞으로 공부할 컬렉션 프레임워크에서도 다양한 자료형을 관리하기 위해 제네릭을 자주 사용합니다.
- ArrayList를 예로 들어 봅시다.
- ArrayList.java 에서 ArrayList 클래스의 정의는 다음과 같습니다.
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
...
}
- 배열은 요소를 가지므로 T보다는 Element 를 의미하는 E를 더 많이 사용합니다.
- ArrayList<E> 에서 E 위치에 원하는 자료형을 넣어 배열을 사용할 수 있습니다.
제네릭 연습해보기
- 혼자 제네릭이 잘 이해가 가지 않아서 복습을 하다가, 수정하고 싶은 부분들이 보여서 프로그램을 수정했다.
- 프로그래밍의 매력은 이런 점이 아닐까 싶다.
- 우선, 교재에서는 doPrinting 메서드가 Material 클래스를 상속받은 클래스들에 구현되어 있었는데, 나는 이 메서드는 Printer에 있어야 한다고 생각했다.
- 그리고 연습을 따로 더 하고 싶었던 이유는 object 를 활용해서 클래스를 구현했을 때는 그래도 쉽게 이해가 갔었는데, 제네릭을 적용한 상태에서 구현하는 것이 생각보다 더 이해가 쉽게 되지 않았다.
- 우선 object 를 사용했을 때 코드이다.
package genericsex1;
public abstract class Material {
}
package genericsex1;
public class Powder extends Material{
@Override
public String toString() {
return "Powder";
}
}
package genericsex1;
public class Plastic extends Material{
@Override
public String toString() {
return "Plastic";
}
}
package genericsex1;
public class ThreeDPrinter {
private Object material;
public Object getMaterial() {
return material;
}
public void setMaterial(Object material) {
this.material = material;
}
public void doPrint() {
System.out.println(this.material + " 재료를 사용하여 프린트합니다.");
}
}
package genericsex1;
public class ThreeDPrinterTest {
public static void main(String[] args) {
ThreeDPrinter printer = new ThreeDPrinter();
Powder powder = new Powder();
Plastic plastic = new Plastic();
printer.setMaterial(powder);
printer.doPrint();
Powder powder2 = (Powder)printer.getMaterial(); // 직접 형변환을 해서 Powder 를 가져와야한다.
System.out.println(powder2);
printer.setMaterial(plastic); // Object로 자료형을 사용하며 재밌는 점인데, printer 객체를 하나만 생성한 다음 setMaterial을 활용해서 수정해주면 된다.
printer.doPrint();
Plastic plastic2 = (Plastic)printer.getMaterial();
System.out.println(plastic2);
}
}
- 이번에는 제네릭을 적용한 코드들이다.
- Material, Powder, Plastic 클래스들의 변화는 없다.
package genericsex1;
public class GenericThreeDPrinter<T extends Material> {
private T material;
public GenericThreeDPrinter(T material) {
super();
this.material = material;
}
public T getMaterial() {
return material;
}
// public void setMaterial(T material) {
// this.material = material;
// } // 생성자에서 material 설정하는 것으로 대체
public void doPrint() {
System.out.println(this.material + " 재료를 사용하여 프린트합니다.");
}
}
package genericsex1;
public class GenericThreeDPrinterTest {
public static void main(String[] args) {
GenericThreeDPrinter<Powder> PowderPrinter = new GenericThreeDPrinter<>(new Powder());
GenericThreeDPrinter<Plastic> PlasticPrinter = new GenericThreeDPrinter<>(new Plastic());
PowderPrinter.doPrint();
PlasticPrinter.doPrint();
Powder powder = PowderPrinter.getMaterial(); // 강제 형 변환이 필요가 없다.
Plastic plastic = PlasticPrinter.getMaterial();
System.out.println(powder);
System.out.println(plastic);
}
}
- generic 을 사용하는 경우에 인스턴스가 생성될 때 자료형을 우리가 지정해주기 때문에, 다운캐스팅을 위한 형 변환을 해줄 필요가 없는 놀라운 효과를 얻게 된다.