13.1 제네릭이란?
다음과 같이 Box 클래스를 선언하려고 한다. Box에 넣을 내용물로 content 필드를 선언하려고 할 때, 타입을 무엇으로 해야 할까?

Box는 다양한 내용물을 저장해야 하므로 특정 클래스 타입으로 선언할 수 없다. 그래서 다음과 같이 Object 타입으로 선언한다.

Object 타입은 모든 클래스의 최상위 부모 클래스이다. 그렇기 때문에 모든 객체는 부모 타입인 Object로 자동 타입 변환이 되므로 content 필드에는 어떤 객체든 대입이 가능하다.

문제는 Box안의 내용물을 얻을 때이다. content는 Object 타입이므로 어떤 객체가 대입되어 있는지 확실하지 않다. 이때 대입된 내용물의 타입을 안다면 강제 타입 변환을 거쳐 얻을 수 있다. 예를들어 내용물이 String 타입이라면 (String)으로 강제 타입 변환해서 내용물을 얻는 식이다.

그러나 어떤 내용물이 저장되어 있는지 모른다면 instanceof 연산자로 타입을 조사할 수는 있지만 모든 종류의 클래스를 대상으로 조사할 수는 없다. 따라서 Object 타입으로 content 필드를 선언하는 것은 좋은 방법이 아니다.
Box를 생성하기 전에 우리는 어떤 내용물을 넣을지 이미 알고 있다. 따라서 Box를 생성할 때 저정할 내용물의 타입을 미리 알려주면 Box는 content에 무엇이 대입되고, 읽을 때 어떤 타입으로 제공할지를 알게 된다. 이것이 제네릭이다.

다음은 Box 클래스에서 결정되지 않은 content의 타입을 T라는 타입 파라미터로 정의한 것이다.

<T>는 T가 타입 파라미터임을 뜻하는 기호로, 타입이 필요한 자리에 T를 사용할 수 있음을 알려주는 역할을 한다.
여기에서 Box 클래스는 T를 content 필드의 타입으로 사용하였다. 즉, Box 클래스는 T가 무엇인지 모르지만, Box 객체가 생성될 시점에 다른 타입으로 대체된다는 것을 알고 있다. 만약 Box의 내용물로 String을 저장하고 싶다면 다음과 같이 Box를 생성할 때 타입 파라미터 T대신 String으로 대체하면 된다.

Box의 내용물로 100을 저정하고 싶다면 다음과 같이 Box를 생성할 때 타입 파리미터 T 대신 Integer로 대체하면 된다. Integer는 정수값을 표현하는 클래스 타입이다.

사실 <T>에서 타입 파라미터로 쓰이는 T는 단지 이름일 뿐이기 때문에 T대신 A부터 Z까지 어떤 알파벳을 사용해도 좋다. 주의할 점은 타입 파라미터를 대체하는 타입은 클래스 및 인터페이스라는 것이다. 바로 위 코드에서 Box<int>라고 하지 않은 이유는 기본 타입은 타입 파라미터의 대체 타입이 될 수 없기 때문이다.
그리고 변수를 선언할 때와 동일한 타입을 호출하고 싶다면 생성자 호출 시 생성자에는 타입을 명시하지 않고 <>만 붙일 수 있다.

실습을 통해 이해해보자. 다음과 같이 Box 클래스를 작성한다.
package ch13.sec01;
public class Box<T> {
public T content;
}
GenericExample 클래스를 다음과 같이 작성하고 실행해보자.
package ch13.sec01;
public class GenericExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
Box<String> box1 = new Box<>();
box1.content = "안녕하세요.";
String str = box1.content;
System.out.println(str);
Box<Integer> box2 = new Box<>();
box2.content = 100;
Integer value = box2.content;
System.out.println(value);
}
}

13.2 제네릭 타입
제네릭 타입은 결정되지 않은 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다. 제네릭 타입은 선언부에 '<>' 부호가 붙고 그 사이에 타입 파라미터들이 위치한다.

