Java

14. 제네릭

문정훈 2022. 3. 30. 21:23
더보기

1. 제네릭 클래스 만들기

2. 제네릭 인터페이스 만들기

3. 멀티 타입 파라미터

4. 제네릭 메소드 

5. 제네릭 메소드와 클래스에서 제한된 타입 파라미터

6. 와일드카드 타입

1. 제네릭 클래스 만들기

클래스와 인터페이스의 제네릭 선언 방법은 이름 옆에 <>붙혀주면 된다. 구체적 타입은 개발자가 개발코드를 작성할 때 이루어진다.

제네릭 클래스 객체를 만들 때 타입옆에 항상<>이 붙어있다고 생각
여기서 자바7 이후부터는 클래스 객체 생성할 경우 생성자 뒤에는<>이렇게만 써도 됨

 

public class BoxExam {
	public static void main(String ar[]) {
    	Box<String> box = new Box<String>();
        
        box.set("홍길동");
        String str = box.get();
        
        Box<Ingeger> box2 = new Box<Integer>();
        box2.set(10);
        int value = box2.get();
    }
}

class Box<T> {
	private T t;
    public void set(T t) {
    	this.t = t;
    }
    
    public T get() {
    	return t;
    }
}

위 예시는 Box라는 제네릭 클래스를 선언하여 제네릭 클래스를 사용하는 간단한 예제이다. 


 

 

2. 제네릭 인터페이스 만들기

public class MainClass {
	public static void main(String ar[]) {
    	Person<String, Integer> person = new Kim<>();
    }
}

interface Person<T, M> {
	void method(T t, M m);
}

class Kim<T, M> implements Person<T, M> {
    @Override
    public void method(T t, M m) {
    
    }
}

위 예시는 구현 메소드의 내용은 없지만 두 개의 제네릭 파라미터를 가진 제네릭 인터페이스를 선언하고 구현 클래스를 어떻게 작성하는지 큰 틀을 작성한 코드이다. 

제네릭 클래스 상속시 하위 클래스는 상속하는 클래스의 제네릭 타입을 모두 가져야함과 동일하게 구현 클래스는 인터페이스에서 매소드의 매개변수에 T M등 제네릭 타입이 존재하므로 구현하는 클래스의 매소드에서 재정의 할 때 T M의 제네릭 타입을 사용하기 위해 구현 클래스도 제네릭 클래스로 작성해야한다. 


 

 

 

3. 멀티 타입 파라미터 

class ClassA<T, M ,...> {};
interface InterfaceA <T, M, ...> {};

멀티 타입 파라미터란 제네릭 클래스와 제네릭 인터페이스를 만들 때 하나의 제네릭 타입만 사용하지 않고 두 개 이상의 제네릭 타입을 사용하는 클래스와 인터페이스를 말한다. 

 

Product<TV, String> product=new Product<TV, String>(): 
이렇게 호출해야 하지만 자바7 이후 부터는 간결하게 


Product<TV, String> product=new Product<>(): 
이렇게 호출이 가능하다.


 

4. 제네릭 메소드 

제네릭 메소드란 매개 타입 또는 리턴 타입, 메소드 안에서타입 파라미터를 가지는 메소드를 말한다. 
예를 들어 Box<T> func(T t); 이 메소드를 보면 리턴 타입과 매개변수 모두 파라미터를 가진다. 
여기서 T값을 결정해 주는 것은 리턴타입 앞에 <T>(타입피라미터)를 추가해준다. 따라서 제네릭 메소드 선언 예시는 

1. <T>Box<T> func(T t);
2. <T,M>Box<M> func(T t);
3. <T> Boolean func(T t);

1번과 3번 같은 경우 제네릭 메소드는 두 가지 방식으로 호출할 수 있다. 
1번 호출은 <String>func(4); 이렇게 선언하면 T값이 String 이란걸 구체적으로 명시해 리턴 값의 T와 매개변수 값의 T를 String으로 치환한다. 1번 3번에 경우 구체적 타입을 지정하지 않고 func(4); 이렇게 매개변수 값만 주게 되면 컴파일러가 매개값의 타입으 보고 구체적 타입 즉 T값을 추정하기 때문에 매개변수를 통해 추정하여 자동으로 리턴 타입의 T까지 같은 T이므로 String으로 치환한다.

