제네릭 (Generic, 제네릭스)

박영준·2023년 1월 3일
1

Java

목록 보기
35/112

1.정의

  • 타입에 대한 정의를 컴파일 시점으로 미루게 한다.

    • 컴파일 시에 객체의 타입을 체크하므로, 객체 타입의 안정성↑ 형변환의 번거로움↓
  • 선언 위치

    • 클래스
    • 메소드
  • 동시에 여러 타입을 선언할 수 있다.

  • 타입 설명 (큰 의미가 있는 것은 아니고, 임의의 참조형 타입이다. 수학에서의 x, y 라고 보면 된다)

    • <T> : Type의 첫 글자를 따서 사용
    • <E> : Element의 첫 글자를 따서 사용
    • <K, V> : Key와 Value의 첫 글자를 따서 사용 (타입 변수가 다수일 경우, 콤마(,)로 나열)
  • 용어

    • 타입 변수/타입 매개변수 : <> 안에 있는 E 같은 타입 문자들
    • 대입된 타입 : 타입 변수 E 대신, 지정된 타입 Tv 같은 것들이 들어간 타입
    • 제네릭 타입 호출 : 타입 변수(타입 매개변수)에 타입을 지정하는 것
    • 매개변수화된 타입 : 그렇게 지정된 타입
    • class Box<T> {...}
      • Box(T) : 제네릭 클래스
      • Box : 원시 타입

2. 장점

  1. 타입의 안정성 제공

    • 의도치 않는 타입의 객체를 저장하는 것을 방지
    • 저장된 객체를 꺼내올 때, 원래와 다른 타입으로 형변환되어서 발생하는 오류를 줄임
  2. 형변환을 생략할 수 있어서, 코드가 간결함

3. 주의점

1) Box<String>Box<Integer>

Box<String>Box<Integer>Box<T>에 들어간 타입 매개변수만 다르게 해서 호출한 것일 뿐
이 둘이 별개의 클래스인 것은 아님

(add(3,5) 와 add(2,4)와 같음)

2) 제네릭 배열 생성

class Box<T> {
	T[] itemArr;			// T 타입의 배열을 위한 참조변수
    
    T[] toArray() {
    	T[] tmpArr = new T[itemArr.length];		// 에러 : 제네릭 배열은 생성 X
        ...
        return tmpArr;
    }
    
    ...
}
  • 제네릭 배열 타입의 참조변수를 선언하는 것은 O

  • 제네릭 배열 생성하는 것은 X

    • new 연산자때문
      • Box<T> 클래스가 컴파일되는 시점에는 T가 어떤 타입이 될지 알 수가 없음

3) 서로 다른 T 를 지칭

class FruitBox<T> {
	...
    
	static <T> void sort(List<T> list, Comparator<? super T> c) {
    	...
	}
}
  • 제네릭 클래스의 T 와 제네릭 메서드 sort()에 선언된 타입 매개변수 T 는 서로 다르다

4. 사용법

예시 1 : 실제 타입 지정

// 제네릭스를 사용하지 않을 경우
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

예시 2 : 참조변수와 생성자의 제네릭 타입

// 올바른 경우
ArrayList <Tv> tvList = new ArrayList<Tv>();		// TV 로 일치

// 틀린 경우
ArrayList <Product> tvList = new ArrayList<Tv>();		// 에러 : Product 타입 과 Tv의 타입은 불일치

예시 3 : 부모-자식

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
  • 부모 클래스 Product 와 자식 클래스 TV, Audio가 있을 때
    • ArrayList 로 Product 제네릭 타입 생성 후, Tv/Audio 객체 저장 가능

예시 4 : 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);
        }
    }
}

예시 5 : HashMap<E>

HashMap<Stirng, Student> map = new HashMap<String, Student>();		// 생성
map.put("자바왕", new Student("자바왕", 1, 1, 100, 100, 100));		// 데이터 저장

예시 6 : extends 로 타입 종류 제한하기

// 자식만 타입으로 지정
class FruitBox<T extends Fruit> {				// Fruit 의 자식만 타입으로 지정 가능
	ArrayList<T> list = new ArrayList<T>();
}

// 자식을 타입으로 지정 + 인터페이스도 구현
class FruitBox<T extends Fruit & Eatable>
  • extends 를 사용하면

    • 특정 타입의 자식들만 대입하도록 제한 가능
    • 동시에 제네릭에서 인터페이스를 구현할 때도 사용 (& 기호를 사용)
  • 참고: 인터페이스 (interface)

예시 7 : ? (와일드 카드)로 다형성 적용하기

// 타입이 일치하지 않을 경우
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>

      • 와일드 카드의 상한 제한
      • T 와 그 자식들만 가능
    • <? super T>

      • 와일드 카드의 하한 제한
      • T 와 그 부모들만 가능
    • <?>

      • 제한 X
      • 모든 타입 가능
      • < ? extends Object > 와 동일

예시 8 : 제네릭 메서드의 선언 위치

class FruitBox<T> {
	...
    
	static <T> void sort(List<T> list, Comparator<? super T> c) {
    	...
	}
}
  • 제네릭 메서드 + 반환 타입
    • 반환 타입 앞에 위치한다

예시 9 : 제네릭 메서드 호출하기

// 제네릭 메서드
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번

    • 같은 클래스 내에 멤버 간에는 참조변수/클래스이름(this./클래스이름.) 생략 가능 O
  • 3번

    • 대입된 타입을 생략할 수 없는 경우에는 참조변수/클래스이름 생략 X
  • 4번

    • Juicer
      • 클래스 이름
      • 생략 불가

연습문제

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;
}

1)

통합 ResponseDto로 성공과 실패의 결과값을 담아 Response로 넘겨주고자 한다.

ResponseDto

@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를 담을 수 있게 필드를 선언

2)

빌더 패턴을 사용한다면, 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) 생성 패턴 - 빌더 패턴

3)

ResponseDto

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ResponseDto<T> {
    int status;
    T data;
}

int status를 통해, HttpStatus를 알려줌
data를 통해, 메시지 혹은 상태값을 알려줌

GlobalExceptionHandler

@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 값임
    }

Controller

@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으로 변환하여 전송
    }
}

4)

UserApiController

@RestController
public class UserApiController {

	@PostMapping("/api/user")
	public ResponseDto<Integer> save(@RequestBody User user) {		//save메소드에 대해 반환타입은 ResponseDto<Integer>
		
		return new ResponseDto<Integer>(HttpStatus.OK, 1);
	}
}

ResponseDto

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ResponseDto<T> {
	HttpStatus status;
	T data;
}

5)

다음과 같은 API를 만들어야 한다고 가정할 때,

data 안에 들어가는 데이터가 여러 경우일 경우

제네릭을 활용해 노가다를 하지 않고 다음과 같이 편하게 구현할 수 있습니다. (공통부분 활용)

@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);
}

ResponseDto

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;
}

6)

멀티 타입 파라미터 사용 시, 콤마로 구분한다.(타입은 두개 이상의 멀티 타입 파라미터를 사용할수 있음)

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());
	}
}
profile
개발자로 거듭나기!

0개의 댓글