다음과 같이 Box 클래스를 선언하려고 한다. Box에 넣을 내용물로 content
field를 선언하려고 할 때, 타입을 무엇으로 해야할까??
public class Box {
public ? content;
}
Box는 다양한 내용물로 저장해야 하므로 특정 클래스 타입으로 선언할 수 없다. 그래서 다음과 같이 Object
타입으로 선언한다.
public class Box {
public Object content;
}
Object
타입은 모든 클래스의 최상위 부모 클래스이다. 그렇기 때문에 객체는 부모 타입인 Object
로 자동 타입 변환이 되므로 contetn
field에는 어떤 객체든 대입이 가능하다.
Box box = new Box();
box content = 모든 객체;
문제는 Box
안의 내용물을 얻을 때이다. contetn
는 Object
타입이므로 어떤 객체가 대입되어 있는 지 확실하지 않다. 이때 대입된 내용물의 타입을 안다면 강제 타입 변환을 거쳐 얻을 수 있다.
String content = (String) box.content;
그러나 어떤 내용물이 저장되어 있는 지 모른다면 instanceof
연산자로 조사할 수는 있찌만, 모든 종류의 클래스를 대상으로 조사할 수 없다. 따라서 content
field를 선언하는 것은 좋은 방법이 아니다.
Box를 생성하기 전에 우리는 어떤 내용물을 넣을지 이미 알고 있다. 따라서, Box
를 생성할 때, 들어갈 내용뮬의 타입을 미리 알려주면 Box
는 content에 무엇이 대입되고, 읽을 때 어떤 타입으로 제공할지를 알게 된다. 이것이 제네릭이다.
제네릭(Generic)이란 결정되지 않은 타입을 파라미터로 처리하고, 실제 사용할 때 파라미터를 구체적인 타입으로 대체시키는 기능
다음은 Box
클래스에서 결정되지 않은 contetn
의 타입을 T
라는 타입 파라미터로 정의한 것이다.
public class Box <T> {
public T content;
}
<T>
는 T
가 타입 파라미터임을 뜻하는 기호로, 타입이 필요한 자리에 T
를 사용할 수 있음을 알려준다. 만약 Box
의 content로 String
을 사용하고 싶다면, 다음과 같이 Box
를 생성할 때 타입 파라미터 T
대신 String
으로 대체하면 된다.
Box<String> box = new Box<String>();
box.content = "hello";
String content = box.content; // 강제 타입 변환이 불필요하다.
만약 content
로 int값을 넣고 싶다면 다음과 같이 넣을 수 있다.
Box<Integer> box = new Box<Integer>();
box.content = 100;
int content = box.content;
generic의 T
는 하나의 타입 파라미터가 되는 것이다. 참고로 T
뿐만 아니라 ,N개 이상의 타입 파라미터를 받을 수 있다. 단, 주의할 것은 타입 파라미터를 대체하는 타입은 클래스, 인터페이스만 가능하고 기본 타입은 불가능하다. 가령 Integer
는 되지만 int
는 안된다.
참고로, 변수를 선언할 때의 동일한 타입으로 호출하고 싶다면 생성자 호출 시 생성자에는 타입을 명시하지 않고 <>
만 붙일 수 있다.
Box<String> box = new Box<String>(); // 같은 코드
Box<String> box1 = new Box<>(); // 같은 코드
generic type은 결정되지 않은 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다. generic 타입은 선언부에 <>
부호가 붙고, 그 사이에 타입 파라미터들이 위치한다.
public class 클래스명<A, B, ...> {
...
}
public interface 인터페이스<A, B, ...> {
...
}
아래의 예제는 Product
class를 generic 타입으로 선언한다. kind
와 model
field를 타입 파라미터로 선언하고, Getter
의 매개변수와 Setter
의 리턴 타입 역시 타입 파라미터로 선언한다. 이렇게 타입 파라미터를 사용하는 이유는 Product
에 다양한 종류와 모델 제품을 저장하기 위해서이다.
public class Product<K,M> {
//field
private K kind;
private M model;
//method
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;}
}
TV
와 Car
class를 다음과 같이 작성해보자.
public class TV {
}
public class Car {
}
public class Main {
public static void main(String[] args) {
//K는 TV로 대체, M은 String으로 대체
Product<TV, String> product1 = new Product<>();
// Setter 매개값은 반드시 TV와 String으로 제공
product1.setKind(new TV());
product1.setModel("LG TV");
//Getter 리턴값은 TV와 String이 됨
TV tv = product1.getKind();
String tvModel = product1.getModel();
// 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();
}
}
Product
class의 field로 Car
와 TV
가 잘 들어가는 것을 볼 수 있었다.
다음은, 인터페이스에 제네릭을 적용시켜보도록 하자. Rentable
interface를 만들고, 다양한 대상을 렌트하기 위해 rent()
메서드와 return type을 타입 파라미터로 선언하도록 하자.
public interface Rentable<P> {
P rent();
}
다음은 렌터 대상인 Home
과 Car
를 다음과 같이 만들어보도록 하자.
public class Home {
public void turnOnLight() {
System.out.println("전등을 켭니다.");
}
}
public class Car {
public void run() {
System.out.println("자동차가 달립니다.");
}
}
다음 HomeAgency
와 CarAgency
는 짐과 자동차를 렌트해주는 대리점 클래스로 Rentable
의 타입 파라미터를 Home
과 Car
로 대체해서 구현하는 방법을 보여준다.
public class HomeAgency implements Rentable<Home>{
@Override
public Home rent() {
return new Home();
}
}
Rentable
interface를 implements
시에 generic을 위한 타입 파라미터를 넘겨주어야 한다. 그 값이 바로 Home
인 것이다.
public class CarAgency implements Rentable<Car> {
@Override
public Car rent() {
return new Car();
}
}
다음은 HomeAgency
와 CarAgency
에서 대여한 Home
과 Car
를 이용하는 방법을 알려준다.
public class Main {
public static void main(String[] args) {
HomeAgency homeAgency = new HomeAgency();
Home home = homeAgency.rent();
home.turnOnLight();
CarAgency carAgency = new CarAgency();
Car car = carAgency.rent();
car.run();
}
}
결과는 다음과 같다.
전등을 켭니다.
자동차가 달립니다.
타입 파라미터는 기본적으로 Object
타입으로 간주되므로 Object
타입이 가지고 있는 메서드를 호출할 수 있다. 다음은 Box
의 content를 비교하기 위해 타입 파라미터로 Object
의 equals
메서드를 호출한다.
public class Box<T>{
public T content;
//box의 내용물이 같은 지 비교
public boolean compare(Box<T> other) {
boolean res = content.equals(other.content);
return res;
}
}
public class Main {
public static void main(String[] args) {
Box box1 = new Box();
box1.content = "100";
Box box2 = new Box();
box2.content = "100";
Box box3 = new Box();
box3.content = 100;
boolean res1 = box1.compare(box2);
System.out.println("res1 " + res1); // res1 true
boolean res2 = box1.compare(box3);
System.out.println("res2 " + res2); // res2 false
}
}
generic method는 타입 파라미터를 가지고 있는 메서드를 말한다. 타입 파라미터가 메서드 선언부에 정의된다는 점에서 generic type과 차이가 있다. generic method는 리턴 타입 앞에 <>
기호를 추가하고 타입 파라미터를 정의한 뒤, 리턴 타입과 매개변수 타입에서 사용한다.
public <A, B, ...> 리턴타입 메서드명(매개변수, ...) { ... }
<A, B, ...>
가 바로 타입 파라미터이다.
다음 boxing
메서드는 타입 파라미터로 <T>
를 정의하고, 매개변수 타입과 리턴 타입에서 T를 사용한다. 정확한 리턴 타입은 T를 내용물로 갖는 Box
객체이다.
public <T> Box<T> boxing(T t) { ... }
타입 파라미터 T는 매개값이 어떤 타입이냐에 따라 컴파일 과정에서 구체적인 타입으로 대체한다.
Box<Integer> box1 = boxing(100);
Box<String> box2 = boxing("hello");
이제 실습을 해보자.
public class Box<T>{
public T t;
public T get() {
return t;
}
public void set(T t) {
this.t = t;
}
}
다음은 generic method인 boxing
을 선언하고 호출하는 방법을 보여준다.
public class Main {
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); // 100
Box<String> box2 = boxing("홍길동");
String strValue = box2.get();
System.out.println(strValue); // 홍길동
}
}
경우에 따라서는 타입 파라미터를 구체적인 타입으로 제한할 필요가 있다. 가령 숫자만 가능하면 Number
또는 자식 클래스(Long
, Integer
등)으로 제한할 필요가 있다.
이렇게 특정 타입과 자식 또는 구현 관계에 있는 타입만 대체할 수 있도록, 타입 파라미터를 제한하는 것을 제한된 타입 파라미터라고 한다. 정의는 다음과 같이 한다.
public <T extends 상위타입> 리턴타입 메서드(매개변수, ...) { ... }
싱위타입은 클래스 뿐만 아니라, 인터페이스도 가능하다. 인터페이스라고 implements
를 사용하지는 않는다. 다음은 Number
타입과 자식 클래스 (Byte
, Short
, Integer
, Long
, Double
)만 대체 가능한 타입 파라미터를 정의한 것이다.
public <T extends Number> boolean compare(T t1, T t2) {
double v1 = t1.doubleValue(); // Number 클래스의 method를 쓸 수 있다.
double v2 = t2.doubleValue(); // Number 클래스의 method를 쓸 수 있다.
return (v1 == v2);
}
타입 파라미터를 Number
타입으로 국한시키면서 Object
메서드 뿐만 아니라, Number
가 가지고 있는 메서드를 사용할 수도 있다. 위 코드는 Number
의 doubleValue
메서드를 실행시켜 double
값을 반환시키는 것이다.
아래의 예제는 Number
로 제약을 건 T
타입 매개변수를 받아서, T
타입을 가지는 두 객체의 값을 비교하는 코드이다.
public class Main {
public static <T extends Number> boolean compare(T t1, T t2) {
System.out.println("compare(" + t1.getClass().getSimpleName() + ", " + t2.getClass().getSimpleName() + ")");
double v1 = t1.doubleValue();
double v2 = t2.doubleValue();
return (v1 == v2);
}
public static void main(String[] args) {
// generic method 호출
boolean res1 = compare(10, 20);
System.out.println(res1); // compare(Integer, Integer) false
// generic method 호출
boolean res2 = compare(4.5, 4.5);
System.out.println(res2); // compare(Double, Double) true
// 불가 String res3 = compare("s1", "s2");
}
}
그런데 generic 티입 파라미터를 사용하는데 있어서, 특정 메서드나 필드에서 타입 파라미터를 어떤 것을 써야할 지 모를 때가 있다. 또는 아무거나 써도 상관없을 때가 있다.
public class Applicant<T> {
public T kind;
public Applicant(T kind) {
this.kind = kind;
}
}
위의 예제는 지원자에 대한 정보를 담은 객체엔데, 제네릭 타입 매개변수인 T
로 다양한 지원자 클래스들을 받아낼 수 있다. 그런데 해당 Applicant
를 다른 메서드에서 매개변수를 받는 경우 다음과 같은 상황이 발생할 수 있다.
public class Course {
public static void registerCourse1(Applicant applicant) {
System.out.println(applicant.kind.getClass().getSimpleName() + "이 Course1을 등록");
}
}
위 코드에서 문제가 발생하진 않지만 Applicant
의 type을 지정해주지 않는다. 즉, Applicant
의 타입 매개변수에 대한 입력을 넣어주지 않고 있다는 것이다. 이렇게 작성된 이유는 실제로 registerCourse1
의 경우는 어떤 입력이 오는 지 모르기 때문이다.
따라서, 다음과 같이 작성이 가능하다.
public class Course {
public static <T> void registerCourse1(Applicant<T> applicant) {
System.out.println(applicant.kind.getClass().getSimpleName() + "이 Course1을 등록");
}
}
그러나 이렇게 된다면 generic으로 굳이 선언되지 않아도 되는 method들도 generic 타입 매개변수가 있는 클래스들을 받을 때마다 똑같은 수의 generic을 받을 수 있게 선언해줘야한다는 것이다. 즉, Applicant
의 타입 매개변수가 10개이면 registerCounse1
도 10개의 타입 매개변수를 써주어야 한다는 것이다. 이는 굉장히 번거로운 작업이 아닐 수 없다. 또한, 사실상 registerCourse1
에 있는 제네릭 타입 매개변수는 Applicant
에 넘겨주기 위한 것이지, 본문에 쓰이는 값들이 아니다. 즉, 파라미터로 넘겨주기 위함이지 본문에서 중요하게 쓰이는 것이 아니라는 것이다.
registerCourse1
의 경우 Applicant
가 어떤 타입 매개변수를 받던 간에 상관없다. 그냥 입력으로 받은 타입 매개변수를 넘겨주기만 하면된다.
이러한 상황을 위해서 사용하는 것이 바로 와일드 카드이다. 와일드 카드는 ?
로 작성하여 제네릭의 매개변수에 어떠한 타입이 쓰이든 상관하지 않겠다고 하는 것이다.
public class Course {
public static void registerCourse1(Applicant<?> applicant) {
System.out.println(applicant.kind.getClass().getSimpleName() + "이 Course1을 등록");
}
}
이전처럼 함수 시그니처의 반환 타입 옆에 <T>
가 없다. 이는 제네릭 타입 매개변수를 받지 않겠다는 것이다. 대신에 Applicant
가 어떠한 타입으로 오든지 상관하지 않겠다는 것이다. 어차피 registerCourse1
에서 쓰이는 것은 T
가 무슨 타입이던간에 상관이없기 대문이다.
정리하자면 와일드카드는 타입 매개변수가 어떠한 값이든 상관없을 때 사용한다는 것이다. 이는 타입 매개변수에 특정한 로직이 없이 read만 한다는 것이지, write는 없다는 것이다. 반면에 타입 매개변수로 T
와 같은 값을 쓰는 것은 read 전용이 아니라 write를 하기 위해 쓴다는 것이다.
이렇게 와일드카드를 써주면 해당 method에서는 타입 매개변수를 딱히 어딘가에 쓰지 않겠다는 것을 은연 중에 내포하는 것이다.
제네릭 타입을 매개값이나 리턴 타입으로 사용할 때, 타입 파라미터로 ?
(와일드카드)를 사용할 수 있다. ?
는 범위에 있는 모든 타입으로 대체할 수 있다는 표시이다. 가령 다음과 같은 상속 관계가 있다고 가정해보자
Person
^
|
-----------------------
^ ^
| |
worker student
^
|
-----------------------
^ ^
| |
HighStudent MiddleStudent
어떤 타입이든 가능하도록 매개변수를 선언할 수 있다.
리턴타입 메서드명(제네릭타입<?> 변수) { ... }
타입 파라미터의 대체 타입으로 Student
와 자식 클래스인 HignStudent
와 MiddleStudent
만 가능하도록 매개변수를 다음과 같이 선언할 수 있다.
리턴타입 메서드명(제네릭타입<? extends Student> 변수) { ... }
반대로 Worker
와 부모 클래스인 Person
만 가능하도록 매개변수를 다음과 같이 선언할 수 있다.
리턴타입 메서드명(제네릭타입<? super worker> 변수) { ... }
정리하면 다음과 같다.
?
: 어떤 타입이든 올 수 있다는 것이다.? extends T
: 타입 T와 그 하위 타입만 허용 (읽기 중심)? super T
: 타입 T와 그 상위 타입만 허용 (쓰기 중심)다음의 예제를 보도록 하자.
public class Person {
}
class Worker extends Person {
}
class Student extends Person {
}
class HighStudent extends Student {
}
class MiddleStudent extends Student {
}
Person
클래스는 위의 상속도를 java 클래스로 구현한 것이다.
public class Applicant<T> {
public T kind;
public Applicant(T kind) {
this.kind = kind;
}
}
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을 등록");
}
}
public class Main {
public static void main(String[] args) {
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()));
Course.registerCourse3(new Applicant<Person>(new Person()));
Course.registerCourse3(new Applicant<Worker>(new Worker()));
// 불가 Course.registerCourse3(new Applicant<Student>(new Student()));
// 불가 Course.registerCourse3(new Applicant<HighStudent>(new HighStudent()));
// 불가 Course.registerCourse3(new Applicant<MiddleStudent>(new MiddleStudent()));
}
}
와일드카드인 ?
는 어떤 타입이든 들어올 수 있다. 해당 타입에 대한 제약은 extends
와 super
로 걸 수 있다.