타입 파라미터는 변수명과 동일한 규칙에 따라 작성할 수 있지만, 일반적으로 대문자 알파벳 한 글자로 표현한다. 외부에서 제네릭 타입을 사용하려면 구체적인 타입을 지정해야 한다. 만약 지정하지 않으면 Object 타입이 암묵적으로 사용된다.
다음 예제에서 Product 클래스를 제네릭 타입으로 선언해보자. kind와 model 필드를 타입 파라미터로 선언하고, Getter의 매개변수와 Setter의 리턴 타입 역시 타입 파라미터로 선언한다. 이렇게 타입 파라미터를 사용하는 이유는 Product에 다양한 종류와 모델 제품을 저장하기 위해서이다.
package ch13.sec02.exam01;
// 제네릭 타입 -> 결정되지 않은 타입을 파라미터로 가지는 클래스와 인터페이스를 말함.
public class Product<K, M> {
// 필드
private K kind;
private M model;
// 메소드
public K getKind() {
return this.kind;
}
public M getModel() {
return this.model;
}
public void setKind(K kind) {
this.kind = kind;
}
public void setModel(M model) {
this.model = model;
}
}
package ch13.sec02.exam01;
public class Tv {
}
package ch13.sec02.exam01;
public class Car {
}
다음 GenericExample 클래스는 Product 제네릭 타입을 이용해서 TV와 Car를 저장하고 얻는 방법을 보여준다.
package ch13.sec02.exam01;
public class GenericExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
// K는 Tv로 대체, M은 String으로 대체
Product<Tv, String> proudct1 = new Product<>();
// Setter 매개값은 반드시 Tv와 String을 제공
proudct1.setKind(new Tv());
proudct1.setModel("스마트 Tv");
// Getter 리턴값은 Tv와 String이 됨
Tv tv = proudct1.getKind();
String tvModel = proudct1.getModel();
System.out.println(tv);
System.out.println(tvModel);
// --------------------------------------------------
// K는 Car로 대체, M은 String으로 대체
Product<Car, String> product2 = new Product<>();
// Setter 매개값은 반드시 Car와 String을 제공
product2.setKind(new Car());
product2.setModel("SUV 자동차");
// Getter 리턴값은 Car와 String이 됨.
Car car = product2.getKind();
String carModel = product2.getModel();
System.out.println(car);
System.out.println(carModel);
}
}

이번에는 Rentable 인터페이스를 제네릭으로 선언해보자. 다양한 대상을 렌트하기 위해 rent() 메소드의 리턴 타입을 타입 파라미터로 선언한다.
package ch13.sec02.exam02;
public interface Rentable<P> {
P rent();
}
렌트 대상인 Home과 Car 클래스를 다음과 같이 작성해보자.
package ch13.sec02.exam02;
public class Home {
public void turnOnLight() {
System.out.println("전등을 켭니다.");
}
}
package ch13.sec02.exam02;
public class Car {
public void run() {
System.out.println("자동차가 달립니다.");
}
}
다음 HomeAgency와 CarAgency는 집과 자동차를 렌트해주는 대리점 클래스로, Rentable의 타입 파라미터를 Home과 Car로 대체해서 구현하는 방법을 보여준다.
package ch13.sec02.exam02;
public class HomeAgency implements Rentable<Home> {
@Override
public Home rent() {
// TODO Auto-generated method stub
return new Home();
}
}
package ch13.sec02.exam02;
public class CarAgency implements Rentable<Car> {
@Override
public Car rent() {
// TODO Auto-generated method stub
return new Car();
}
}
다음 GenericExample 클래스는 HomeAgency와 CarAgency에서 대여한 Home과 Car를 이용하는 방법을 보여준다.
package ch13.sec02.exam02;
public class GenericExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
HomeAgency homeAgency = new HomeAgency();
Home home = homeAgency.rent();
home.turnOnLight();
CarAgency carAgency = new CarAgency();
Car car = carAgency.rent();
car.run();
}
}

타입 파라미터는 기본적으로 Object 타입으로 간주되므로 Object가 가지고 있는 메소드를 호출할 수 있다. 다음 예제는 Box의 내용물을 비교하기 위해 타입 파라미터로 Object의 equals() 메소드를 호출한다.
package ch13.sec02.exam03;
public class Box<T> {
public T content;
// Box의 내용물이 같은지 비교
public boolean compare(Box<T> other) {
boolean result = content.equals(other.content);
return result;
}
}
package ch13.sec02.exam03;
public class GenericExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
Box box1 = new Box();
box1.content = "100";
Box box2 = new Box();
box2.content = "100";
Box box3 = new Box();
box3.content = 100; // -> 박싱을 생각하자.
boolean result1 = box1.compare(box2);
System.out.println("result1 : " + result1);
boolean result2 = box1.compare(box3);
System.out.println("result2 : " + result2);
}
}

