제네릭(Generic)이란 결정되지 않은 타입을 파라미터로 처리하고 실제 사용할 때 파라미터를 구체적인 타입으로 대체시키는 기능을 말한다.
✅ 코드 형태
public class 클래스명<T> { //<T>는 단지 타입 파라미터를 쓸 것을 의미한다.
public T 필드명;
}
예를 들어, Box 클래스에서 결정되지 않은 content(필드)의 타입을 T라는 타입 파라미터로 정의하자.
public class Box<T> {
public T content;
}
Box 클래스는 T가 무엇인지 모르지만, Box 객체가 생성될 시점에 다른 타입으로 대체된다는 것을 알고 있다. 만약 Box의 내용물로 String을 저장하고 싶다면 타입 파라미터를 String으로 지정하면 된다.
Box<String> box = new Box<String>(); //타입 파라미터 String으로 지정
box.content = "안녕하세요";
String content = box.content; //강제 타입 변환 없이 바로 "안녕하세요"를 얻을 수 있다.
※ 주의할 점은 타입 파라미터를 대체하는 타입은 클래스와 인터페이스라는 것이다.
Box<String>
라고 하지 않은 이유는 기본 타입은 타입 파라미터의 대체 타입이 될 수 없기 때문이다.
변수를 선언할 때와 동일한 타입으로 호출하고 싶다면 생성자 호출 시 생성자에는 타입을 명시하지 않고 <>만 붙일 수 있다.
Box<String> box = new Box<String>(); //굳이 이렇게 안 써도 됨
Box<String> box = new Box<>(); //<>만 써도 동일한 타입으로 호출 가능
ex) 제네릭 타입 파라미터 실습
public class Box<T> {
public T content; //타입 파라미터로 T 사용... T는 아무런 의미없이 "타입을 갖는다."의 의미만 갖고 있다.
}
public class GenericExample {
public static void main(String... args) {
//String 타입으로 생성
Box<String> box1 = new Box<>();
box1.content = "안녕하세요";
String str = box1.content;
System.out.println(str);
//String 타입으로 생성
Box<Integer> box2 = new Box<>();
box2.content = 100;
Integer value = box2.content;
System.out.println(value);
}
}
제네릭 타입은 결정되지 않은 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다.
제네릭 타입은 선언부에 <>
부호가 붙고 그 사이에 파라미터에 타입 파라미터들이 위치한다.
✅ 코드 형태
public class 클래스명<A, B, ...> { ... }
public interface 인터페이스명<A, B, ...> { ... }
타입 파라미터는 일반적으로 대문자 알파벳 한글자로 표현한다.
외부에서 제네릭 타입을 사용하려면 타입 파라미터에 구체적인 타입을 지정해야 한다. 만약 지정하지 않을 경우, Object 타입이 암묵적으로 사용된다.
ex) 다양한 종류와 모델 제품을 저장하는 Product 클래스를 제네릭 타입으로 선언해보자.
/* 제네릭 타입 */
public class Product<K, M> {
/* 필드 */
private K kind;
private M model;
/* 메소드; Getter/Setter */
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; }
}
public class Tv {
}
public class Car {
}
public class GenericExample {
public static void main(String... args) {
/* K는 Tv로, M은 String으로*/
Product<Tv, String> product1 = new Product<>();
product1.setKind(new Tv());
product1.setModel("스마트 Tv");
Tv tv = product1.getKind();
String tvModel = product1.getModel();
/* K는 Car로, M은 String으로*/
Product<Car, String> product2 = new Product<>();
product2.setKind(new Car());
product2.setModel("SUV");
Car car = product2.getKind();
String carModel = product2.getModel();
}
}
ex) Rentable
인터페이스를 제네릭 타입으로 선언해보자.
public interface Rentable<P> {
P rent();
}
public class Home {
public void turnOnLight() {
System.out.println("전등을 켭니다.");
}
}
public class Car {
public void run() {
System.out.println("자동차가 달립니다.");
}
}
public class HomeAgency implements Rentable<Home> {
@Override
public Home rent() {
return new Home();
}
}
public class CarAgency implements Rentable<Car>{
@Override
public Car rent() {
return new Car();
}
}
public class GenericExample {
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();
}
}
제네릭 메소드는 타입 파라미터를 가지고 있는 메소드를 말한다.
제네릭 메소드는 리턴 타입 앞에 <>
기호를 추가하고 타입 파라미터를 정의한 뒤, 리턴 타입과 매개변수 타입에서 사용한다.
✅ 코드 형태
public <타입 파라미터, ...> 리턴타입 메소드명(매개변수, ...) { ... }
타입 파라미터 T는 매개값이 어떤 타입이냐에 따라 컴파일 과정에서 구체적인 타입으로 대체된다.
/* T는 Integer로 대체, Box<Intgeer> 가 리턴된다. */
1) Box<Integer> box1 = boxing(100);
/* T는 String으로 대체, Box<String> 가 리턴된다. */
2) Box<String> box2 = boxing("안녕하세요")
ex) 제네릭 메소드 실습
public class Box<T> {
//필드
private T t;
//Getter 메소드
public T get() {
return t;
}
//Setter 메소드
public void set(T t) {
this.t = t;
}
}
public class GenericExample {
//제네릭 메소드
public static <T> Box<T> boxing(T t){
Box<T> box = new Box<T>();
box.set(t);
return box;
}
public static void main(String... args) {
Box<Integer> box1 = boxing(100); //T를 Integer로 대체
int intValue = box1.get();
System.out.println(intValue);
Box<String> box2 = boxing("고길동 아저씨"); //T를 String으로 대체
String strValue = box2.get();
System.out.println(strValue);
}
}
⚠️ 경우에 따라 타입 파라미터를 대체하는 구체적인 타입을 제한할 필요가 있다 !
모든 타입으로 대체할 수 없고, 특정 타입과 자식 or 구현 관계에 있는 타입만 대체할 수 있는 타입 파라미터를 제한된 타입 파라미터(bounded type parameter)라고 한다.
✅ 코드 형태
public <T extends 상위타입> 리턴타입 메소드(매개변수, ...) { ... }
상위 타입은 클래스뿐만 아니라 인터페이스도 가능하다. 인터페이스라고 해서 implements
를 사용하지는 않는다.
ex) 제한된 타입 파라미터 실습
public class GenericExample {
//제한된 타입 파라미터를 갖는 제네릭 메서드
public static <T extends Number> boolean compare(T t1, T t2) { //타입 파라미터 T를 대체할 타입을 Number로 제한
System.out.println("compare(" + t1.getClass().getSimpleName() + "," +
t2.getClass().getSimpleName() + ")");
//Number 메소드 사용
double v1 = t1.doubleValue(); //Number타입의 doubleValue() 메소드 호출
double v2 = t2.doubleValue(); //Number타입의 doubleValue() 메소드 호출
return (v1 == v2);
}
public static void main(String... args) {
//제네릭 메소드 호출
boolean result1 = compare(10, 20);
System.out.println(result1);
System.out.println();
//제네릭 메소드 호출
boolean result2 = compare(4.5, 4.5);
System.out.println(result2);
}
}
제네릭 타입을 매개값이나 리턴 타입으로 사용할 때 타입 파라미터로
?
(와일드 카드)를 사용할 수 있다.
?
는 범위에 있는 모든 타입으로 대체할 수 있다는 표시이다.
리턴타입 메소드명(제네릭타입<? extends 부모> 변수) { ... }
리턴타입 메소드명(제네릭타입<? super 특정자식> 변수) { ... }
리턴타입 메소드명(제네릭타입<?> 변수) { ... }