출처 : 신용권의 이것이 자바다.
제네릭이란 자바 5부터 새로 추가된 기능으로 타입을 파라미터화 해서 컴파일 시에 구체적인 타입이 결정되도록 하는 것.
- 컬렉션, 람다식(함수적 인터페이스), 스트림, NIO에서 널리 사용된다.
- API 도큐먼트에 많이 표현되기에 이해하지 못하면 API를 읽기 힘들다.
// 비제네릭
List list = new ArrayList();
list.add("hello");
String str = (String) list.get(0); // 타입 변환 O
// 제네릭
List<String> list = new ArrayList<String>();
list.add("hello");
String str = list.get(0); // 타입 변환 X
타입을 파라미터로 가지는 클래스와 인터페이스를 말한다.
<>
부호가 붙고 <>
사이에는 타입 파라미터가 위치한다.public class 클래스명<T> {...}
public interface 인터페이스명<T> {...}
public class Box {
private Object object; // Object는 모든 자바 클래스의 최상위 부모 클래스임
public void set(Object obeject) { this.objecct = object; }
public Object get() { return object; }
}
Box box = new Box();
box.set("hello"); // String을 Object에 자동 타입 변환
String str = (String) box.get(); // Object를 String으로 강제 타입 변환
Object 타입을 사용하므로서 빈번한 타입 변환이 발생한다(성능 저하를 일으킴)
두 개 이상의 타입 파라미터를 사용해서 선언할 수 있다.
public class Product<T, M> {
private T kind;
private M model;
public T getKind() {
return kind;
}
public M getModel() {
return model;
}
public void setKind(T kind) {
this.kind = kind;
}
public void setModel(M model) {
this.model = model;
}
}
변수를 선언할 때 구체적으로 타입을 지정하면 생성자를 호출할 때는 구체적인 타입을 명시하지 않아도 된다. 컴파일러가 알아서 해석해준다.
List<String> list = new ArrayList<>(); // 선언부에 String을 명시하여 초기화부에는 생략할 수 있음.
매개변수 타입과 리턴 타입으로 타입 파라미터를 갖는 메소드를 말한다.
<>
기호를 추가하고 타입 파라미터를 기술한다.public <타입 파라미터> 리턴타입 메소드명(매개변수, ...) { ... }
제네릭 메소드는 두 가지 방법으로 호출할 수 있다. 명시적으로 구체적 타입을 지정해주는 방법과 매개값을 보고 구체적 타입을추정하는 방법이 있는데 일반적으로 후자의 방법을 더 많이 사용한다.
Box<Integer> box = <Integer>boxing(100); // 타입 파라미터를 명시적으로 Integer로 지정
Box<Integer> box = boxing(100); // 매개변수의 타입을 보고 Integer로 추정
public class Util {
public static <T> Box<T> boxing(T t) {
Box<T> box = new Box<>();
box.setT(t);
return box;
}
}
public class Box<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
public class BoxMethodExample {
public static void main(String[] args) {
Box<Integer> box1 = Util.boxing(100);
int intValue = box1.getT();
Box<String> box2 = Util.boxing("스트링링");
String strValue = box2.getT();
}
}
boolean타입의 정적 제네릭 메소드 compare()를 정의하여 이를 활용한다.
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 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 CompareMethodExample {
public static void main(String[] args) {
Pair<Integer, String> pair1 = new Pair<>(1, "사과"); // 제네릭 타입 구체적 명시
Pair<Integer, String> pair2 = new Pair<>(1, "사과");
boolean result = Util.compare(pair1, pair2); // true
if (result) {
System.out.println("논리적 동등 객체");
} else {
System.out.println("논리적 동등 객체 아님");
}
Pair<String, String> pair3 = new Pair<>("user1", "홍길동"); // 제네릭 타입 구체적 명시
Pair<String, String> pair4 = new Pair<>("user2", "홍길동");
boolean result2 = Util.compare(pair3, pair4);
if (result2) {
System.out.println("논리적 동등 객체");
} else {
System.out.println("논리적 동등 객체 아님");
}
}
}
타입 파라미터에 저장되는 구체적 타입을 제한할 필요가 종종 있다. 숫자를 연산하는 제네릭 메소드에서는 Number타입과 그 하위 타입의 인스턴스만 가져야 하듯 말이다. 이것이 제한된 타입 파라미터가 필요한 이유이다.
제한된 타입 파라미터를 선언하려면 타입 파라미터 뒤에 extends 키워드를 붙이고 상위 타입을 명시하면 된다. 여기서 extends는 상속이 아니라 종류의 의미로 사용된다.
public <T extends 상위 타입> 리턴타입 메소드(매개변수, ...) { ... }
{}
안에서 타입 파라미터의 변수로 사용 가능한 것은 상위 타입의 멤버(필드, 메소드)로 제한된다.doubleValue() 메소드는 Number 클래스에 정의되어 있는 메소드로 숫자를 double 타입으로 변환한다. 크면 1, 같으면 0, 작으면 -1을 리턴한다.
public class Util {
public static <T extends Number> int compare(T t1, T t2) {
double v1 = t1.doubleValue();
double v2 = t2.doubleValue();
return Double.compare(v1, v2);
}
}
public class BoundedTypeParameterExample {
public static void main(String[] args) {
// String str = Util.compare("1", "2"); // String은 Number 타입이 아님
int result1 = Util.compare(10, 20); // int -> Integer autoBoxing
System.out.println(result1);
int result2 = Util.compare(4.5, 3.0); // double -> Double autoBoxing
System.out.println(result2);
}
}
일반적으로 코드에서
?
를 와일드카드라고 부른다. 제네릭 타입을 매개변수나 리턴타입으로 사용할 때 타입 파라미터를 제한할 목적으로 사용된다.
제네릭타입<?>
: Unbounded Wildcards (제한 없음)
타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있다.
제네릭타입<? extends 상위타입>
: Upper Bounded Wildcards
타입 파라미터를 대치하는 구체적인 타입으로 상위 타입이나 하위 타입만 올 수 있다.
제네릭타입<? super 하위타입>
: Lower Bounded Wildcards(하위 클래스)
타입 파라미터를 대치하는 구체적인 타입으로 하위 타입이나 상위 타입이 올 수 있다.
제네릭 타입 Course는 과정 클래스로 과정 이름과 수강생을 저장할 수 있는 배열을 가지고 있다. 타입 파라미터 T가 적용된 곳은 수강생 타입 부분이다.
public class Course<T> {
private String name;
private T[] students;
public Course(String name, int capacity) {
this.name = name;
students = (T[]) (new Object[capacity]);
}
public String getName() {
return name;
}
public T[] getStudents() {
return students;
}
public void add(T t) {
for (int i = 0; i < students.length; i++) {
if (students[i] == null) {
students[i] = t;
break;
}
}
}
}
![스크린샷 2022-02-14 17.29.44](/Users/mac/Library/Application Support/typora-user-images/스크린샷 2022-02-14 17.29.44.png)
Course<?>
수강생은 모든 타입(Person, Worker, Student, HighStudent)이 될 수 있다.
Course<? extends Student>
수강생은 Student와 HighStudent만 될 수 있다.
Course<? super Worker>
수강생은 Worker와 Person만 될 수 있다.
제네릭 타입도 다른 타입과 마찬가지로 부모 클래스가 될 수 있다.
public class ChildProduct<T, M> extends Product<T,M> { ... }
public class ChildProduct<T, M, C> extends Product<T,M> { ... }
package generic;
public class ChildProduct<T, M, C> extends Product {
private C company;
public C getCompany() {
return company;
}
public void setCompany(C company) {
this.company = company;
}
}
Product를 상속 하는 ChildProduct에는 윗 부분에서 선언만 Product 클래스에 타입 파라미터 C가 추가 되었다.