Java Category/Java

[Java] 제네릭(Generic)

ReBugs 2023. 7. 31.

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 


제네릭의 개념

제네릭이란 결정되지 않은 타입을 파라미터로 처리하고 실제 사용할 때 파라미터를 구체적인 타입으로 대체시기는 기능이다.

 

아래의 코드는 Box 클래스에서 결정되지 않은 content의 타입을 T라는 타입 파라미터로 정의한 것이다.

public class Box <T>{
	public T content;
}

<T>는 T가 타입 파라미터임을 뜻하는 기호로, 타입이 필요한 자리에 T를 사용할 수 있음을 알려주는 역할을 한다.

Box 클래스는 T가 무엇인지 모르지만, Box 객체가 생성될 시점에 다른 타입으로 대체된다는 것을 알고 있다.

 

Box의 내용물로 String 타입을 저장하고 싶으면 아래와 같이 코드를 작성하면 된다.

Box<String> box = new Box<String>();
box.content = "안녕하세요";
String con = box.content;

 

Box의 내용물로 int 타입을 저장하고 싶으면 아래와 같이 코드를 작성하면 된다.

Box<Integer> box = new Box<Integer>();
box.content = 100; //자동 박싱
int con = box.content;

<T>에서 타입 파라미터로 쓰이는 T는 A부터 Z까지 어떤 알파벳을 써도 상관이 없다.

또한 주의할 점은 타입 파라미터를 대체하는 타입은 클래스 및 인터페이스이다.

 

아래와 같이 변수를 선언할 때와 동일한 타입으로 호출하고 싶다면 생성자 호출시 생성자에는 타입명을 명시하지 않아도 된다.

Box<Integer> box = new Box<>();

 


 

제네릭 타입

public class 클래스명<A, B, ...>{}
public interface 인터페이스명<A, B, ...>{}
  • 제네릭 타입 : 결정되지 않은 타입을 파라미터로 가지는 클래스와 인터페이스
  • 선언부에 <> 부호가 붙고 그 사이에 타입 파라미터들이 위치
  • 타입 파라미터 : 일반적으로 대문자 알파벳 한 글자로 표현
  • 외부에서 제네릭 타입을 사용하려면 타입 파라미터에 구체적인 타입을 지정해야 함
  • 외부에서 제네릭 타입을 지정하지 않으면 암묵적으로 Object 객체가 사용된다.
타입 파라미터
타입 파라미터는 기본적으로 Object 타입으로 간주되므로 Object가 가지고 있는 메소드를 호출할 수 있다.
예를 들어, 타입 파라미터는 Object의 equals() 메소드를 호출할 수 있고, 타입 파라미터로 대입된 객체가 equals() 메소드가 재정의 되어 있다면 재정의된 equals() 메소드가 호출된다.

 

제네릭 타입 클래스

Product.java

//제네릭 타입
public class Product<K, M> {
	//필드
	private K kind;
	private M model;
	
	//메소드
	public K getKind() { return this.kind; }
	public M getModel() { return this.model; }
	public void setKind(K kind) { this.kind = kind; }
	public void setModel(M model) { this.model = model; }
}

 

GenericExample.java

 

public class GenericExample {
	public static void main(String[] args) {
		//K는 Tv로 대체, M은 String으로 대체
		Product<Tv, String> product1 = new Product<>();
		
		//Setter 매개값은 반드시 Tv와 String을 제공
		product1.setKind(new Tv());
		product1.setModel("스마트Tv");
		
		//Getter 리턴값은 Tv와 String이 됨
		Tv tv = product1.getKind();
		String tvModel = product1.getModel();
		//------------------------------------------------------------------------
		//K는 Car로 대체, M은 String으로 대체
		Product<Car, String> product2 = new Product<>();
			
		//Setter 매개값은 반드시 Car와 String을 제공
		product2.setKind(new Car());
		product2.setModel("SUV자동차");
		
		//Getter 리턴값은 Car와 String이 됨
		Car car = product2.getKind();
		String carModel = product2.getModel();
	}
}

 

제네릭 타입 인터페이스

 

제네릭 타입 인터페이스 선언

