제네릭이란?
클래스의 인터페이스, 메소드를 정의할 때, 타입(type)을 파라미터로 사용할 수 있도록 해준다.
public T genericMethod(T genericTypeObject)
제네릭을 사용하는 이유
컴파일 시 강한 타입 체크가 가능하다.
컴파일 시에 미리 타입을 강하게 체크하여 에러를 사전에 방지할 수 있다.
타입 변환(casting)을 제거한다.
제네릭 이전
List list = new ArrayList();
list.add("hello");
String str = (String) list.get(0); // 타입 변환이 필요하다.
제네릭 이후
List<String> list = new ArrayList<String>();
list.add("hello");
String str = list.get(0); // 타입 변환이 필요없다.
제네릭 타입(class<T> interface<T>)
타입을 파라미터로 가지는 클래스와 인터페이스를 생성할 수 있다.
public class 클래스명<T> { ... }
public interface 인터페이스명<T> { ... }
타입 파라미터의 네이밍은 변수명과 동일한 규칙에 따라 작성할 수 있다. 단, 관례적으로 영문 대문자 한글자로 작성한다.
모든 종류의 객체를 저장하면서 타입 변환이 일어나지 않는 마법을 부릴 수 있다.
제네릭 예제
Box 클래스 작성
제네릭 타입 파라미터를 받는 클래스이다.
public class Box<T> {
public T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
public class Main {
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.setT("문자열");
String stringBoxT = stringBox.getT();
System.out.println("stringBoxT = " + stringBoxT); // 저장된 문자열 출력
Box<Integer> integerBox = new Box<>();
integerBox.setT(100);
Integer integerBoxT = integerBox.getT();
System.out.println("integerBoxT = " + integerBoxT); // 저장된 숫자 출력
}
}
위와 같이 Box<T>
클래스의 제네릭 타입 파라미터를 이용하여 어떤 값이든 삽입하고 반환할 수 있게 되었다.
타입에 구애받지 않는 비슷한 로직이 필요한 경우에 기존엔 Object
타입으로 받아준 뒤에 캐스팅을 하였는데, 제네릭이 나온 이후로는 제네릭을 이용하면 캐스팅도 필요 없고 컴파일 시 강한 타입 체크도 하기 때문에 실수를 최소한으로 할 수 있다.
제네릭 멀티 타입 파라미터(class, interface)
제네릭 타입은 두 개 이상의 멀티 타입 파라미터를 사용할 수 있고, 이 경우 콤마로 구분한다.
멀티 타입 파라미터 예제 코드
public class Product <M, P>{
private M model;
private P price;
public M getModel() {
return model;
}
public void setModel(M model) {
this.model = model;
}
public P getPrice() {
return price;
}
public void setPrice(P price) {
this.price = price;
}
}
위와 같은 형식으로 멀티 타입 파라미터를 받는 Product
클래스를 만들 수 있다.
Product<String, Integer> product1 = new Product<>();
product1.setModel("K-11");
product1.setPrice(10000);
위는 M
타입으로는 String
을 주고 P
타입으로는 Integer
를 준 예제이다.
public class Laptop {
String cpu;
String ram;
String vga;
public String getCpu() {
return cpu;
}
public void setCpu(String cpu) {
this.cpu = cpu;
}
public String getRam() {
return ram;
}
public void setRam(String ram) {
this.ram = ram;
}
public String getVga() {
return vga;
}
public void setVga(String vga) {
this.vga = vga;
}
@Override
public String toString() {
return "Laptop{" +
"cpu='" + cpu + '\'' +
", ram='" + ram + '\'' +
", vga='" + vga + '\'' +
'}';
}
}
public class DiscountablePrice {
double discountRate;
double price;
public double getDiscountRate() {
return discountRate;
}
public void setDiscountRate(double discountRate) {
this.discountRate = discountRate;
}
public double getPrice() {
return price - (price * discountRate);
}
public void setPrice(double price) {
this.price = price;
}
@Override
public String toString() {
return "DiscountablePrice{" +
"discountRate=" + discountRate +
", price=" + price +
", discountedPrice=" + this.getPrice() +
'}';
}
}
위와 같이 사용자 정의 타입 Laptop
과 DiscountPrice
등을 만들어서 해당 제네릭 타입에 대입할 수도 있다.
public class Main {
public static void main(String[] args) {
Product<Laptop, DiscountablePrice> product2 = new Product<>();
Laptop laptopModel = new Laptop();
laptopModel.setCpu("i7");
laptopModel.setRam("16gb");
laptopModel.setVga("Geforce 1070");
product2.setModel(laptopModel);
DiscountablePrice discountablePrice = new DiscountablePrice();
discountablePrice.setPrice(10000);
discountablePrice.setDiscountRate(0.2);
product2.setPrice(discountablePrice);
System.out.println(product2.getModel() + " + " + product2.getPrice());
}
}
제네릭 메소드( R method(T t))
제네릭 메소드는 파라미터와 리턴 타입으로 제네릭 타입을 갖는 메소드를 말한다.
제네릭 메소드 선언법은 리턴 타입 앞에 <>
기호를 추가하고 타입 파라미터를 기술한 다음, 리턴 타입과 파라미터 타입으로 제네릭 타입을 사용하면 된다.
예제 코드 1 (형식)
public static <P, R> R genericTypeMethod(P param, R paramToReturn) {
System.out.println("param = " + param);
System.out.println("paramToReturn = " + paramToReturn);
return paramToReturn;
}
위와 같은 형식으로 사용할 수 있다.
public static void main(String[] args) {
ParamType param = new ParamType();
param.setParamString("파라미터");
ReturnType paramToReturn = new ReturnType();
paramToReturn.setReturnString("리턴");
genericTypeMethod(param, paramToReturn);
}
파라미터와 리턴 값을 위한 타입을 만들어서 임의로 작성해보았다.
예제 코드 2 (Pair 클래스 응용)
public class Pair <K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
}
public class Util {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
boolean keyCompare = p1.getKey().equals(p2.getKey());
boolean valueCompare = p1.getValue().equals(p2.getValue());
return keyCompare && valueCompare;
}
}
public class Main {
public static void main(String[] args) {
Pair<String, String> p1 = new Pair<>("1", "홍길동");
Pair<String, String> p2 = new Pair<>("1", "홍길동");
boolean compare1 = Util.compare(p1, p2);
if(compare1) {
System.out.println("key와 value가 모두 일치합니다.");
} else {
System.out.println("key와 value가 일치하지 않습니다.");
}
Pair<Integer, String> p3 = new Pair<>(100, "user1");
Pair<Integer, String> p4 = new Pair<>(100, "user2");
boolean compare2 = Util.compare(p3, p4);
if (compare2) {
System.out.println("key와 value가 모두 일치합니다.");
} else {
System.out.println("key와 value가 일치하지 않습니다.");
}
}
}
위와 같이, 제네릭 타입을 받는 key
와 value
를 갖는 Pair
클래스를 만들고, Util
클래스 메소드의 .compare()
로 논리적 동치를 비교하는 예제를 작성해보았다.
Object
객체가 갖는 공통 메소드는 모든 객체의 타입에 무관하게 공통으로 제공되기 때문에, 타입에 무관한 연산을 수행할 수 있는게 핵심이다.
제한된 타입 파라미터()
종종 구체적인 타입을 제한할 필요가 있을 때 사용한다.
이를테면 숫자 연산을 할 때는 Number
를 갖는 하위 타입을 받아야 한다.
extends
키워드를 이용하지만 인터페이스도 가능하다.
public class NumberUtil {
public static <T extends Number> double add(T num1, T num2) {
return num1.doubleValue() + num2.doubleValue();
}
}
public class Main {
public static void main(String[] args) {
int a = 10;
double b = 20.10;
double result = NumberUtil.add(a, b);
System.out.println("result = " + result); // 결과 30.1
}
}
위는 Number
의 하위 타입 값을 받아서 double
형태로 변환한 뒤에 반환하는 메소드의 예제이다.
와일드카드 타입(<?>, <? extends ...>, <? super ...>)
코드의 ?
를 일반적으로 와일드카드 타입이라 한다. 제네릭 타입을 파라미터 타입으로 사용하거나 리턴 타입으로 사용할 때, 구체적 타입 대신에 와일드 카드를 다음과 같이 세가지 형태로 사용할 수 있다.
제네릭타입<?>
: Unbounded Wildcards (제한 없음)
타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있다.
제네릭타입<? extends 상위타입>
: Upper Bounded Wildcards (상위 클래스 제한)
extends
뒤에 기재한 상위 타입의 하위 타입만 올 수 있다.
제네릭타입<? super 하위타입>
: Lower Bounded Wildcards (하위 클래스 제한)
super
뒤에 기재한 하위 타입의 상위 타입만 올 수 있다.
위의 와일드 카드 타입이 왜 나왔는지 알려면
위의 현상을 생각해보면 된다.
Object
는 모든 것의 상위에 있는 자바 최상위 클래스이지만, ArrayList<Object>
는 ArrayList<String>
를 포용할 수 없다. 왜냐하면 제네릭 타입에 대해서는 상속 관계 등이 전혀 상관없기 때문이다.
그래서 말 그대로 제네릭 타입에 어떠한 타입이 들어갈지 모를 때 ?
라고 정의해 놓으면 아무런 타입이나 들어갈 수 있는 것이다.
? extends A
와 같은 형식으로 타입을 지정해놓으면 A
를 상속한 타입만 하위 타입만 들어갈 수 있게 된다.
? super B
와 같은 형식으로 타입을 지정해 놓으면 B
가 상속 받는 타입을 포함하여 그 위의 상위 타입만 올 수 있다.
제네릭 타입의 상속과 구현
제네릭 타입도 다른 타입처럼 부모 클래스가 될 수 있다.
public class ChildProduct<M, P, C> extends Product<M, P> {
private C company;
public C getCompany() {
return company;
}
public void setCompany(C company) {
this.company = company;
}
}
이전에 생성했던 Product<M, P>
클래스에서 제네릭 타입의 회사 정보가 필요하다면, 위와 같이 C
타입을 추가해서 company
라는 멤버를 받으면 된다.