: 타입을 파라미터로 가지는 클래스와 인터페이스
: 선언 시 클래스 또는 인터페이스 이름 뒤에 <> 부호를 붙임
: <> 사이에는 타입 파라미터가 위치
타입 파라미터
: 일반적으로 대문자 알파벳 한 문자로 표현
: 개발 코드에서는 타입 파라미터 자리에 구체적인 타입을 지정해야 한다.
제네릭 타입의 사용 여부에 따른 비교
: 사용하지 않은 경우 (오브젝트 타입사용 -> 빈번한 타입 변환이 발생 -> 프로그램의 성능 저하
: 사용한 경우 ( 클래스 선언시 타입 파라미터 사용, 컴파일 시 타입 파라미터가 구체적으로 들어가면서 다양한 코드를 생성 가능하도록 해줌)
class Box {
private Object object;
public void set(Object obj) {
this.object = obj;
}
public Object get() {
return object;
}
}
기본적인 만능박스의 모습. Object타입으로 선언한 이유는 필드에 모든 종류의 객체를 저장하고 싶어서이다. Object 클래스는 모든 자바 클래스의 최상위 클래스이기 때문에 자식 객체는 부모 타입에 대입할 수 있다는 성질을 따라 모든 자바 객체는 Object타입으로 자동 타입 변환되어 저장된다.
public class BoxEx {
public static void main(String[] args) {
Box box = new Box();
box.set("홍길동");
// 박스의 get이라는 걸 이용해서 name에 홍길동 담아주기
// 근데 get은 object타입으로 String 보다 큰 타입이니까
// 다운 해줘야 한다
String name = (String) box.get();
System.out.println();
// 박스에서 apple값 가져와서 apple에 넣어줄때도
// 다운 해줘야 한다
box.set(new Apple());
Apple apple = (Apple) box.get();
}
}
class Apple{
}
// 형 변환하는 만능 박스
class Box {
private Object object;
public void set(Object obj) {
this.object = obj;
}
public Object get() {
return object;
}
}
public class Original_Shape {
public static void main(String[] args) {
// 박스 안에 Integer 담겠다
Box box = new Box();
box.set(new Integer(10));
// 사용할 타입으로 형을 낮춰서 사용
Integer number = (Integer) box.get();
System.out.println(number);
// 박스 안에 String 넣기
box.set("홍길동");
String name = (String) box.get();
System.out.println(name);
}
}
// 만능 박스
class Box {
private Object obj;
public void set(Object obj) {
this.obj = obj;
}
public Object get() {
return obj;
}
public Box() { }
public Box(Object obj) {
this.obj = obj;
}
}
위와 같이 사용한다. 하지만 모든 타입을 저장할 수 있다는 Object클래스만의 장점은 있지만, 타입 변환이 빈번해지면 전체 프로그램 성능에 좋지 못한 결과를 가져올 수 있다. 이럴때 제네릭을 사용하는 것이다.
Object의 자리에 T를 넣어주면 간단히 바꿔 사용할 수 있다.
class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
이런식으로 박스를 설정한다면 객체 생성시 사용할 타입을 <>에 넣어 호출하면된다. 예를 들어 String타입을 사용한다고 생각해보자.
Box<String> box = new Box<String>();
이런식으로 객체 생성을 하면 박스 속 내용은 이런식으로 바뀐고 생각하면 된다.
class Box<Stirng> {
private String t;
public void set(String t) {
this.t = t;
}
public String get() {
return t;
}
}
이런식으로 사용할 수 있다.
public class B_Generic_Shape {
public static void main(String[] args) {
// 박스 안에 Integer 담겠다
Box1<Integer> box = new Box1<Integer>();
box.set(new Integer(10));
Integer number = box.get();
System.out.println(number);
}
}
// 만능 박스
class Box1<T> {
private T t;
public void set(T t) {this.t = t;}
public T get() {return t;}
public Box1() {}
public Box1(T t) {this.t = t;}
}
public class Generic {
public static void main(String[] args) {
// 객체 생성 해줄 때
// 타입을 String으로 해주겠다 라는 걸
// <>에 넣어줬기 때문에 따로 다운캐스팅이나 그런게 필요 없다
Box1<String> box1 = new Box1<String>();
box1.set("홍길동");
String name = box1.get();
System.out.println();
// 타입이 String이 아니라 Apple1이 들어가야하니까
// 따로 하나 더 받아줌
Box1<Apple1> box2 = new Box1<Apple1>();
box2.set(new Apple1());
Apple1 Apple1 = (Apple1) box2.get();
}
}
class Apple1{
}
// 형 변환하는 만능 박스
class Box1<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
public class A_Generic_Test {
public static void main(String[] args) {
Car<KiaCar> car1 = new Car<KiaCar>(new KiaCar(), new KiaCar());
car1.setB(new KiaCar());
KiaCar kc = car1.getB();
Car<HyunDaeCar> car2 = new Car<HyunDaeCar>(new HyunDaeCar(), new HyunDaeCar());
}
}
class Car<T>{
private T o1;
private T o2;
public Car(T o1, T o2) {
this.o1 = o1;
this.o2 = o2;
}
void setB(T o1) { }
T getB() {
return o1;
}
}
class KiaCar{ }
class HyunDaeCar{ }
제네릭 타입은 두 개 이상의 멀티 타입 파라미터도 사용할 수 있다. 이 경우 각 타입의 파라미터를 콤마로 구분하기만 하면 된다.
class<K, V>
혹은 interface<K, V>
와 같은 모습.
class ProductExample {
public static void main(String[] agrs) {
// A회사 Kind = String / Model = String
Product<String, String> a = new Product<String, String>();
// B회사 Kind = Integer / Model = String
Product<Integer, String> b = new Product<Integer, String>();
// C회사 Kind = Student / model = Person
Product<Student, Person> c = new Product<Student, Person>();
}
}
class Product <T, M>{
private T kind;
private M model;
public Product() {}
public Product(T kind, M model) {
this.kind = kind;
this.model = model;
}
void setKind(T kind) {
this.kind = kind;
}
T getKind() {
return kind;
}
void setModel(M model) {
this.model = model;
}
M getModel() {
return model;
}
}
class Student{}
class Person{}
public class C_Gerneric_Shape {
public static void main(String[] args) {
// 2개 받기
Box2<Integer, String> box = new Box2<Integer, String>();
// 클래그 집어넣기
Car car = new Car<Double, Boolean, Float>();
}
}
// 만능 박스
class Box2<T, V> {
private T t;
private V v;
void setT(T t) { this.t = t; }
void setV(V v) { this.v = v; }
T getT() { return t; }
V getV() { return v; }
}
class Car<A, B, C> {
A a;
B b;
C c;
void setA(A a) { this.a = a; }
void setB(B b) { this.b = b; }
void setC(C c) { this.c = c; }
A getA() { return a; }
B getB() { return b; }
C getC() { return c; }
}
매개 타입과 리턴 타입으로 타입 파라미터를 가지는 메소드를 말한다.
public <타입 파라미터, ...> 리턴타입 메소드명(매개변수, ...) {...}
다음 boxing() 제네릭 메소드는 <>기호 안에 타입 파라미터 T를 기술한 뒤, 매개변수 타입으로도 T를 사용했고, 리턴타입으로도 T를 사용했다.
public class C_BoxingMethodEx {
public static void main(String[] args) {
Box2<Integer> result = Util.boxing(new Integer(10));
System.out.println(result.get());
System.out.println();
Car1<String, Integer> result1 = Util.carRun("소나타",3500);
Car1<String, String> result2 = Util.carRun("기아차", "현대차");
System.out.println();
Product1<String, Boolean, Integer> result3 = Util.goProduct1("홍길동", true, 20);
System.out.println(result3);
}
}
class Util {
public static <T> Box2 <T> boxing(T t) {
Box2<T> box = new Box2<T>();
box.set(t);
return box;
}
// 자동차로?
public static <T, V> Car1 <T, V> carRun(T t, V v){
Car1<T, V> car = new Car1<T, V>(t, v);
System.out.println(car.getT() + "\t " + car.getV());
return car;
}
// Product1를 리턴타입으로 쓰는 메소드 만들기
public static <A, B, C> Product1 <A, B, C> goProduct1(A a, B b, C c){
Product1<A, B, C> pro1 = new Product1<A, B, C>();
pro1.setA(a);
pro1.setB(b);
pro1.setC(c);
return pro1;
}
}
class Box2<T> {
T t;
void set(T t) {
this.t = t;
}
T get() {
return t;
}
}
class Car1<T, V> {
T t;
V v;
public Car1(T t, V v) {
this.t = t;
this.v = v;
}
T getT() { return t; }
V getV() { return v; }
}
class Product1<A, B, C> {
A a;
B b;
C c;
void setA(A a) { this.a = a; }
void setB(B b) { this.b = b; }
void setC(C c) { this.c = c; }
A getA() { return a; }
B getB() { return b; }
C getC() { return c; }
@Override
public String toString() {
return getA() + "\t" + getB() + "\t" + getC();
}
}
class CompareMethodEx {
public static void main(String[] args) {
Pair<Integer, String> p1 = new Pair<Integer, String>(1, "사과");
Pair<Integer, String> p2 = new Pair<Integer, String>(1, "사과");
boolean result1 = Util01.<Integer, String>compare(p1, p2);
if (result1) {
System.out.println("논리적으로 동등한 객체입니다.");
} else {
System.out.println("논리적으로 동등하지 않는 객체입니다.");
}
Pair<String, String> p3 = new Pair<String, String>("user1", "홍길동");
Pair<String, String> p4 = new Pair<String, String>("user2", "홍길동");
boolean result2 = Util01.compare(p3, p4);
if (result2) {
System.out.println("논리적으로 동등한 객체입니다.");
} else {
System.out.println("논리적으로 동등하지 않는 객체입니다.");
}
}
}
class Util01 {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
boolean keyCompare = p1.getKey().equals(p2.getKey());
boolean valueCompare = p1.getValue().equals(p2.getValue());
return keyCompare && valueCompare;
}
}
class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) {this.key = key;}
public void setValue(V value) {this.value = value;}
public K getKey() {return key;}
public V getValue() {return value;}
}
타입 파라미터에 지정되는 구체적인 타입을 제한할 필요가 종종 있다. 예를 들어 숫자를 연산하는 제네릭 메소드는 매개값으로 Number타입 또는 하위 클래스 타입의 인스턴스만 가져야하기 때문이다.
제한된 타입 파라미터를 선언하려면 타입 파라미터 뒤에 extends
키워드를 붙이고 상위 타입을 명시하면 된다. 상위타입은 클래스 뿐 아니라 인터페이스도 가능한데 이 경우에도 extends
를 붙여주면 된다 implements
가 아니라
public <T extends 상위타입> 리턴타입 메소드(매개변수, ...) {...}
public class F_BoundedTypeParameterEx {
public static void main(String[] args) {
Util02.compare(new Integer(10), new Double(1.4));
Util02.compare(new Byte((byte)1), new Short((short)14));
// 숫자만 받을 수 있도록 만들어줬기 때문에 String 은 되지 않는다
//Util.compare(new String("이순신"), new String("홍길동"));
}
}
// 제네릭에서 하위 타입으로 제한하기
class Util02 {
// Number 클래스와 Number클래스의 하위 클래스는 다 가능하다
// = 숫자는 다 가능하다
public static <T extends Number> int compare(T t1, T t2) {
double v1 = t1.doubleValue();
double v2 = t2.doubleValue();
return Double.compare(v1, v2);
}
}
코드에서 ?를 일반적으로 와일드 카드라 부른다. 제내릭 타입을 매개값이나 리턴 타입으로 사용할 때 구체적인 타입 대신에 와일드카드를 다음과 같이 세가지 형태로 사용할 수 있다.
public class G_WildCardEx {
// 모든 과정
public static void registerCourse(Course<?> course) {
System.out.println(course.getName() + " 수강생: " +
Arrays.toString(course.getStudents()));
}
// 학생 과정
public static void registerCourseStudent(Course<? extends Student> course) {
System.out.println(course.getName() + " 수강생: " +
Arrays.toString(course.getStudents()) );
}
// 직장인과 일반인 과정
public static void registerCourseWorker(Course<? super Worker> course) {
System.out.println(course.getName() + " 수강생: " +
Arrays.toString(course.getStudents()));
}
public static void main(String[] args) {
Course<Person> personCourse = new Course<Person>("일반인과정", 5);
personCourse.add(new Person("일반인"));
personCourse.add(new Worker("직장인"));
personCourse.add(new Student("학생"));
personCourse.add(new HighStudent("고등학생"));
Course<Worker> workerCourse = new Course<Worker>("직장인과정", 5);
workerCourse.add(new Worker("직장인"));
Course<Student> studentCourse = new Course<Student>("학생과정", 5);
studentCourse.add(new Student("학생"));
studentCourse.add(new HighStudent("고등학생"));
Course<HighStudent> highStudentCourse = new Course<HighStudent>("고등학생과정", 5);
highStudentCourse.add(new HighStudent("고등학생"));
registerCourse(personCourse);
registerCourse(workerCourse);
registerCourse(studentCourse);
registerCourse(highStudentCourse);
System.out.println();
//registerCourseStudent(personCourse); (x)
//registerCourseStudent(workerCourse); (x)
registerCourseStudent(studentCourse);
registerCourseStudent(highStudentCourse);
System.out.println();
registerCourseWorker(personCourse);
registerCourseWorker(workerCourse);
//registerCourseWorker(studentCourse); (x)
//registerCourseWorker(highStudentCourse); (x)
G_WildCardEx.registerCourse(workerCourse);
// workerCouse는 Student의 자식이 아니니 에러
// G_WildCardEx.registerCourseStudent(workerCourse);
}
}
class Course<T> {
private String name;
private T[] students;
public Course(String name, int capacity) {
this.name = name;
students = (T[]) (new Object[capacity]);
}
public String getName() { return name; }
public T[] getStudents() { return students; }
public void add(T t) {
for(int i=0; i<students.length; i++) {
if(students[i] == null) {
students[i] = t;
break;
}
}
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() { return name; }
public String toString() { return name; }
}
class Worker extends Person {
public Worker(String name) {
super(name);
}
}
class Student extends Person {
public Student(String name) {
super(name);
}
}
class HighStudent extends Student {
public HighStudent(String name) {
super(name);
}
}
-> Couse라는 제네릭 타입의 클래스를 만들었을 때
Couse<?> = 모든 타입을 사용 가능하다
Couse<? extends Student> = Student와 그 하위인 HighStudent만 가능하다
Couse<? super Worker> = Worker와 그 상위 타입인 Person만 가능하다
다른 타입과 마찬가지로 부모 클래스가 될 수 있다.
public class ChildProduct<T, M> extends Product<T, M> {...}
위 코드는 Product<T, M> 제네릭 타입을 상속해서 ChildProduct<T, M> 타입을 정의한다.
자식 제네릭 타입은 추가적으로 타입 파라미터를 가질 수 있다. 다음은 세 가지 타입 파라미터를 가진 자식 제네릭 타입을 선언한 것이다
public class ChildProduct<T, M, C> extends Product<T, M> {...}