public interface Rentable<P> {
	P rent();
}

public class Home {
	public void turnOnLight() {
		System.out.println("전등을 켭니다.");
	}
}
public class Car {
	public void run() {
		System.out.println("자동차가 달립니다.");
	}
}

아래의 HomeAgency와 CarAgency는 Rentable의 타입 파라미터를 Home과 Car로 대체해서 구현하는 방법을 보여준다.

public class HomeAgency implements Rentable<Home> {
	@Override
	public Home rent() {
		return new Home();
	}
}
public class CarAgency implements Rentable<Car>{
	@Override
	public Car rent() {
		return new Car();
	}
}

public class GenericExample {
	public static void main(String[] args) {
		HomeAgency homeAgency = new HomeAgency();
		Home home = homeAgency.rent();
		home.turnOnLight();

		CarAgency carAgency = new CarAgency();
		Car car = carAgency.rent();
		car.run();
	}
}
/*
전등을 켭니다.
자동차가 달립니다.
*/

위에 예제의 실행 내용과 같이

HomeAgency와 CarAgency를 생성하면 각 Home 객체와 Car 객체가 리턴되고, 각 메소드를 호출할 수 있다.

 


 

제네릭 메소드

  • 제네릭 메소드 : 타입 파라미터를 가지고 있는 메소드
  • 타입 파라미터가 메소드 선언부에 정의
  • 타입 파라미터, 리턴타입, 메소드명(매개변수) 순으로 선언한다
  • 타입 파라미터는 메소드로 들어온 매개값이 어떤 타입이냐에 따라 컴파일 과정에서 구체적인 타입으로 대체된다.

아래의 boxing() 메소드는 타입 파라미터로 <T>를 정의하고 매개 변수 타입과 리턴 타입에서 T를 사용한다.

정확한 리턴 타입은 T를 내용물로 갖는 Box 객체이다.

 

public <T> Box<T> boxing(T t){...}​

 

Box.java

public class Box<T> {
	//필드
	private T t;
	
	//Getter 메소드
	public T get() {
		return t;
	}
	
	//Setter 메소드
	public void set(T t) {
		this.t = t;
	}
}

 

GenericExample.java

public class GenericExample {
	//제네릭 메소드
	public static <T> Box<T> boxing(T t) {
		Box<T> box = new Box<T>();
		box.set(t);
		return box;
	}

	public static void main(String[] args) {
		//제네릭 메소드 호출
		Box<Integer> box1 = boxing(100);
		int intValue = box1.get();
		System.out.println(intValue);

		//제네릭 메소드 호출
		Box<String> box2 = boxing("홍길동");
		String strValue = box2.get();
		System.out.println(strValue);
	}
}
/*
100
홍길동
*/

 


 

제한된 타입 파라미터

  • 제한된 타입 파라미터 : 모든 타입으로 대체할 수 없고, 특정 타입과 자식 또는 구현 관계에 있는 타입만 대체할 수 있는 타입 파라미터
  • 예를 들어 숫자를 연산하는 제네릭 메소드는 대체 타입으로 Number 또는 자식 클래스(Byte, Short, Integer, Long, Double)로 제한
  • 상위 타입은 클래스뿐만 아니라 인터페이스도 가능(인터페이스라고 해서 implements 를 사용하지 않음)
public class GenericExample {
	//제한된 타입 파라미터를 갖는 제네릭 메소드
	public static <T extends Number> boolean compare(T t1, T t2) {
		//T의 타입을 출력
		System.out.println("compare(" + t1.getClass().getSimpleName() + ", " +
				t2.getClass().getSimpleName() + ")"); //리플렉션

		//Number의 메소드 사용
		double v1 = t1.doubleValue(); //Number의 메소드 사용
		double v2 = t2.doubleValue(); //Number의 메소드 사용

		return (v1 == v2);
	}

	public static void main(String[] args) {
		//제네릭 메소드 호출
		boolean result1 = compare(10, 20);
		System.out.println(result1);
		System.out.println();

		//제네릭 메소드 호출
		boolean result2 = compare(4.5, 4.5);
		System.out.println(result2);
	}
}
/*
compare(Integer, Integer)
false

compare(Double, Double)
true
*/

 


 

