신용권 님의 ''이것이 자바다'' 13장 공부 기록
책을 보면서 내용을 정리했다.
Java 5 부터 제네릭 타입이 새로 추가되었는데, 제네릭 타입을 이용함으로써 잘못된 타입이 사용될 수 있는 문제를 컴파일 과정에서 제거할 수 있게 되었다.
제네릭은 클래스와 인터페이스, 그리고 메소드를 정의할 때 타입(type)을 파라미터(parameter)로 사용할 수 있도록 한다. 타입 파라미터는 코드 작성 시 구체적인 타입으로 대체되어 다양한 코드를 생성하도록 해준다.
제네릭의 두 가지 이점
추가 검색: 책 내용과 다른 점
책의 내용과는 달리 https://ict-nroo.tistory.com/42 에서 댓글을 보면, 타입변환을 제거할 수 있다는 말은 잘못된 말이라는 내용이 나온다. 바이트 코드 상에서는 형변환이 일어나기 때문이다.
"타입변환을 제거할 수 있다."
이 부분은 잘못된 정보입니다.
byte code 뜯어보면 Generic은 java.lang.Object로 서술되어있고 Run time시의 형변환 이나 제네릭을 통한 byte code상의 형변환은 정확히 똑같습니다.
opcode - checkcast가 발생합니다
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.checkcast
결론적으로
List list = new ArrayList();
list.add("hello");
String str = (String) list.get(0);List list = new ArrayList();
list.add("hello");
String str = list.get(0);직접 참조 자료형을 형변환 하는 것이나 Generic을 사용하는 것이나
동일한 byte code로 작성되며 성능이점 또한 없습니다.출처: https://ict-nroo.tistory.com/42 [개발자의 기록습관]의 댓글 중
추가적으로
https://cla9.tistory.com/44 를 보면 어떤 식으로 generic의 타입 변환이 일어나는 지 친절하게 알려주고 있다.
제네릭이 타입변환을 하는 과정에서 에 대한 경계를 지정하지 않으면 역할을 받는 변수의 타입을 명시하지 않을 때 컴파일시 최상위 타입인 Object 타입으로 받아들이거나, 명시했을 때는 컴파일시 해당 타입으로 변경시키는 것이다 .
결론
다양한 타입을 받을 수 있고, 자동으로 타입변환을 해주는 이점을 가지지만, 제네릭이 타입 변환을 제거하여 프로그램의 성능을 향상시킨다는 말을 옳지 않다고 볼 수 있겠다.
//제네릭 타입
public class 클래스명<T>{...}
public interface 인터페이스명<T>{...} //타입 T를 파라미터로 가진다. 일반적으로 대문자 알파벳 한 글자로 표현한다.
public class Box{
private Object object;
public void set(Object object) {this.object = object}
public Object get(){return object}
}
//Object 타입을 사용함으로써 모든 종류의 자바 객체를 저장할 수는 있지만, 저장할 때, 그리고 다시 찾아올 때 타입변환이 발생한다.
//이는 프로그램 성능에 좋지 않은 영향을 끼칠 수 있다.
//따라서 모든 종류의 객체를 저장하면서 타입 변환이 발생하지 않도록 하는 방법을 찾았는데, 그것이 제네릭이다.
public class Box<T>{
private T t;
public T get() {return t;}
public void set(T t){this.t = t}
}
//타입 파라미터 T를 사용해서 Object 타입을 모두 T로 대체했다. T는 Box 클래스로 객체를 생성할 때 구체적인 타입으로 변경된다.
Box<T> box = new Box<T>;
//T가 String 타입으로 변경되어 Box클래스의 내부가 변경된다.
public class Box<String> {
Private String t;
public String get(){return t;}
public void set(String t){this.t =t}
} //다른 타입을 T에 대입해도 같은 결과가 나온다.
제네릭 타입은 두 개 이상의 멀티 타입 파라미터를 사용할 수 있는데, 이 경우 타입 파라미터를 콤마로 구분한다.
//제네릭 클래스
public class Produc<T,M>{
private T kind;
private M model;
public T getKind() {return this.kind}
public M getModel() {return this.model}
public void setKind(T kind) {this.kind = kind}
public void setModel(M model) {this.model = model}
}
//제네릭 객체 생성
public class ProductEx{
public static void main(String[]args){
Product<Tv, String> product1 = new Product<Tv, String>();
//Product<Tv, String> product1 = new Product<>; 로 선언할 수도 있다.(다이아몬드 연산자)
product1.setKind(new Tv());
product1.setModel("스마트tv");
Tv tv = product1.getKind();
String tvModel = product1.getModel();
}
}
제네릭 메소드는 매개 타입과 리턴 타입으로 타입 파라미터를 갖는 메소드를 말한다.
//제네릭 메소드 양식
public<타입파라미터,...> 리턴타입 메소드명(매개변수,...){}
//public<T> Box<T> boxing(T t){...}
리턴타입 변수 = <구체적 타입> 메소드명(매개값...);//명시적으로 구체적 타입 지정
//Box<Integer> box = <Integer>boxing(100);
리턴타입 변수 = 메소드명(매개값...);//매개값을 통해 구체적 타입 추측
//Box<Integer> box = boxing(100);
제네릭 메소드는 정적 메소드로 사용가능하다(예제에는 거의 정적메소드로만 적혀 있다.)
예제: 제네릭 메소드 호출
//제네릭 메소드가 있는 클래스
public class Util {
public static <K,V> boolean compare(Pair<K,V> p1, Pair<K,V> p2) {
boolean keyCompare = p1.getKey().equals(p1.getKey());
boolean valueCompare = p1.getValue().equals(p2.getValue());
return keyCompare&&valueCompare;
}
}
//제네릭 타입
public 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;}
}
//제네릭 메소드 호출
public class Ex {
public static void main(String[] args) {
Pair<Integer, String> p1 = new Pair(100, "사과");
Pair<Integer, String> p2 = new Pair(100, "사과");
boolean answer = Util.<Integer, String>compare(p1, p2);
// = Util.compare(p1, p2) 비명시적으로 가능
if(answer) {
System.out.println("논리 동등 객체");
} else {
System.out.println("논리 다른 객체");
}
}
}
타입 파라미터에 지정되는 구체적인 타입을 제한할 필요가 있을 때가 있다. 이 때 제한된 타입 파라미터(bounded type parameter)를 사용한다.
public <T extends 상위타입> 리턴타입 메소드(매개변수, ...){...} //상위타입은 클래스뿐아니라 인터페이스도 가능하다.
타입 파라미터에 지정되는 구체적인 타입은 상위 타입이거나 상위 타입의 하위 또는 구현 클래스만 가능하다.
주의할 점은 메소드의 중괄호 {} 안에서 타입 파라미터 변수로 사용 가능한 것은 상위 타입의 멤버(필드, 메소드)로 제한된다. 하위 타입에만 있는 필드와 메소드는 사용할 수 없다.
예제: 숫자 변환 시험
//수 비교하는 메소드
public class UtilN {
public static <T extends Number> int compare(T t1, T t2) {
double v1 = t1.doubleValue();
double v2 = t2.doubleValue();
//Number 하위 Double 클래스로 변환되었으므로 다시 double 타입으로 변환함.
return Double.compare(v1, v2);
}
}
public class BoundedTypeParameterEx {
public static void main(String[] args) {
int result = UtilN.compare(10, 20);//Number타입만 가능함
System.out.println(result);//int -> Integer(자동 Boxing, Number의 하위 클래스로 변환)
int result1 = UtilN.compare(10.2,3.1);
System.out.println(result1);//double => Double(자동 Boxing)
}
}
코드에서 ?를 일반적으로 와일드카드(wildcard)라고 부른다. 제네릭 타입을 매개값이나 리턴 타입으로 사용할 때 구체적인 타입 대신에 와일드카드를 다음과 같이 세 가지 형태로 사용할 수 있다.
- 제네릭타입<?> : Unbounded Wildcards(제한없음), 타입파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있다.
- 제네릭타입<? extends 상위타입> : Upper Bounded Wildcards(상위 클래스 제한), 타입 파라미터를 대치하는 구체적인 타입으로 상위 타입이나 하위 타입만 올 수 있다.(상위타입-> 하위 타입 가능)
- 제네릭타입<? super 하위타입> : Lower Bounded Wildcards(하위 클래스 제한), 타입 파라미터를 대치하는 구체적인 타입으로 하위 타입이나 상위타입이 올 수 있다.(하위 타입 -> 상위 타입 가능)
//Person, 최상위 클래스
public class Person {
String name;
public Person(String name) {
this.name = name;
};
public String getName() {return name;}
//이부분이 있어야 registerCourse의 출력값이 정상적으로 나옴. toString이 원래는 뭐였을까?
//검색->
//Object"클래스가 가진 메소드 중 "toString"메소드가 있습니다.
//물론 "Object" 클래스의 모든 메소드는 모든 클래스가 사용이 가능합니다.
//"toString" 메서드는 객체가 가지고 있는 정보나 값들을 문자열로 만들어 리턴하는 메소드 입니다.
//그래서 이것을 의미 있는 값으로 재정의하여 사용한다!
//출처: https://backback.tistory.com/68 [Back Ground]
@Override
public String toString() {
return name;
}
}
//Person 하위 클래스
public class Student extends Person{
String student;
public Student(String student) {
super(student);
};
}
//Student 하위 클래스
public class HighStudent extends Student{
public HighStudent(String highStudent) {
super(highStudent);
}
}
//Person 하위 클래스
public class Worker extends Person{
public Worker(String worker) {
super(worker);
};
}
//제네릭 타입
public 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;}
//T[] 리턴 타입 메소드
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;
}
}
}
}
//수강생 등록
import java.util.Arrays;
public class WildCardEx {
//double[] values = {1.0, 1.1, 1.2};
//System.out.println(values.toString()); // 이렇게 하면 [D@46a49e6 같은 값이 나옵니다.
//System.out.println(Arrays.toString(values)); // 이렇게 하면 [1.0, 1.1, 1.2] 이 출력됩니다.
//출처: https://crmn.tistory.com/61 [크롬망간이 글 쓰는 공간]
//
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 Student("고등학생"));
Course<HighStudent> highStudentCourse = new Course<HighStudent>("고등학생 과정", 5);
highStudentCourse.add(new HighStudent("고등학생"));
//강의별 와일드카드 범위 따라 출력, 범위 안 맞으면 출력 안됨
registerCourse(personCourse);
registerCourse(studentCourse);
registerCourse(highStudentCourse);
registerCourse(workerCourse);
System.out.println();
// registerCourseStudent(personCourse);
registerCourseStudent(studentCourse);
registerCourseStudent(highStudentCourse);
// registerCourseStudent(workerCourse);
System.out.println();
registerCourseWorker(personCourse);
// registerCourseWorker(studentCourse);
// registerCourseWorker(highStudentCourse);
registerCourseWorker(workerCourse);
System.out.println();
}
}
public class Child<T, M, C> extends Parent<T,M>{...}
//부모 제네릭 클래스
public class Product <T,M>{
private T kind;
private M model;
public T getKind() { return kind;}
public M getModel() { return model;}
public void setKind(T kind) {this.kind =kind;}
public void setModel(M model) {this.model=model;}
}
class Tv{}
//자식 제네릭 클래스
public class ChildProduct<T,M,C> extends Product<T,M>{
private C company;
public void setCompany(C company) {this.company=company;}
public C getCompany() {return this.company;}
}
//제네릭 인터페이스
public interface Storage<T> {
public void add(T item, int index);
public T get(int index);
}
//제네릭 구현 클래스
public class StorageImpl<T> implements Storage<T>{
private T[] array;
public StorageImpl(int capacity) {
this.array = (T[])(new Object[capacity]);
}
@Override
public void add(T item, int index) {
array[index] = item;
}
@Override
public T get(int index) {
return array[index];
}
}
//제네릭 타입 사용 클래스
public class ChildEx {
public static void main(String[] args) {
ChildProduct<Tv,String,String> product = new ChildProduct<>();
product.setKind(new Tv());
product.setModel("Smart TV");
product.setCompany("Samsung");
Storage<Tv> storage = new StorageImpl<Tv>(100);
storage.add(new Tv(), 0);
Tv tv = storage.get(0);
}
}
//Pair 제네릭 타입
public class Pair <K,V>{
private K key;
private V value;
//따로 setKey, setValue로 나눌 수도 있고, 일케 생성자로도 가능
public Pair(K key, V value) {
this.key=key;
this.value=value;
}
public K getKey() { return this.key;}
public V getValue() { return this.value;}
}
//ChilPair 제네릭 타입(Pair 상속받음)
public class ChildPair<K,V> extends Pair<K,V>{
public ChildPair(K key, V value) {
super(key,value);
}
}
//OtherPair 제네릭 타입
public class OtherPair<K,V> {
private K key;
private V value;
public OtherPair(K key, V value) {
this.key=key;
this.value=value;
}
public K getKey() {return this.key;}
public V getValue() {return this.value;}
}
//제네릭 메소드
public class Util {
//Pair 사용
// public static <K,V> V getValue(Pair<K,V> pair, K key) {
// if(pair.getKey()==key) {
// return pair.getValue();
// } else {
// return null;
// }
// }
//ChildPair 사용
public static <P extends Pair<K,V>, K, V> V getValue(P pair, K key) {
if(pair.getKey()==key) {
return pair.getValue();
} else {
return null;
}
}
}
//제네릭 메소드 호출
public class UtilEx {
public static void main(String[] args) {
// TODO Auto-generated method stub
Pair<String, Integer> pair = new Pair<>("홍길동", 35);
Integer age = Util.getValue(pair, "홍길동");
System.out.println(age);
ChildPair<String, Integer> childPair = new ChildPair<>("홍삼원",20);
Integer childAge = Util.getValue(childPair, "홍삼순");
System.out.println(childAge);
/*OtherPair<String,Integer> otherPair = new OtherPair<>("홍삼원", 20);
OtherPair는 Pair를 상속하지 않으므로 예외가 발생합니다.
int otherAge = Util.getValue(otherPair, "홍삼원");
System.out.println(otherAge);*/
}
}