13.3 제네릭 메소드
제네릭 메소드는 타입 파라미터를 가지고 있는 메소드를 말한다. 타입 파라미터가 메소드 선언부에 정의된다는 점에서 제네릭 타입와 차이가 있다. 제네릭 메소드는 리턴 타입 앞에 <> 기호를 추가하고 타입 파라미터를 정의한 뒤, 리턴 타입과 매개변수 타입에서 사용한다.
제네릭 메소드는 리턴타입과 매개변수 타입에서 사용한다.

다음 boxing() 메소드는 타입 파라미터로 <T>를 정의하고 매개변수 타입과 리턴 타입에서 T를 사용한다.
정확히 리턴 타입은 T로 내용물로 갖는 Box 객체이다.

타입 파라미터 T는 매개값이 어떤 타입이냐에 따라 컴파일 과정에서 구체적인 타입으로 대체된다.

1번은 100의 클래스 타입이 Integer이므로 타입 파라미터 T는 Integer로 대체되어 Box<Integer>가 리턴된다.
2번 "안녕하세요"의 클래스 타입이 String이므로 타입 파라미터 T는 String으로 대체되어 Box<String>이 리턴된다.
실습을 해보자. 먼저 제네릭 타입인 Box 클래스를 다음과 같이 선언한다.
package ch13.sec03.exam01;
public class Box<T> {
// 필드
private T t;
// Getter 메소드
public T get() {
return t;
}
// Setter 메소드
public void set(T t) {
this.t = t;
}
}
다음 GenericExample 클래스는 제네릭 메소드인 boxing을 선언하고 호출하는 방법을 보여준다.
package ch13.sec03.exam01;
public class GenericExample {
// 제네릭 메소드
// 매개변수 타입 / 리턴 타입
public static <T> Box<T> boxing(T t) {
Box<T> box = new Box<>();
box.set(t);
return box;
}
public static void main(String[] args) {
// 제네릭 메소드 호출
Box<Integer> box1 = boxing(100);
int intValue = box1.get();
System.out.println(intValue);
// 제네릭 메소드 호출
Box<String> box2 = boxing("홍길동");
String strValue = box2.get();
System.out.println(strValue);
}
}

13.4 제한된 타입 파라미터 (중요)
경우에 따라서는 타입 파라미터를 대체하는 구체적인 타입을 제한할 필요가 있다. 예를 들어 숫자를 연산하는 제네릭 메소드는 대체 타입으로 Number 또는 자식 클래스(Byte, Short, Integer, Long, Double)로 제한할 필요가 있다.
이처럼 모든 타입으로 대체할 수 없고, 특정 타입과 자식 또는 구현 관계에 있는 타입만 대체할 수 있는 타입 파라미터를 제한된 타입 파라미터라고 한다. 정의는 다음과 같이 한다.

상위 타입은 클래스뿐만 아니라 인터페이스도 가능하다. 인터페이스라고 해서 implements를 사용하지는 않는다. 다음은 Number 타입과 자식 클래스(Byte, Short, Integer, Long, Double)에만 대체 가능한 타입 파라미터를 정의한 것이다.

타입 파라미터가 Number 타입으로 제한되면서 Object의 메소드뿐만 아니라 Number가 가지고 있는 메소드도 사용할 수 있다. 위 코드에서 doubleValue() 메소드는 Number 타입에 정의되어 있는 메소드로, double 타입 값을 리턴한다.
package ch13.sec04;
public class GenericExample {
// 제한된 타입 파라미터를 갖는 제네릭 메소드
public static <T extends Number> boolean Compare(T t1, T t2) {
// T의 타입을 출력
System.out.println("compare(" + t1.getClass().getSimpleName() + ", " + t2.getClass().getSimpleName() + ")");
// Number의 메소드를 사용
double v1 = t1.doubleValue();
double v2 = t2.doubleValue();
return (v1 == v2);
}
public static void main(String[] args) {
// 제네릭 메소드 호출
boolean result1 = Compare(10.0, 20);
System.out.println(result1);
System.out.println();
boolean result2 = Compare(4.5, 4.5);
System.out.println(result2);
}
}

