제네릭
Java 5부터 제네릭(Generic) 타입이 새로 추가 되었다.
제네릭 타입을 이용해서 잘못된 타입이 사용될 수 있는 문제를 "컴파일 과정"에서 제거하는 것이 가능해졌다.
제네릭은 컬랙션, 람다식, 스트림 등 널리 사용되므로 확실히 이해해 두어야 한다.
특히 API 문서를 보면 제네릭 표현이 많기 때문에 이를 이해하지 못하면 문서를 이해하는데 어려움이 생길 수 있다.
제네릭은 클래스와 인터페이스, 메소드를 정의할 때 타입을 파라미터로 사용할 수 있도록 한다.
즉 데이터의 타입을 일반화(generalize)하여 컴파일 시 type check로 런타임 시 안정성을 보장한다.
제네릭의 이점
[1] 컴파일 시 강한 타입 체크가 가능함
자바 컴파일러에서 잘못 사용된 타입의 문제점을 제거하기 위해 제네릭 코드에 대해 강한 타입 체크를 실시함.
따라서 런타임 시 에러 발생의 문제를 방지하고, 컴파일 시 에러를 체크하여 에러 발생을 사전에 방지하는데 도움을 준다.
[2] 타입 변환(casting)을 제거함
비제네릭 코드는 불필요한 타입 변환을 하기 때문에 프로그램 성능에 악영향을 미친다.
다음 코드를 보면 List에 문자열 요소를 저장했지만, 요소를 찾아올 때는 반드시 String 타입 변환을 해야 한다.
List list = new ArrayList();
list.add("hello");
String str = (String)list.get(0);
// String 변수에 저장하기 위해 String 타입으로
// ArrayList를 형변환 해야 한다.
하지만 제네릭 코드를 사용한다면 타입 변환을 할 필요가 없기 때문에 프로그램 성능이 향상된다.
아래 코드는 List에 저장되는 요소를 String 타입으로 국한하기 때문에 타입 변환을 할 필요가 없어 프로그램 성능이 향상된다.
제네릭 타입
제네릭 타입은 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다.
제네릭 타입은 클래스 또는 인터페이스 이름 뒤에 "<>" 부호가 붙고, 사이에 타입 파라미터가 위치한다.
public class 클래스명<T> {...}
public interface 인터페이스명<T> {...}
타입 파라미터는 변수명과 동일한 규칙에 따라 작성할 수 있지만, 일반적으로 대문자 알파벳 한 글자료 표현한다.
제네릭 타입을 실제 코드에서 사용하려면 타입 파라미터에 구체적인 타입을 지정해야 한다.
제네릭 타입을 설정하지 않을 때의 경우 : 빈번한 타입 변환
" 모든 객체를 담을 수 있는 클래스를 만들자 " 라는 해결책으로 쉽게 클래스 내부에 최상위 객체인 Object 를 이용해 변수를 생성하여, 모든 객체에 대한 변수를 담아서 이를 get/set한다고 가정하자 !
이렇게 만들어진 클래스 변수를 생성해서 String 배열을 Set(저장)한다면 " String 타입을 Object 타입으로 자동 타입 변환 " 해서 저장할 것이고, Get(출력)한다면 "Object타입을 String타입으로 강제 타입 변환"해서 값을 얻을 것이다.
이 경우, Get/Set시 모두 타입 변환이 발생하고, 이러한 타입 변환이 빈번해지면 프로그램 성능에 좋지 못한 결과를 도출할 수 있다.
제네릭 타입 설정 : 해결 방법
public class BoX<T> {
private T t;
public T get() {return t; }
pbulic void set(T t) ( this.T = t; }
}
위와 같이 제네릭을 사용해 타입 파라미터 T를 사용해서 Object 타입을 모두 T로 대체해보자
T는 Box 클래스로 객체를 생성할 때 구체적인 타입으로 변경된다.
그 예시를 아래에서 설명하면 다음과 같다.
String 변수를 사용하는 객체 | Integer 변수를 사용하는 객체 |
Box<String> box = new Box<String>(); | Box<Integer> box = new Box<Integer>(); |
public class Box<String> { public String t; public void set(String t) { this.t = t; } public String get() { retunr t; } |
public class Box<Integer> { public Integer t; public void set(Integer t) { this.t = t; } public Integer get() { retunr t; } |
위와 같이 객체 생성 시 타입을 지정하고, 실제 클래스가 사용될 때도 구체적인 타입을 지정함으로써 타입 변환을 최소화 시킬 수 있다.
이러한 점을 통해서 제네릭의 장점으로 확인했던,
- 런타임 시 강한 타입 체크 가능
- 클래스 사용/생성 시 타입을 지정함으로써 타입 변환을 최소화함
멀티 타입 파라미터
놀랍게도 제네릭 타입은 2개 이상의 멀티 타입 파라미터를 지정할 수 도 있다.
이 경우 각 타입 파라미터를 콤마로 구분한다.
코드 예제를 보면 한번에 이해할 수 있을 것이다 !!
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; }
}
위의 코드에서는 각 kind와 model이라는 멀티 타입 파라미터를 선언하고 있다.
이 경우 2개의 타입을 객체의 인자로 넣어서 객체를 생성할 수 있을 것이다.
그 예시도 아래와 같다
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(); // product1에 정의된 TV 객체 반환
Product<Car, String> product1 = new Product<Car, String>();
product1.setKind(new Car());
product1.setModel("전기차");
Car car = product1.getKind(); // product2에 정의된 Car 객체 반환
String carModel = product1.getModel(); //Car객체의 String 타입의 model 변수 반환
제네릭 메소드
제네릭 메소드는 매개 타입과 리턴 타입으로 타입 파라미터를 갖는 메소드를 의미한다.
리턴 타입 앞에 < > 를 기술하고, 타입 파라미터를 입력한뒤 , 리턴 타입과 매개 타입으로 타입 파라미터를 사용한다.
아래 예시는 다음과 같다
타입 파라미터 : <T>. 리턴 타입: Box<t> 메소드명: boxing 매개변수: (T t)
public <타입파라미터, > 리턴타입 메소드_명(매개변수...) {}
public <T> Bot<T> boxing(T t) { .. }
제네릭 메서드는 두가지 방식으로 호출하는 것이 가능하다.
1 -명시적으로 구체적 타입을 지정- | 2 - 매개값을 보고 구체적 타입을 추정- |
리턴타입 변수 <구체적타입> 메소드 명(매개값); |
리턴타입 변수 = 메소드명(매개값) |
Box<Integer> box = <Integer>boxing(100) 타입 파라미터를 명시적으로 지정함 |
Box<Integer> box = boxing(100) 타입 파라미터를 매개값을 보고 추정 |
제네릭 메서드의 예제를 살펴보면서 정리해보자
public calss 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();
}
}
제한된 타입 파라미터
위의 내용에서 보면 미리 타입을 지정할 필요가 없이 어떤 타입이든 생성시에 지정하면 될 것 같다.
하지만 실제로 타입 파라미터에 지정되는 구체적인 타입을 제한할 필요가 때마다 발생한다.
예를 들면, Number 타입 또는 하위 클래스 타입의 인스턴스만 가져야하는 제네릭 메서드에 String 값이 오지 않도록 제한할 필요가 있다.
이럴 경우 사용하는 것이 제한된 타입 파라미터 (bounded type parameter)이다.
제한된 타입 파라미터를 선언하기 위해서는 타입 파라미터 뒤에 extends 키워드를 붙이고 상위 타입을 명시하면 된다.
이 때 상위 타입은 클래스 또는 인터페이스가 들어올 수 있는데 모두 extneds 키워드로 명시한다.
public < T extends 상위타입 > 리턴타입 메소드명(매개변수 ..) { .. } |
타입 파라미터에 지정되는 구체적인 타입은 상위타입이거나 상위 타입의 하위 또는 구현 클래스만 가능하다.
<중요> 이 대 주의할점은 메소드 body {} 내에서 타입 파라미터 변수로 사용 가능한 것은 상위 타입의 멤버 (필드,메소드)로 제한된다.
즉, 하위 타입의 메소드는 사용할 수 없다.
다음의 예시를 살펴보자
//### Util 클래스 ###
public <T extends Number> int compare(T t1, T t2) {
double v1 = t1.doubleValue();
double v2 = t2.dobbleValue();
return Dobule.compare(v1, v2);
//### 제네릭 메서드 호출 클래스 ###
public class BoundedTypeParameter {
public static main(String[] args) {
int res1 = Util.compare(10, 20);
System.out.println(res1);
// int -> Integer로 자동 boxing
int res2 = Util.compare(4.5, 3);
System.out.println(res2);
// double -> Double로 자동 boxing
String str = Util.compare("a", "b"); (x)
// String은 Number의 상위 타입의 멤버가 아님
제네릭 타입의 와일드 카드
제네릭 타입을 매개값이나 리턴 타입으로 사용할 때 구체적인 타입 대신 와일드 카드를 다음과 같이 3가지 형태로 사용할 수 있다.
제네릭 타입<?> - Unbounded WildCards (제한없음) |
타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있음 |
제네릭 타입<? extends 상위타입> - Upper Bounded WildCards ( 상위 클래스 제한) |
타입 파라미터를 대치하는 구체적인 타입으로 하위 타입이 올 수 있음 |
제네릭 타입 <? super 상위타입> - Lower Bounded WildCards (하위 클래스 제한) |
타입 파라미터를 대치하는 구체적인 타입으로 상위 타입이 올 수 있음 |
아래와 같을 때 각 ? 와일드 카드의 예시를 살펴보면
- Course<?> : 수강생은 모든 타입이 가능함
- Course<? extends Student> : 수강생은 Student와 HighStudent만 가능
- Course<? super Worker> : 수강생은 Worker와 Person만 가능
+
제네릭 타입의 상속
제네릭 타입은 다른 타입과 마찬가지로 부모 클래스로써 상속이 가능하다.
아래 코드 예시로 제네릭 타입의 Product를 상속받은 ChildProduct를 정의하면 다음과 같다.
public class ChildProduct<T,M> extneds Product<T,M> {...}
public class ParentProduct<T,M,C> extneds Product<T,M> {...}
'Tech Stack > JAVA' 카테고리의 다른 글
컬렉션 프레임 워크 (0) | 2022.02.07 |
---|