와일드카드 타입 파라미터

출처 : 이것이 자바다 유튜브 영상

위와 같은 상속 관계가 있다고 가정해 보자.

 

Student와 자식 클래스인 HighStudent, middleStudent만 타입 파라미터의 대체 타입으로 가능하도록 하고 싶다면 아래와 같이 선언하면 된다.

리턴타입 메소드명(제네릭타입<? extends Student> 변수) {}

 

반대로 Worker와 부모 클래스인 Person만 가능하도록 매개변수를 아래와 같이 선언할 수 있다.

리턴타입 메소드명(제네릭타입<? super Worker> 변수) {}

 

 아래와 같이 어떤 타입이든 가능하도록 매개변수를 선언할 수도 있다.

리턴타입 메소드명(제네릭타입<?> 변수) {}

 

 

아래의 예제에 대한 설명

  • registerCourse1()은 모든 사람이 들을 수 있는 과정을 등록한다.
  • registerCourse2()는 학생만 들을 수 있는 과정을 등록한다.
  • registerCourse3() 은 직장인과 일반인만 들을 수 있는 과정을 등록한다.

Person.java

public class Person {
}

class Worker extends Person {
}

class Student extends Person {
}

class HighStudent extends Student {
}
	
class MiddleStudent extends Student{
}

 

Applicant.java

public class Applicant<T> {
	public T kind;

	public Applicant(T kind) {
		this.kind = kind;
	}
}

 

Course.java

public class Course {
	//모든 사람이면 등록 가능
	public static void registerCourse1(Applicant<?> applicant) {
		System.out.println(applicant.kind.getClass().getSimpleName() +
				"이(가) Course1을 등록함");
	}
	
	//학생만 등록 가능
	public static void registerCourse2(Applicant<? extends Student> applicant) {
		System.out.println(applicant.kind.getClass().getSimpleName() + 
				"이(가) Course2를 등록함");
	}
	
	//직장인 및 일반인만 등록 가능
	public static void registerCourse3(Applicant<? super Worker> applicant) {
		System.out.println(applicant.kind.getClass().getSimpleName() +
				"이(가) Course3을 등록함");
	}
}

 

GenericExample.java

public class GenericExample {
	public static void main(String[] args) {
		//모든 사람이 신청 가능
		Course.registerCourse1(new Applicant<Person>(new Person()));
		Course.registerCourse1(new Applicant<Worker>(new Worker()));
		Course.registerCourse1(new Applicant<Student>(new Student()));
		Course.registerCourse1(new Applicant<HighStudent>(new HighStudent()));
		Course.registerCourse1(new Applicant<MiddleStudent>(new MiddleStudent()));
		System.out.println();

		//학생만 신청 가능
		//Course.registerCourse2(new Applicant<Person>(new Person())); (x)
		//Course.registerCourse2(new Applicant<Worker>(new Worker())); (x)
		Course.registerCourse2(new Applicant<Student>(new Student()));
		Course.registerCourse2(new Applicant<HighStudent>(new HighStudent()));
		Course.registerCourse2(new Applicant<MiddleStudent>(new MiddleStudent()));
		System.out.println();
			
		//직장인 및 일반인만 신청 가능
		Course.registerCourse3(new Applicant<Person>(new Person()));
		Course.registerCourse3(new Applicant<Worker>(new Worker()));
		//Course.registerCourse3(new Applicant<Student>(new Student())); 		(x)
		//Course.registerCourse3(new Applicant<HighStudent>(new HighStudent())); 	(x)
		//Course.registerCourse3(new Applicant<MiddleStudent>(new MiddleStudent())); 	(x)
	}
}
/*
Person이(가) Course1을 등록함
Worker이(가) Course1을 등록함
Student이(가) Course1을 등록함
HighStudent이(가) Course1을 등록함
MiddleStudent이(가) Course1을 등록함

Student이(가) Course2를 등록함
HighStudent이(가) Course2를 등록함
MiddleStudent이(가) Course2를 등록함

Person이(가) Course3을 등록함
Worker이(가) Course3을 등록함
*/

 

댓글