제네릭
- 데이터의 타입(data type)을 일반화한다(generalize)는 뜻
- 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법
- 결정되지 않은 타입을 파라미터로 처리하고 실제 사용시 파라미터를 구체적인 타입으로 대체시키는 방법
- 컴파일 시에 미리 타입 검사(type check)를 수행 가능하므로 클래스나 메소드 내부에서 사용되는 객체의 타입 안정성을 높임
- 반환값에 대한 타입 변환 및 타입 검사에 들어가는 노력을 줄일수 있음
//제네릭은 클래스와 메소드에만 다음과 같은 방법으로 선언
class MyArray<T> {
T element;
void setElement(T element) { this.element = element; }
T getElement() { return element; }
}
//선언된 제네릭 클래스(generic class)를 생성할 때에는 타입 변수 자리에 사용할 실제 타입을 명시해야 함
MyArray<Integer> myArr = new MyArray<Integer>();
<T>
- T가 타입 파라미터임을 뜻하는 기호
- 'T'를 타입 변수(type variable)라고 하며, 임의의 참조형 타입을 의미
- 타입이 필요한 자리에 T를 사용할 수 있음을 알려주는 역할을 한다.
- 'T'뿐만 아니라 어떠한 문자를 사용해도 상관없으며, 여러 개의 타입 변수는 쉼표(,)로 구분하여 명시할 수 있음
//Box 클래스에서 결정되지 않은 content의 타입을 T라는 타입 파라미터로 정의한것
public class Box <T> {
public T content;
}
Box 클래스는 T를 content 필드의 타입으로 사용했다.
Box는 T가 뭔지 모르지만 Box 객체가 생성될 시점에 다른 타입으로 대체된다는것을 알고있다.
//Box의 내용물로 String을 저장하고 싶을때
Box<String> box = new Box<String>(); //Box 생성시 <String>으로 지정
box.content = "안녕하세요.;
String content = box.content; //강제 타입 변환이 필요 없이 "안녕하세요"를 바로 얻을수 있음
//Box의 내용물로 정수값을 저장하고 싶을때
Box<Integer> box = new Box<Integer>(); //Box 생성시 <Integer>으로 지정
box.content = 100;
int content = box.content; //강제 타입 변환이 필요없이 100을 바로 얻을 수 있음
주의 : 타입 파라미터를 대체하는 타입은
클래스
및인터페이스
그래서 Box<int>
와 같이 사용할 수 없다. (int는 클래스나 인터페이스가 아니므로)
변수 선언시와 동일한 타입으로 호출 하는 방법
생성자 호출 시 생성자에는 타입을 명시하지 않고
<>
만 붙일수 있음
package ch13.sec01;
public class Box <T> {
public T content; //타입 파라미터로 T 사용
}
package ch13.sec01;
public class GenericExample {
public static void main(String[] args) {
// Box<String> box1 = new Box<String>();
Box<String> box1 = new Box<>();
box1.content = "안녕하세요";
String str = box1.content;
System.out.println(str); //안녕하세요
// Box<Integer> box2 = new Box<Integer>();
Box<Integer> box2 = new Box<>();
box2.content = 100;
int value = box2.content;
System.out.println(value); //100
}
}
제네릭 타입
결정되지 않은 타입을 파라미터로 가지는 클래스와 인터페이스
선언부에 '<>' 부호가 붙고 그 사이에 타입 파라미터들이 위치public class 클래스명<A, B, ...> {...} public interface 인터페이스명<A, B, ...> {...}
타입 파라미터는 변수명과 동일한 규칙에 따라 작성할 수 있지만 일반적으로 대문자 알파벳 한글자로 표현한다.
외부에서 제네릭 타입을 사용하려면 타입 파라미터에 구체적인 타입을 지정해야한다.
지정하지 않으면 Object 타입이 암묵적으로 사용된다.
Product 클래스를 제네릭 타입으로 선언하고, kind, model 필드를 타입 파라미터로 선언하고, Getter의 매개변수와 Setter의 리턴 타입 역시 타입파라미터로 선언
Product 에 다양한 종류와 모델 제품을 저장하기 위해서 타입 파라미터를 사용함
package ch13.sec02.exam01;
public class Product <K,M> { // 제네릭타입(타입 파라미터로 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;}
}
package ch13.sec02.exam01;
public class Tv {
}
package ch13.sec02.exam01;
public class Car {
}
package ch13.sec02.exam01;
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();
}
}
package ch13.sec02.exam02;
public interface Rentable <P> { //타입파라미터 P 정의
P rent(); //타입 파라미터 P를 리턴 타입으로 사용
}
package ch13.sec02.exam02;
public class Home {
public void turnOnLight() {
System.out.println("전등을 켭니다.");
}
}
package ch13.sec02.exam02;
public class Car {
public void run() {
System.out.println("자동차가 달립니다.");
}
}
집과 차를 렌트해주는 대리점 클래스 HomeAgency, CarAgency
Rentable의 타입 파라미터를 Home, Car 로 대체해서 구현하는 방법을 보여준다.
package ch13.sec02.exam02;
public class HomeAgency implements Rentable<Home> { //타입파라미터 P를 Home으로 대체
@Override
public Home rent() {
return new Home(); //리턴 타입이 반드시 Home 이어야함
}
}
package ch13.sec02.exam02;
public class CarAgency implements Rentable<Car> {
@Override
public Car rent() {
return new Car(); //리턴 타입이 반드시 Car여야 함
}
}
HomeAgency, CarAgency 에서 대여한 Home, Car를 이용하는 방법
package ch13.sec02.exam02;
public class GenericExmaple {
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(); //자동차가 달립니다.
}
}
타입 파라미터는 기본적으로 Object 타입으로 간주되므로 Object 가 갖고있는 메소드를 호출가능
package ch13.sec02.exam03;
public class Box <T>{
public T content;
//Box 내용물이 같은지 비교
public boolean compare(Box<T> other) {
boolean result = content.equals(other.content);
return result;
}
}
package ch13.sec02.exam03;
public class GenericExample {
public static void main(String[] args) {
Box box1 = new Box();
box1.content = "100";
Box box2 = new Box();
box2.content = "100";
Box box3 = new Box();
box3.content = 100;
boolean result1 = box1.compare(box2);
System.out.println("result 1 " + result1); //Box 의 내용물 비교
boolean result2 = box2.compare(box3);
System.out.println("result 2 " + result2); //Box 의 내용물 비교
//result1 : true
//result2 : false
}
}
제네릭 메소드
타입 파라미터를 가지고 있는 메소드
타입 파라미터가 메소드 선언부에 정의된다는 점에서 제네릭 타입과 차이가 있음
제네릭 메소드 사용법
리턴 타입 앞에 <> 기호를 추가하고 타입 파라미터를 정의한 뒤,
리턴 타입과 매개변수 타입에서 사용public <A, B, ...> 리턴타입 메소드명(매개변수, ...) {...}
다음 boxing() 메소드는 타입 파라미터로 를 정의하고 매개변수 타입과 리턴 타입에서 T를 사용한다.
정확한 리턴 타입은 T를 내용물로 갖는 Box 객체이다.
public <T> Box<T> boxing(T t) {...}
타입 파라미터 T는 매개값이 어떤 타입이냐에 따라 컴파일 과정에서 구체적인 타입으로 대체된다.
① Box<Integer> box1 = boxing(100);
② Box<String> box2 = boxing("안녕하세요");
①은 100의 클래스 타입이 Integer 이므로 타입 파라미터 T 는 Integer 로 대체되어 Box<Integer>
가 리턴
②는 “안녕하세요” 의 클래스 타입이 String 이므로 타입 파라미터 T 는 String으로 대체되어 Box<String>
이 리턴
package ch13.sec03.exam01;
//제네릭 타입인 Box 클래스 선언
public class Box <T> {
//필드
public T t;
//Getter 메소드
public T get() {
return t;
}
//Setter 메소드
public void set(T t) {
this.t = t;
}
}
package ch13.sec03.exam01;
//제네릭 메소드인 boxing을 선언하고 호출하는 방법에 대한 예제
public class GenericExample {
//제네릭 메소드
public static <T> Box<T> boxing(T t) { //타입파라미터 <T> 정의
Box<T> box = new Box<>();
box.set(t);
return box;
}
public static void main(String[] args) {
//제네릭 메소드 호출
Box<Integer> box1 = boxing(100); //T를 Integer로 대체
int intValue = box1.get();
System.out.println(intValue);
//제네릭 메소드 호출
Box<String> box2 = boxing("홍길동"); //T를 String으로 대체
String strValue = box2.get();
System.out.println(strValue);
// 100
// 홍길동
}
}
제한된 타입 파라미터
타입 파라미터를 대체하는 구체적인 타입을 제한할 필요가 있을때 사용
모든 타입으로 대체할 수 없고 특정 타입과 자식 또는 구현 관계에 있는 타입만 대체할 수 있는 타입 파라미터public <T extends 상위타입> 리턴타입 메소드(매개변수, ...) {...}
예를 들어, 숫자를 연산하는 제네릭 메소드는 대체 타입으로 Number 또는 자식 클래스(Byte, Short, Integer, Long, Doubld) 로 제한할 필요가 있음
상위 타입은 클래스 뿐 아니라 인터페이스도 가능.
인터페이스라고 해서 implements 를 사용하지는 않는다.
다음은 Number 타입과 자식 클래스(Byte, Short, Integer, Long, Doubld) 에만 대체 가능한 타입 파라미터를 정의한 것이다.
//Number 타입과 자식 클래스(Byte, Short, Integer, Long, Double)에만 대체 가능한 타입 파라미터 정의한것
public <T extends Number> boolean compare(T t1, T t2) {
double v1 = t1.doubleValue(); //Number 의 doubleValue() 메소드 사용
double v2 = t2.doubleValue(); //Number 의 doubleValue() 메소드 사용
return (v1 == v2);
}
타입 파라미터가 Number 타입으로 제한되면서 Object 의 메소드 뿐 아니라, Number 가 가지고 있는 메소드도 사용 가능
package ch13.sec04;
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();
double v2 = t2.doubleValue();
return (v1 == v2);
}
public static void main(String[] args) {
//제네릭 메소드 호출
boolean result1 = compare(10,20); // T를 Integer 타입으로 대체
System.out.println(result1);
System.out.println();
//제네릭 메소드 호출
boolean result2 = compare(4.5, 4.5); //T를 Double 타입으로 대체
System.out.println(result2);
//compare(Integer, Integer)
//false
//compare(Double, Double)
//true
}
}
와일드카드 (?)
- 제네릭 타입을 매개값이나 리턴 타입으로 사용할 때 타입 파라미터 ?(와일드카드)를 사용할 수 있다.
- '?' 는 범위에 있는 모든 타입으로 대체할 수 있다는 표시
예를 들어 다음과 같은 상속 관계가 있다고 가정해보자.
//Student 와 자식 클래스인 HighStudent 와 MiddleStudent 만 가능하도록 매개변수를 다음과 같이 선언
리턴타입 메소드명(제네릭타입<? extends Student> 변수) {...}
//Worker와 부모 클래스인 Person 만 가능하도록 매개변수를 다음과 같이 선언
리턴타입 메소드명(제네릭타입<? extends Student> 변수) {...}
//어떤 타입이든 가능하도록 매개변수를 선언
리턴타입 메소드명(제네릭타입<?> 변수) {...}
Course 클래스의 메소드
package ch13.sec05;
public class Person {
}
class Worker extends Person {
}
class Student extends Person {
}
class HighStudent extends Student {
}
class MiddleStudent extends Student {
}
package ch13.sec05;
public class Applicant<T> {
public T kind;
public Applicant(T kind) {
this.kind = kind;
}
}
package ch13.sec05;
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를 등록함");
}
}
package ch13.sec05;
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 Student()));
// Course.registerCourse2(new Applicant<Worker>(new Student()));
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()));
// Course.registerCourse3(new Applicant<HighStudent>(new HighStudent()));
// Course.registerCourse3(new Applicant<MiddleStudent>(new MiddleStudent()));
// Person이(가) Course1을 등록함
// Worker이(가) Course1을 등록함
// Student이(가) Course1을 등록함
// HighStudent이(가) Course1을 등록함
// MiddleStudent이(가) Course1를 등록함
//
// Student이(가) Course2를 등록함
// HighStudent이(가) Course2를 등록함
// MiddleStudent이(가) Course2를 등록함
//
// Person이(가) Course3을 등록함
// Worker이(가) Course3을 등록함
}
}
출처: 이것이 자바다 (개정판) : JAVA 프로그래밍의 기본서 - 신용권, 임경균 저