하지만 2번에 경우는 구체적 타입을 주지 않으면 매개변수로 추정할 수 있는 값은 T 뿐이어서 리턴 값의 파라미터인 M값은 추정할 수 가 없다. 따라서 이 경우는 구체적 명시를 제대로 해주어야 한다. 두 개다 매개변수로 지정해주면 컴파일러는 추정을 하지만 그게 아닌 이상 구체적으로 주어야한다. 

 

※ 정리
● 선언방법: (public static) <T, …> void func(T t, …);

 

 경우1) 제네릭 타입이 하나인 경우: 호출 시 func(”Hello”); 이렇게 호출하면 T가 String임을 알아서 인지함 

 

 경우2) 제네릭 타입이 여러 개 인 경우: <String, Integet, …>func(”Hello”, 10, …); 메소드에 각각의 제네릭 타입에 모두 타입을 주면 컴파일러가 추정가능하다. 즉 <>를 생략할 수 있다. 하지만 그게 아니라면 하나하나 제네릭 타입을 모두 지정해 주어야한다.

또한 매개변수가 없는 경우는 매소드앞에<>로 타입을 선언해야함 추정할 값이 없기 때문에 이 역시 메소들를 호출하는 문 앞에 <>으로 제네릭 타입을 지정해줘야한다. 

 

 

※ 제네릭 메소드 탐구1(?)

public class ClassA<T, M> {
	public <K, B> void method(T t, K k) {
    
    }
}

제네릭 클래스에서 선언된 제네릭 타입은 필드, 메소드 안에서 모두 유효하다.
제네릭 메소드를 만들 수 있는데 메소드 리턴 타입 앞에 붙은 제네릭 타입은 그 메소드에서만 유효한 타입이다. 

 

 

※ 제네릭 메소드 탐구2

package chap4_stack;

public class stack {
	public static void main(String ar[]) {
    	Park<String, Integer> p = new Park<>();
        p.<Integer>method(new Car<>(), 10); // 1번코드
        p.method(new Car<>(), 10); // 2번코드
    }
}
	
class Park<T, M> {
	public <K> void method(Car<M> car, K k) {
    	System.out.println(k);
    }
}

class Car<T> {
	
}

method 메소드의 매개변수의 제네릭 클래스(Car)가 들어가 있다. 

M은 클래스의 제네릭 타입 M을 뜻하고 객체 생성시 생성자 호출할 떄 T와 M 값을 주었기 때문에 메소드 호출 시 매개변수의 Car클래스의 제네릭 타입을 생략해도 된다.

K의 타입은 어떤가? method를 호출할 때 두 번째 인자는 method 메소드의 제네릭 타입 K를 결정하였다. 따라서 1번 코드와 같이 제네릭 K를 지정해도 되고 2번 코드와 같이 생략해도 잘 동작한다. 

 

 

※ 제네릭 메소드 탐구3

class Child <T extends Parent> {
	public <M extends T> Car<?> method(Car<? extends Car<T>> car) {
    	Car<M> car1 = new Car<>();
        return car1;
    }
}

위 코드를 분석해보자. 

 

- Child라는 클래스는 제네릭 타입 T로 선언되어 있는데 T에 들어올 수 있는 타입은 Parent클래스를  포함해 이를 상속한 타입만 들어올 수 있다. T는 Child클래스의 필드, 메소드에 영향을 주게된다.


- method는 제네릭 메소드 인데 타입이 M이다 이때 M 타입은 T를 포함해 T를 상속한 타입만 올 수 있다. 이 메소드의 리턴 타입은 와일드카드 타입으로 리턴할 Car클래스의 제네릭 타입은 아무 타입이나 와도 상관 없다. 와일드 카드 타입이므로 제네릭메소드 타입인 M과는 독립적이다. 


- method의 매개변수는 Car클래스인데 이때 이 Car 클래스의 제네릭 타입은 Car클래스를 포함해 이를 상속한 클래스가 올 수 있고 T값은 이미 정해져 있다. 클래스 선언시 T를 지정하기 때문이다. 


 

 

5. 제네릭 메소드와 클래스에서 제한된 타입 파라미터

- 클래스와 메소드 선언시 클래스는 이름 뒤에 T 메소드는 리턴 타입 앞의 T를 제한하는데만 사용된다. 나머지는 이 방식으로 제한되지 않는다.

 

