타입에 대한 정의를 컴파일 시점으로 미루게 한다.
선언 위치
동시에 여러 타입을 선언할 수 있다.
타입 설명 (큰 의미가 있는 것은 아니고, 임의의 참조형 타입이다. 수학에서의 x, y 라고 보면 된다)
<T>
: Type의 첫 글자를 따서 사용<E>
: Element의 첫 글자를 따서 사용<K, V>
: Key와 Value의 첫 글자를 따서 사용 (타입 변수가 다수일 경우, 콤마(,)로 나열)용어
타입 변수
/타입 매개변수
: <> 안에 있는 E 같은 타입 문자들대입된 타입
: 타입 변수 E 대신, 지정된 타입 Tv 같은 것들이 들어간 타입제네릭 타입 호출
: 타입 변수(타입 매개변수)에 타입을 지정하는 것매개변수화된 타입
: 그렇게 지정된 타입class Box<T> {...}
Box(T)
: 제네릭 클래스Box
: 원시 타입타입의 안정성 제공
형변환을 생략할 수 있어서, 코드가 간결함
Box<String>
과 Box<Integer>
Box<String>
과 Box<Integer>
는 Box<T>
에 들어간 타입 매개변수만 다르게 해서 호출한 것일 뿐
이 둘이 별개의 클래스인 것은 아님
(add(3,5) 와 add(2,4)와 같음)
class Box<T> {
T[] itemArr; // T 타입의 배열을 위한 참조변수
T[] toArray() {
T[] tmpArr = new T[itemArr.length]; // 에러 : 제네릭 배열은 생성 X
...
return tmpArr;
}
...
}
제네릭 배열 타입의 참조변수를 선언하는 것은 O
제네릭 배열 생성하는 것은 X
Box<T>
클래스가 컴파일되는 시점에는 T가 어떤 타입이 될지 알 수가 없음class FruitBox<T> {
...
static <T> void sort(List<T> list, Comparator<? super T> c) {
...
}
}
// 제네릭스를 사용하지 않을 경우
ArrayList tvList = new ArrayList(); // 참조변수와 생성자에 타입 변수 E 대신, 실제 타입 Tv를 지정함
tvList.add(new Tv());
Tv t = (Tv)tvList.get(0) // 형변환 필요 O
// 제네릭스를 사용할 경우
ArrayList <Tv> tvList = new ArrayList<Tv>(); // 참조변수와 생성자에 타입 변수 E 대신, 실제 타입 Tv를 지정함
tvList.add(new Tv());
Tv t = tvList.get(0) // 형변환 필요 X
// 올바른 경우
ArrayList <Tv> tvList = new ArrayList<Tv>(); // TV 로 일치
// 틀린 경우
ArrayList <Product> tvList = new ArrayList<Tv>(); // 에러 : Product 타입 과 Tv의 타입은 불일치
ArrayList <Product> tvList = new ArrayList<Product>();
list.add(new Product);
list.add(new Tv()); // 자식 객체
list.add(new Audio()); // 자식 객체
// But 주의! ArrayList에 저장된 객체를 꺼낼 경우, 이땐 형변환이 필요
Product p = list.get(0); // Product 객체의 경우, 형변환 필요 X
Tv t = (Tv)list.get(1); // TV 객체 (Product의 자식 객체)의 경우, 형변환 필요 O
Iterator<E>
import java.util.*;
class Ex {
public static void main(String args[]) {
ArrayList<Student> list = new ArrayList<Student>();
list.add(new Student("자바왕", 1, 1));
list.add(new Student("자바짱", 1, 2));
list.add(new Student("홍길동", 2, 1));
Iterator<Student> it = list.iterator();
while (it.hasNext()) {
Student s = it.next(); // 형변환 필요 X
System.out.println(s.name);
}
}
}
제네릭스를 사용하지 않는다면
참고: 자바 컬렉션 (Java Collection, 컬렉션 프레임워크) - 3. Enumeration, Iterator, ListIterator
HashMap<E>
HashMap<Stirng, Student> map = new HashMap<String, Student>(); // 생성
map.put("자바왕", new Student("자바왕", 1, 1, 100, 100, 100)); // 데이터 저장
키 타입 Stirng, 값 타입 Student 으로 데이터를 저장한다
값을 꺼내올 때, 형변환 필요 X
// 자식만 타입으로 지정
class FruitBox<T extends Fruit> { // Fruit 의 자식만 타입으로 지정 가능
ArrayList<T> list = new ArrayList<T>();
}
// 자식을 타입으로 지정 + 인터페이스도 구현
class FruitBox<T extends Fruit & Eatable>
extends 를 사용하면
// 타입이 일치하지 않을 경우
ArrayList<Product> list = new ArrayList<Tv>(); // 에러
// 1. ? 로 객체를 생성할 경우 (부모: Product, 자식: Tv, Audio)
ArrayList<? extends Product> list = new ArrayList<Tv>();
ArrayList<? extends Product> list = new ArrayList<Audio>();
// 2. ? 를 메서드의 매개변수에 사용할 경우
static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp = "";
for (Fruit f : box.getList()) {
tmp = f + " ";
}
return new Juice(tmp);
}
1번
<? extends T>
<? super T>
<?>
class FruitBox<T> {
...
static <T> void sort(List<T> list, Comparator<? super T> c) {
...
}
}
// 제네릭 메서드
static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {
String tmp = "";
for (Fruit f : box.getList()) {
tmp = f + " ";
}
return new Juice(tmp);
}
// 1. 제네릭 메서드를 호출할 경우
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> fruitBox = new FruitBox<Fruit>();
...
System.out.println(Juicer.<Fruit>makeJuice(fruitBox); // 부모: Fruit
System.out.println(Juicer.<Apple>makeJuice(appleBox); // 자식: Apple
// 2. 대입된 타입을 생략할 수 있는 경우 (대부분)
System.out.println(Juicer.makeJuice(fruitBox);
System.out.println(Juicer.makeJuice(appleBox);
// 3. 대입된 타입을 생략할 수 없는 경우
System.out.println(<Fruit>makeJuice(fruitBox); // 에러 : 클래스 이름 생략 X
System.out.println(this.<Fruit>makeJuice(appleBox);
System.out.println(Juicer.<Fruit>makeJuice(appleBox);
// 4. 제네릭 타입이 다른 다수의 객체를 매개변수로 지정할 경우
System.out.println(Juicer.makeJuice(new FruitBox<Fruit>())); // 부모: Fruit
System.out.println(Juicer.makeJuice(new FruitBox<Apple>();); // 자식: Apple
2번
3번
4번
1. 클래스 Box가 다음과 같이 정의되어 있을 때, 다음 중 오류가 발생하는 문장은? 경고가 발생하는 문장은?
class Box<T> { //지네릭 타입 T를 선언
T item;
void setItem(T item) {
this.item = item;
}
T getItem() {
return item;
}
}
/* 잘못된 문장
1. Box<Object>타입의 참조변수에 Object타입 저장불가 (타입 불일치)
Box<Object> b = (Object)new Box<String>( );
2. 대입된 타입이 String이므로, setItem(T item)의 매개변수 역시, String타입만 허용
그러나, setItem의 매개변수는 new Object( )
new Box<String>( ).setItem(new Object( ));
*/
/* 올바른 문장
1. 대입된 타입인 String과 일치하는 타입을 매개변수("ABC")로 지정했기 때문
new Box<String>( ).setItem("ABC");
*/
2. 지네릭 메서드 makeJuice( )가 아래와 같이 정의되어 있을 때, 이 메서드를 올바르게 호출한 문장을 모두 고르시오. (Apple과 Grape는 Fruit의 자손이라고 가정하자.)
class Juicer {
static <T extends Fruit> String makeJuice(FruitBox<T> box) {
String tmp = "";
for (Fruit f : box.getList())
tmp += f + " ";
return tmp;
}
}
/* 잘못된 문장
1. 지네릭 메서드에 대입된 타입이 Apple이므로, 이 메서드의 매개변수는 'FruitBox<Apple>'타입이 된다.
Juicer.<Apple>makeJuice(new FruitBox<Fruit>( ));
2. Grape가 Fruit의 자손일지라도, 타입이 다르기 때문
Juicer.<Fruit>makeJuice(new FruitBox<Grape>( ));
3. 지네릭 메서드의 타입 호출이 생략되지 않았다면,
원래 형태는 Juicer.<Object>makeJuice(new FruitBox<Object>( ));
그러나, <T extends Fruit>로 타입 제한이 걸려있기 때문에, 타입 T는 Fruit의 자식이어야 한다.
Object는 Fruit의 자식이 아니므로 에러 발생
Juicer.makeJuice(new FruitBox<Object>( ));
*/
/* 올바른 문장
1. 지네릭 메서드에 대입된 타입은 Fruit, 매개변수도 'FruitBox<Fruit>'타입
Juicer.<Fruit>makeJuice(new FruitBox<Fruit>( ));
2. 지네릭 메서드의 타입 호출이 생략된 형태
원래 형태는 Juicer.<Apple>makeJuice(new FruitBox<Apple>( ));
Juicer.makeJuice(new FruitBox<Apple>( ));
*/
3. 올바르지 않은 문장을 모두 고르시오.
class Box<T extends Fruit> { // 지네릭 타입 T를 선언
T item;
void setItem(T item) {
this.item = item;
}
T getItem() {
return item;
}
}
/* 잘못된 문장
1. Object는 Fruit의 자손이 아니기 때문
Box<?> b = new Box<Object>( );
2. 타입 불일치
Box<Object> b = new Box<Fruit>( );
3. new연산자는 타입이 명확해야하므로, 와일드카드와 같이 사용불가
Box<? extends Object> b = new Box<? extends Fruit>( );
*/
/* 올바른 문장
1. Box<?>는 Box<? extends Object>를 줄여쓴 것
그래도, new Box<>( ) 대신 new Box( )를 사용하는 것을 권장
Box<?> b = new Box( );
*/
4. 아래의 메서드는 두 개의 ArrayList를 매개변수로 받아서, 하나의 새로운 ArrayList로 병합하는 메서드이다. 이를 지네릭 메서드로 변경하시오.
// 수정 전
public static ArrayList<? extends Product> merge (ArrayList<? extends Product> list, ArrayList<? extends Product> list2) {
ArrayList<? extends Product> newList = new ArrayList<>(list);
newList.addAll(list2);
return newList;
}
// 수정 후 : <? extends Product> -> <T>
public static <T extends Product> ArrayList<T> merge (ArrayList<T> list, ArrayList<T> list2) {
ArrayList<T> newList = new ArrayList<>(list);
newList.addAll(list2);
return newList;
}
통합 ResponseDto로 성공과 실패의 결과값을 담아 Response로 넘겨주고자 한다.
@Getter
@AllArgsConstructor
public class ResponseDto<T> {
private boolean success;
private T data;
private Error error;
//성공 시
public static <T> ResponseDto<T> success(T data) {
return new ResponseDto<>(true, data, null);
}
//실패 시
public static <T> ResponseDto<T> fail(String code, String message) {
return new ResponseDto<>(false, null, new Error(code, message));
}
@Getter
@AllArgsConstructor
static class Error {
private String code;
private String message;
}
}
성공 시 -> true와 data로 어떠한 데이터도 담을 수 있게 필드를 선언
실패 시 -> false와 직접 작성한 code와 message를 담은 error를 담을 수 있게 필드를 선언
빌더 패턴을 사용한다면, builder 앞에 제네릭 타입이 붙는다.
@Getter
@Builder
@AllArgsConstructor
public class ResponseDto<T> {
private String status;
private String message;
private T data;
}
ResponseDto.<UserInfoResponseDto>builder()
.status("success")
.message("사용자 정보 요청 기능 수행")
.data(userService.getUserInfo(userDetails))
.build()
참고: 디자인 패턴 - (2) 생성 패턴 - 빌더 패턴
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ResponseDto<T> {
int status;
T data;
}
int status를 통해, HttpStatus를 알려줌
data를 통해, 메시지 혹은 상태값을 알려줌
@ControllerAdvice
@RestController
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)
private ResponseDto<String> handleArgumentException(Exception e) {
return new ResponseDto<String>(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage()); // INTERNAL_SERVER_ERROR = 500 값임
}
@RestController
public class UserApiController {
@Autowired
private UserService userService;
// Json 데이터를 받으려면 @RequestBody로 받아야함
// 회원가입
@PostMapping("/auth/joinProc")
public ResponseDto<Integer> save(@RequestBody User user) {
userService.회원가입(user);
return new ResponseDto<Integer>(HttpStatus.OK.value(), 1); // 자바 오브젝트를 JSON으로 변환하여 전송
}
}
@RestController
public class UserApiController {
@PostMapping("/api/user")
public ResponseDto<Integer> save(@RequestBody User user) { //save메소드에 대해 반환타입은 ResponseDto<Integer>
return new ResponseDto<Integer>(HttpStatus.OK, 1);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ResponseDto<T> {
HttpStatus status;
T data;
}
다음과 같은 API를 만들어야 한다고 가정할 때,
제네릭을 활용해 노가다를 하지 않고 다음과 같이 편하게 구현할 수 있습니다. (공통부분 활용)
@GetMapping(value = "/stock/info")
public Response info() {
return new Response(200, "OK", new ResponseStockInfo("Apple Inc","AAPL","NASDAQ", 167.30));
}
@GetMapping(value = "/stock/list")
public Response list() {
List<ResponseStockList> stockLists = new ArrayList<>();
stockLists.add(new ResponseStockList("Apple Inc", "AAPL"));
stockLists.add(new ResponseStockList("Microsoft Corporation", "MSFT"));
return new Response(200, "OK", stockLists);
}
json의 data 부분은 T(제네릭 타입)을 이용하여 여러 타입을 지정할 수 있습니다.
//==Response DTO==//
@Data
@AllArgsConstructor
static class Response<T> {
private Integer code;
private String msg;
private T data;
}
//==Response DTO==//
@Data
@AllArgsConstructor
static class ResponseStockInfo {
private String companyName;
private String ticker;
private String market;
private Double price;
}
//==Response DTO==//
@Data
@AllArgsConstructor
static class ResponseStockList {
private String companyName;
private String ticker;
}
멀티 타입 파라미터 사용 시, 콤마로 구분한다.(타입은 두개 이상의 멀티 타입 파라미터를 사용할수 있음)
class ExMultiTypeGeneric<K, V> implements Map.Entry<K,V>{
private K key;
private V value;
@Override
public K getKey() {
return this.key;
}
@Override
public V getValue() {
return this.value;
}
@Override
public V setValue(V value) {
this.value = value;
return value;
}
}
// 제네릭 클래스
class ClassName<K, V> {
private K first; // K 타입(제네릭)
private V second; // V 타입(제네릭)
void set(K first, V second) {
this.first = first;
this.second = second;
}
K getFirst() {
return first;
}
V getSecond() {
return second;
}
}
// 메인 클래스
class Main {
public static void main(String[] args) {
ClassName<String, Integer> a = new ClassName<String, Integer>();
a.set("10", 10);
System.out.println(" fisrt data : " + a.getFirst());
// 반환된 변수의 타입 출력
System.out.println(" K Type : " + a.getFirst().getClass().getName());
System.out.println(" second data : " + a.getSecond());
// 반환된 변수의 타입 출력
System.out.println(" V Type : " + a.getSecond().getClass().getName());
}
}