제네릭
제네릭을 사용하는 이유
- Java 5부터 제네릭이 지원이 되었으며 제네릭을 통해 잘못된 타입이 사용될 수 있는 문제를 컴파일 과정에서 제거할 수 있게 되었다
제네릭의 장점
- 컴파일 시 강한 타입 체크를 할 수 있다
- 타입 변환을 제거한다
// Case 비제네릭
List list = new ArrayList();
list.add("hello");
String str = (String)list.get(0); // 타입 변환을 반드시 해야한다
// Case 제네릭
List<String> list = new ArrayList<String>();
list.add("hello");
String str = list.get(0);
- 위에 예제처럼 제네릭으로 String을 지정하는 경우, List의 데이터 타입이 String으로 제한되기 때문에 별도의 타입 변환 작업을 거치지 않아도 된다
제네릭 타입
제네릭 타입은 클래스 또는 인터페이스 이름 뒤에
<>
부호가 붙고, 사이에 타입 파라미터가 위치한다타입 파라미터는 변수명과 동일한 규칙에 따라 작성할 수 있지만, 일반적으로 대문자 알파벳 한 글자로 표현한다
제네릭 타입을 실제 코드에서 사용하려면 타입 파라미터에 구체적인 타입을 지정해야한다
public class 클래스명<T> { ... } public interface 인터페이스명<T> { ... }
제네릭을 사용하는 이유에 대해 예시를 통해 자세히 알아보자
public class Box {
private Object object;
public void set(Object object) { this.object = object; }
public Object get() { return object; }
}
- Box클래스의 필드 타입이 Object인데, Object 타입으로 선언한 이유는 필드에 모든 종류의 객체를 저장하고 싶어서이다
- Object 클래스는 모든 자바 클래스의 최상위 부모 클래스이다
- 따라서 자식 객체는 부모 타입에 대입할 수 있다는 성질 때문에 모든 자바 객체는 Object 타입으로 자동 타입 변환되어 저장된다
- 위 예시에서 set() 메소드를 보면 Object 타입으로 매개값을 받아 모든 객체를 받을 수 있게 했고, 받은 매개값을 Object 필드에 저장시킨다
- get() 메소드는 Object타입으로 리턴한다
- 따라서 get() 메소드를 호출하여 필드에 저장된 원래 타입의 객체를 얻으려면 강제 타입 변환을 해야한다
- Object 타입을 사용하면 모든 종류의 자바 객체를 저장할 수 있다는 장점은 있지만
- 저장할 때 타입 변환이 발생하고, 읽어올 때에도 타입 변환이 발생한다
- 타입 변환이 빈번해지면 전체 프로그램 성능에 좋지 못한 결과를 가져올 수 있다
- 이런 타입변환의 문제를 해결하기 위해
제네릭
을 사용하는 것이다 - 아래에 제네릭을 이용하여 Box 클래스를 수정해 보자
public class Box<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
- 이제 Box 클래스를 사용해보자
Box<String> box1 = new Box<String>();
Box<Integer> box2 = new Box<Integer>();
Box
box = new Box<>(); 와 같이 new연산자 뒤에 붙는 제네릭에서는 Java 7부터 타입 생략이 가능하다
컴파일러가 참조타입의 제네릭을 보고 유추할 수가 있기때문에 생략이 가능해졌다
- 타입 파리미터 T는 String과 Integer로 대체되어 아래처럼 Box 클래스의 내부를 자동으로 재구성한다
// Box<String> box1 = new Box<String>();
public class Box<String> {
private String t;
public void set(String t) { this.t = t; }
public String get() { return t; }
}
// Box<Integer> box2 = new Box<Integer>();
public class Box<Integer> {
private Integer t;
public void set(Integer t) { this.t = t; }
public Integer get() { return t; }
}
멀티 타입 파라미터
- 제네릭 타입은 두 개 이상의 멀티 타입 파라미터를 사용할 수 있는데, 이 경우 각 타입 파라미터를 콤마로 구분한다
public class Product<T, M> {
private T kind;
private M model;
public T getKind() { return this.kind; }
public M getModel() { return this.model; }
public void setKind(T kind) { this.kind = kind; }
public void setModel(M model) { this.model = model; }
}
public class ProductExample {
public static void main(String[] args){
Product<Tv, String> product1 = new Product<Tv, String>();
product1.setKind(new Tv());
product1.setModel("스마트Tv");
Tv tv = product1.getKind();
String tvModel = product1.getModel();
Product<Car, String> product2 = new Product<Car, String>();
product2.setKind(new Car());
product2.setModel("디젤");
Car car = product2.getKind();
String carModel = product2.getModel();
}
}
- 위 예제 역시 Java 7부터는
Product<Tv, String> product1 = new Product<>();
와 같이 생략이 가능하다
제네릭 메소드
제네릭 메소드
란? 매개 타입과 리턴 타입으로 타입 파라미터를 갖는 메소드르 ㄹ말한다public <타입 파라미터, ...> 리턴타입 메소드명(매개변수, ...) { ... }
제네릭 메소드를 호출할 때는
- 구체적인 타입 명시 리턴타입 변수 = <구체적타입> 메소드명(매개값); - 매개값을 보고 컴파일러가 구체적인 타입을 추정 리턴타입 변수 = 메소드명(매개값);
public class Util {
public static <T> Box<T> boxing(T t) {
Box<T> box = new Box<T>();
box.set(t);
return box;
}
}
public class BoxingMethodExample {
public static void main(String[] args) {
Box<Integer> box1 = Util.<Integer>boxing(100);
int intValue = box1.get();
Box<String> box2 = Util.boxing("홍길동");
String strValue = box2.get();
}
}
- 이해한 내용을 글로 풀어서 적어보자면
- <T> 를 통해 리턴타입과 매개변수 타입이 T 라는 정보를 알 수 있다
- Box<Integer> box1 → 리턴타입은 Box<Integer>이고
- <Integer>boxing(100) → 매개변수 타입은 Integer라는 것을 알 수 있다
- Util.boxing("홍길동")을 보면 위와같이 매개변수 타입이 없는데 이것은 컴파일러가 "홍길동"을 보고 타입을 추정하여 수행된다
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;
}
}
제한된 타입 파라미터
- 제한된 타입 파라미터만 선언 되어야하는 이유
- 숫자만 연산하는 제네릭 메소드는 매개값으로 Number 타입 또는 하위 클래스 타입의 인스턴스만 가져야 한다
public <T extends Number> int compare<T t1, T t2> {
double v1 = t1.doubleValue();
double v2 = t2.doubleValue();
return Double.compare(v1, v2);
}
와일드 타입
?
을 와일드카드라고 부른다- 제네릭타입<?>: 모든 클래스나 인터페이스 타입이 올 수 있다
- 제네릭타입<? extends 상위타입>: 상위 타입이나 하위 타입만 올 수 있다
- 제네릭타입<? supter 하위타입>: 하위타입이나 상위 타입이 올 수 있다
public class Cource<T> {
private String name;
private T[] students;
public Cource(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;
}
}
}
}
- Person 하위 클래스로 Worker와 Student가 있고, Student 하위 클래스로 HighStudent가 있다고 가정해보자
- Course<?> : 수강생은 모든 타입이 될 수 있다
- Course<? extends Student> : 수강생은 Student와 HighStudent만 될 수 있다
- Course<? super Worker> : 수강생은 Worker와 Person만 될 수 있다
제네릭 타입의 상속과 구현
pubilc class ChildProduct<T, M> extends Product<T, M> { ... }
public class ChildProduct<T, M, C> extends Product<T, M> { ... }
- 자식 제네릭 타입은 추가적으로 타입 파라미터를 가질 수 있다
Reference
이것이 자바다 - 신용권 지음
'Java' 카테고리의 다른 글
Java char타입의 초기화를 할 때 '' 불가한 이유 (0) | 2022.04.02 |
---|---|
Java GC 처리방법 (0) | 2022.04.01 |
Java 상속 (0) | 2022.01.09 |
Java의 Class (0) | 2022.01.08 |
Java에서 String이 메모리에 생성되는 과정의 이해 (0) | 2022.01.06 |