Generic이란
Java 5 부터 추가된 문법으로, 잘못된 타입이 사용될 수 있는 문제를 컴파일러 과정에서 제거할 수 있게 한다.
컬렉션, 람다식, 스트림, NIO에서 널리 사용된다.
API Documents에는 제네릭 표현이 많기 때문에 제네릭을 정확히 이해해야만 API Docs를 정확히 이해할 수 있다.
클래스와, 인터페이스 그리고 메소드를 정의할 때 타입(Type)을 파라미터(Parameter)로 사용할 수 있도록 한다.
타입파라미터는 코드 작성시 구체적인 타입으로 대체되어 다양한 코드를 생성하도록 해준다.
컴파일 시 강한 타입체크
자바 컴파일러는 코드에서 잘못 사용된 타입 때문에 발생하는 문제점을 제거하기 위해 제너릭 코드에 대해 강한 타입체크를 한다.
실행시 타입 에러가 나지 않도록 컴파일 시 미리 타입을 강하게 체크하여 에러를 사전에 방지한다.
타입 변환(Casting) 제거
비 제네릭 코드는 불필요한 타입 변환을 하므로 프로그램 성능에 악영향을 미친다.
List list = new ArrayList();
list.add("hello");
String str = list.get(0);
List에 문자열 요소를 저장했지만, 요소를 찾아올 때는 반드시 String 타입으로 변환해야 한다.
List list = new ArrayList();
list.add("hello");
String str = (String)list.get(0);
다음과 같이 제네릭 코드로 수정하면
List<String> list = new ArrayList<String>(); // new 인스턴스<생략 가능> ex) new ArrayList<>();
list.add("hello");
String str = list.get(0);
제네릭 타입(Class<T>, Interface<T>)
제너릭 타입이란 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다.
클래스 또는 인터페이스 이름 뒤에 "<>" 부호가 붙고, 사이에 타입 파라미터가 위치한다.
public class 클래스명 <T> {...}
public interface 인터페이스명 <T> {...}
/* 타입파라미터 이름은 T 이다. */
타입 파라미터는 일반적으로 대문자 알파벳 한글자로 표현한다.
제네릭 타입을 실제 코드에 사용하려면 타입 파라미터에 구체적인 타입을 지정해야한다.
public class Box {
private Object object;
public Object get() {
return object;
}
public void set(Object object) {
this.object = object;
}
}
Box 클래스의 필드 타입을 Ojbect 타입으로 선언한 이유는 필드에 모든 종류의 객체를 저장하기 위함이다.
Object 클래스는 모든 자바의 최상위 조상(부모) 클래스이므로 모든 자바 객체는 Object타입으로 자동 타입변환되어 저장된다.
Object object = Java의 모든 Object;
set() 메소드를 통해 저장된 객체를 get() 메소드를 통해 Object 타입으로 반환받을 수 있다.
이때, 다음과 같은 강제 타입변환을 필요로 한다.
Box box = new Box();
box.set("hello"); // String -> Object 자동 타입 변환
String str = (String)box.get(); // Object -> String 강제 타입 변환
이와같이 Object 타입을 사용하면 모든 종류의 자바 객체를 저장할 수 있다는 장점이 있는 반면, 저장시에 자동으로 타입변환이 발생하고, 읽어올 때에는 강제로 타입을 변환을 필요로 한다.
타입 변환이 빈번해지면 전체 프로그램 성능에 좋지 못한 결과를 가져올 수 있다.
class Box <T> {
private T t;
public T get() {
return t;
}
public void set(T t) {
this.t = t;
}
}
타입 파라미터 T 사용으로 Object 타입을 모두 T로 대체했다.
T는 Box클래스로 객체를 생성할 때 구체적인 타입으로 변경된다.
Box<String> box = new Box<>();
타입 파라미터 T는 String 타입으로 변경되어 Box 클래스의 내부는 다음과 같이 재구성된다.
public class Box<String> {
private String t;
public void set(String t) {
this.t = t;
}
public String get() {
return t;
}
}
필드 타입이 T에서 String으로 변경되었고 set() 메소드도 String 타입만 매개값으로 받을수 있게 변경되었다.
get() 메소드 역시 String 타입으로 리턴하도록 변경되었다.
이는 저장할 때와 읽어올 때 타입 변환이 전혀 발생되지 않게 된다.
Box<String> box = new Box<>();
box.set("hello"); //<String> 으로 인해 자동 형변환 X
String str = box.get(); //<String> 으로 인해 강제 형변환 X
int에 대한 객체타입인 Integer 으로 Box객체를 생성해보자.
Box<Integer> box = new Box<>();
타입 파라미터 T는 Integer 타입으로 변경되어 Box 클래스는 내부적으로 다음과 같이 자동으로 재구성된다.
public class Box<Integer> {
private Integer t;
public void set(Integer t) {
this.t = t;
}
public Integer get() {
return t;
}
}
다음 코드를 보면 저장할 때와 읽어올 때 Integer Wrapper클래스의 AutoBoxing, AutoUnboxing만 발생할 뿐 전혀 타입변환이 발생하지 않는다.
Box<Integer> box = new Box<>();
box.set(6);// int(6) -> Integer(6)자동 Boxing
int value = box.get();// Integer(6) -> int(6) 자동 Unboxing
이와 같이 제네릭은 클래스를 설계할 때 구체적인 타입을 명시하지 않고 타입 파라미터로 대체했다가 실제 클래스가 사용될 때 구체적인 타입으로 지정함으로써 타입 변환을 최소화 시킨다.
멀티 타입 파라미터(Class<K,V, ...> , Interface<K,V, ...>)
제네릭 타입은 두개 이상의 멀티 타입 파라미터를 사용할 수 있다.
각 타입 파라미터는 콤마로 구분한다.
Product<T, M> 제네릭 타입 클래스를 정의한다.
class Product <T, M> {
private T kind; //종류
private M model; //모델
public T getKind() {return kind;}
public void setKind(T kind) {this.kind = kind;}
public M getModel() {return model;}
public void setModel(M model) {this.model = model;}
}
public class Example {
static class Tv {}
static class Car {}
public static void main(String[] args) {
/* Logic - 1 */
}
}
main메소드 Logic - 1
Product<Tv, String> 객체와 Product<Car, String> 객체를 생성한 뒤 set을 통해 값을 주입하고 get을 통해 값을 추출한다.
/* Logic - 1 */
Product<Tv, String> product1 = new Product<>();
product1.setKind(new Tv());
product1.setModel("스마트 TV");
Tv tv = product1.getKind();
String tvModel = product1.getModel();
Product<Car, String> product2 = new Product<>();
product2.setKind(new Car());
product2.setModel("과학5호기");
Car car = product2.getKind();
String carModel = product2.getModel();
객체 생성과 동시에 Product 클래스는 각각 다음과 같이 재구성 된다.
class Product <Car, String> {
private Car kind; //종류
private String model; //모델
public Car getKind() {return kind;}
public void setKind(Car kind) {this.kind = kind;}
public String getModel() {return model;}
public void setModel(String model) {this.model = model;}
}
class Product <Tv, String> {
private Tv kind; //종류
private String model; //모델
public Tv getKind() {return kind;}
public void setKind(Tv kind) {this.kind = kind;}
public String getModel() {return model;}
public void setModel(String model) {this.model = model;}
}
제너릭 메소드(<T, R> R method (T t))
제너릭 메소드는 매개 타입과 리턴 타입으로 타입 파라미터를 갖는 메소드를 말한다.
선언 방법은 리턴타입 앞에 <> 기호를 추가하고 타입 파라미터를 기술한 다음, 리턴타입과 매개타입으로 타입파라미터를 사용한다.
public<타입파라미터, ...> 리턴타입 메소드명(매개변수, ...) {}
public<T> Box<T> boxing(T t){}
지정한 타입 파라미터는 매개변수의 타입과 리턴타입의 타입에서 사용할 수 있다.
제네릭 메소드는 두 가지 방식으로 호출한다.
코드에서 타입 파라미터의 구체적인 타입을 명시적으로 지정한다.
만약 구체적인 타입을 생략한다면, 컴파일러가 매개값의 타입을 보고 구체적인 타입을 추정하게 된다.
Box<Integer> box = <Integer>boxing(100); // 타입 파라미터를 명시적으로 Integer로 지정
Box<Integer> box = boxing(100); // 타입 파라미터를 Integer로 추정
아래와 같이 예제 코드를 구성해본다.
Util 클래스에 정적 제네릭 메소드로 Boxing() 을 정의하고 Example 클래스에서 호출한다.
Util.java
public class Util {
public static <T> Box<T> boxing(T t){
Box<T> box = new Box();
box.set(t);
return box;
}
}
Example.java
public class Example {
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();
}
}
box1 객체는 Integer를 명시적으로 타입파라미터로 지정하였지만, box2 객체는 타입 파라미터를 String으로 추정하게된다.
다음 예제는 Util클래스에 정적 제네릭 메소드로 compare()를 정의하고 Example클래스에서 호출하는 예제이다.
값이 동일한지 비교하고 boolean값을 리턴하도록 로직을 작성한다.
[Pair.java]
public class Pair<K, V> {
private K key;
private V value;
// 생성자
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
// setter getter
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;}
}
[Util.java]
public class Util {
public static <K, V> boolean comapre(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;
}
}
[Example.java]
public class Example {
public static void main(String[] args) {
Pair<Integer, String> p1 = new Pair(1, "사과");
Pair<Integer, String> p2 = new Pair(1, "사과");
boolean result1 = Util.<Integer, String>comapre(p1, p2); // 구체적인 타입 명시적 지정
if(result1) {
System.out.println("논리적으로 동등한 객체이다.");
} else {
System.out.println("논리적으로 동등하지 않는 객체이다.");
}
Pair<String, Integer> p3 = new Pair("hong", "홍길동");
Pair<String, Integer> p4 = new Pair("hong", "홍길동");
boolean result2 = Util.comapre(p3, p4); // 구체적인 타입 추정
if(result2) {
System.out.println("논리적으로 동등한 객체이다.");
} else {
System.out.println("논리적으로 동등하지 않는 객체이다.");
}
}
}
Util.java에서 compare() 메소드의 타입 파라미터는 K와 V로 선언되었는데, 리턴타입인 제네릭타입 Pair가 K와 V를 가지고 있기 때문이다.
이와같이 제네릭 메소드의 타입파라미터는 리턴타입이 제네릭 타입인경우 제네릭타입의 타입으로 지정할 수 있다.
[번외]
현업에서 다음과 같은 형태의 코드를 본 적이 있다.
@Service
public class Service {
public <T, E> List<E> listJSON(final Map<String, Object> param) throws Exception {
return mapper.list(param);
}
}
해당 메소드를 호출하는 컨트롤러 코드이다.
@Controller
public class exampleController {
@Autowired private Service service;
@ResponseBody
@RequestMapping(value = "listJSON")
public List<Map<String, Object>> listJSON(final @RequestParam Map<String, Object> param) throws Exception {
return service.listJSON(param);
}
}
리턴타입인 List의 제네릭타입은 Map<String, Object> 이다.
Service 클래스의 listJson() 메소드에 파라미터로 Map을 넘겼기 때문에 다음과 같이 다시 작성할 수 있다.
@Service
public class Service {
public <T, E> List<E> listJSON(final T param) throws Exception {
return mapper.list(param);
}
}
여기서 E는 리턴타입의 Map<String, Object>를 가르키기 때문에 멀티 타입 파라미터 E를 단일 T 1개로 공유할 수 있다.
@Service
public class Service {
public <T> List<T> listJSON(final T param) throws Exception {
return mapper.list(param);
}
}
그렇다면 왜 굳이 T, E 2개의 멀티타입 파라미터를 사용했는가? 라는 의문을 가질 수 있는데, 그저 구분을 짓기 위함이 아닐까 생각이 든다.
'JAVA > BASIC' 카테고리의 다른 글
자바 Object 클래스 객체복제 (얇은복제) (0) | 2023.04.10 |
---|---|
JAVA Generic 제네릭 (2) / 제한된 타입 파라미터, 와일드카드 타입, 제네릭 타입의 상속과 구현 (0) | 2023.03.12 |
람다식과 인터페이스 익명구현 객체 (0) | 2022.10.27 |
Static 정적 멤버(변수,메서드 등) (0) | 2021.05.09 |
클래스간 상속관계에서 Final로 인한 메서드오버라이딩 제한 (0) | 2021.05.06 |