- 제네릭 타입에서 <>로 제네릭 타입을 선언하는데 제한된 타입 파라미터는”public <T extends Number> T method(T t){}; 이렇게 선언하면 
public <T> T method(T t){}; 이 상태에서 T 값에는 Number를 상속하는 클래스만 들어 갈 수 있따는 의미이다. 

 

- 제한된 타입 파라미터 사용시 주의할 점은 클래스에서는 class ClassA<T>{} 이렇게 클래스 선언시에만 T를 제한하는 것이고, 메소드에서는 메소드 선언시 public <T> void method1(T t){}; 리턴타입 앞의 저 T에서만 제한한다는 것이다. 메소드 선언시 private <T> void method3(Car<T extends String> car) 이렇게 리턴 타입 앞의 T를 제한하지 않고 매개변수에 들어가는 제네릭 클래스의 제네릭 타입을 제한하는 것은 틀렸다. 저런 상황에선 와일드카드 타입을 사용해 
private void method3(Car<? extends String> car) 이렇게 선언해야한다. 

 

- 예시1) public <T extends Car<String>> void method3(T t) {}; 이 선언은 Car<String>를 상속한 클래스만 T 타입이 될 수 있다는 의미이다.

 

- 예시2) public class Car<T extends Number>{} 이 선언 역시 클래스의 제네릭 타입에 Number를 상속한 타입만 올 수 있다는 것이다. 

 

※ 제한된 타입의 파라미터를 지정할려고 할 때 extends 키워드를 사용하고 클래스 또는 인터페이스가 올 수 있다.

만약 인터페이스인 경우에 implements키워드를 사용하지 않고 extends 키워드를 그냥 사용한다. 


 

 

6. 와일드카드 타입

6.1 와일드카드 타입이란?

- 리턴 타입과 매개변수의 제네릭 클래스가 들어갈 때 그 클래스의 제네릭 타입 이 두가지만을 제한하는 경우에 사용된다. 나머지에는 와일드카드 타입이 사용되지 않는다. 

 

- 제네릭 클래스의 제네릭 타입 T가 아닌 클래스 내에서 다른 제네릭 타입을 사용하는 경우에 해당 제네릭 타입을 제한하기 위해선 그 제네릭을 와일드카드 타입으로 지정하고 제한해야한다.

아래 예시를 들면,

 

 

6.2 와일드카드 타입 예시

원래라면 
public <T> void method1(Car<T> car) {} 이렇게 선언하고 사용시 T의 값을 구체화 하면된다.
와일드카드 타입의 경우 

 

경우1) public static void method2(Car<?> car)
경우2) private void method3(Car<? extends String> car)
경우3) protected void method4(Car<? super Person> car)

 

1번의 경우 : ?자리가 원래 T이다. 1번의 경우는 T자리에 아무 (클래스) 타입이나 올 수 있단  의미이다. 


2번의 경우 : String 클래스를 상속하는 클래스 타입만 T 자리에 올 수 있단 의미이다. 다시 말해 T자리에는 String 클래스를 상속하는 하위 클래스 타입들만 올 수 있단 것이다. 

 

3번의 경우 :  Person을 포함해 이보다 보다 부모 클래스 타입만 ? 자리에 올 수 있다. 

 

 

6.3 매개변수에 와일드카드 타입 예시

public <K> void method2(Car<? extends String> car) {} 
이 상황에서 매개변수의 제네릭 클래스의 제네릭 타입은 명시적으로 적어야하고 String을 포함해 상속된 클래스 타입만 올 수 있다.  K는 와일드카드 타입에 아무런 영향도 안준다. 

 

 

6.4 리턴 타입에 와일드카드 타입

public <K> Car<? super Person> method(Car<? extends String> car) {
    Car<Integer> car1 = new Car<>();
    return car1;
}

리턴하는 객체는 Car 객체여야하고 Car클래스의 제네릭 타입은 Person를 포함한 상위 클래스만 리턴이 가능하단 의미이다. 위 예시에선 클래스의 제네릭 타입 K는 클래스 내부에서 아무런 영향도 없는 제네릭 타입이다. 


 

'Java' 카테고리의 다른 글

15. 람다식  (0) 2022.05.02
13. 컬렉션 프레임워크  (0) 2022.03.30
12. 자바 API 클래스  (0) 2022.02.17
11. 자바 예외 처리(실행 예외, 예외 처리 코드, 사용자 정의 예외)  (0) 2022.02.16
10. 중첩 클래스  (0) 2022.02.06