13.5 와일드카드 타입 파라미터
제네릭 타입을 매개값이나 리턴 타입으로 사용할 때 타입 파라미터로 ?(와일드카드)를 사용할 수 있다.
?는 범위에 있는 모든 타입으로 대체할 수 있다는 표시이다. 예를 들어 다음과 같은 상속 관계가 있다고 가정해보자.

타입 파라미터의 대체 타입으로 Student와 자식 클래스인 HighStudent와 MiddleStudent만 가능하도록 매개변수를 다음과 같이 선언할 수 있다.

반대로 Workder와 부모 클래스인 Person만 가능하도록 매개변수를 다음과 같이 선언할 수 있다.

어떤 타입이든 가능하도록 매개변수를 선언할 수도 있다.

다음 예제에서 Course 클래스의 메소드 registerCourse1()은 모든 사람이 들을 수 있는 과정을 등록하고, registerCourse2()는 학생만 들을 수 있는 과정을 등록한다. 그리고 registerCourse3()은 직장인과 일반인만 들을 수 있는 과정을 등록한다.
package ch13.sec05;
public class Person {
}
class Worker extends Person {
}
class Student extends Person {
}
class HighStudent extends Student {
}
class MiddleStudent extends Student {
}
package ch13.sec05;
public class Applicant<T> {
public T kind;
public Applicant(T kind) {
this.kind = kind;
}
}
package ch13.sec05;
public class Course {
// 모든 사람이면 등록가능
public static void registerCourse1(Applicant<?> applicant) {
System.out.println(applicant.kind.getClass().getSimpleName() + "이(가) Course1을 등록함");
}
// 학생만 등록 가능 -> 학생과 학생의 자식만 등록 가능.
public static void registerCourse2(Applicant<? extends Student> applicant) {
System.out.println(applicant.kind.getClass().getSimpleName() + "이(가) Course2를 등록함");
}
// 직장인 및 일반인만 등록 가능 -> 직장인과 직장인의 부모만 등록 가능.
public static void registerCourse3(Applicant<? super Worker> applicant) {
System.out.println(applicant.kind.getClass().getSimpleName() + "이(가) Course3을 등록함");
}
}
package ch13.sec05;
public class GenericExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
// 모든 사람이 신청 가능
Course.registerCourse1(new Applicant<Person>(new Person()));
Course.registerCourse1(new Applicant<Worker>(new Worker()));
Course.registerCourse1(new Applicant<Student>(new Student()));
Course.registerCourse1(new Applicant<HighStudent>(new HighStudent()));
Course.registerCourse1(new Applicant<MiddleStudent>(new MiddleStudent()));
System.out.println();
// 학생만 신청 가능
// Course.registerCourse2(new Applicant<Person>(new Person()));
// Course.registerCourse2(new Applicant<Worker>(new Worker()));
Course.registerCourse2(new Applicant<Student>(new Student()));
Course.registerCourse2(new Applicant<HighStudent>(new HighStudent()));
Course.registerCourse2(new Applicant<MiddleStudent>(new MiddleStudent()));
System.out.println();
// 직장인 및 일반인만 신청 가능
Course.registerCourse3(new Applicant<Person>(new Person()));
Course.registerCourse3(new Applicant<Worker>(new Worker()));
// Course.registerCourse1(new Applicant<Student>(new Student()));
// Course.registerCourse1(new Applicant<HighStudent>(new HighStudent()));
// Course.registerCourse1(new Applicant<MiddleStudent>(new MiddleStudent()));
}
}

'자바 > 이것이 자바다' 카테고리의 다른 글
| 15. 컬렉션 자료구조 (0) | 2023.11.18 |
|---|---|
| 14. 멀티 스레드 (1) | 2023.11.17 |
| 12. java.base 모듈 (1) | 2023.11.17 |
| 11. 예외 처리 (0) | 2023.11.15 |
| 10. 라이브러리와 모듈 (0) | 2023.11.15 |