이전 포스팅을 통해
기본적인 제네릭의 개념을 알아보았다.
이번에는 제네릭 심화 개념을
파헤쳐 볼까한다.
제네릭을 사용하면 타입 파라미터로 모든 클래스를 넣어줄 수 있다. 이렇게 모든 타입이 가능하다는 말은 프로그램에 있어 좋은 말이 아니다. 다양한 타입이 들어오면서 데이터의 정체성을 훼손시킬 수 있다. 예를들면 어떤 제네릭 클래스는 숫자 타입만 사용할 목적으로 만든 클래스지만 문자열 클래스가 들어가도 컴파일 과정에서 어떤 오류도 나지 않는다.
그래서 제네릭은 '제약'을 걸어줄 수 있다. 제약의 기준은 클래스의 상하관계다. 상한선과 하한선을 걸어주는 것이다.
여기서 방법이 두 가지가 있다.
1. 타입 파라미터 < T > 이용 ( extends (상한선) )
2. 와일드 카드 < ? > 이용 ( extends(상한선) , super (하한선) )
< T extends 상위 타입 >은 주로 제네릭 클래스나 제네릭 메소드 생성시, 타입의 상한선을 지정해주기 위해 주로 사용된다. 예를 들면, Number의 하위 클래스만 타입 파라미터로 들어올 수 있는 클래스를 만들려고 할 때, < T extends Number >를 통해 타입에 상한선을 둘 수 있다. 이렇게 하면 객체 생성시, 타입 파라미터를 String으로 주면 컴파일 오류가 난다.
< ? extends 상위 타입 or 하위 타입 >은 주로 특정 메소드에 접근시, 타입의 상한선과 하한선을 지정해주기 위해 주로 사용된다. 즉 메소드에 매개변수로 접근하거나 return 시, 일종의 거름망 역할을 하는 것이다.
public <T> void register(Course<? extends Person> course, T t)
제네릭 메소드의 매개변수에 와일드카드가 사용되었다. course 객체가 register 메소드 매개변수에 접근하려면 course 객체의 타입 파라미터가 Person의 하위 클래스여야 한다. 이처럼 메소드의 입구와 출구에서 타입 파라미터에 상한선과 하한선을 두어, 객체의 접근에 제약을 둘 때 와일드 카드가 사용된다.
< T extends 최상위 타입 >
extends 키워드는 상한선을 지정해준다.
< 제네릭 메소드 클래스 >
package genericstudy;
public class Util {
// 제네릭 메소드 생성 : T는 Number Class가 상한선
public <T extends Number> int compare(T t1, T t2) {
// T로 Number클래스의 자식클래스만 올 수 있음
// 가장 큰 범위의 double형으로 바꾸어 데이터 손실 방지
double v1 = t1.doubleValue(); // double형으로 바꾸기
double v2 = t2.doubleValue(); // double형으로 바꾸기
return Double.compare(v1, v2); // 두 값 비교 같으면 1 다르면 -1
}
}
< Main 클래스 >
package genericstudy;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
Util util = new Util();
int result1 = util.compare(20, 30); // t1 < t2 이면 return -1
int result2 = util.compare(4.5, 2.0); // t1 > t2 이면 return 1
int result3 = util.compare(4.0, 4); // t1 == t2 이면 reuturn 0
System.out.println(result1); // 출력 : -1
System.out.println(result2); // 출력 : 1
System.out.println(result3); // 출력 : 0
}
}
T를 extends를 통해 Number 클래스로 상한선을 두었기 때문에 Number 클래스 본인과 자식클래스 외에는 파라미터로 들어가지를 못한다.
이처럼 문자열을 넣어주면 바로 오류 표시가 나온다.
와일드 카드<?>
위에서는 extends를 이용하여 상한선만 정해보았지만 이번에는 ? (와일드 카드)를 이용하여 상한선, 하한선을 설정하는 방법을 알아보겠다. 매개값이나 리턴타입으로 ?(와일드카드)를 사용하면 3가지 형태로 사용이 가능하다.
< ? > : 전체 => 전체 타입 가능
<? extends 상위타입 A > : 상한선 => A 자식클래스까지만 가능
< ? super 하위 타입 B > : 하한선 => B 조상클래스만 가능
그럼 코드로 한번 살펴보자. 강의 등록 시스템을 만들어 보았다.
<Main 클래스>
package wildgenericstudy;
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
// 강좌 개설
Course<Person> personCourse = new Course("일반인과정");// 일반인 강좌 개설
Course<Worker> workerCourse = new Course("직장인과정"); // 직장인 강좌 개설
Course<Student> studentCourse = new Course("학생과정");// 학생 강좌 개설
Course<HighStudent> highStudentCourse = new Course("고등학생과정");// 고등학생 강좌 개설
// 강의 관리 시스템 객체 생성
CourseSystem cs = new CourseSystem();
// 학생 생성
Person person1 = new Person("김지현"); //일반인
Person person2 = new Person("오수진"); //일반인
Student student1 = new Student("이시영"); // 학생
Worker worker1 = new Worker("차인하"); // 직장인
Worker worker2 = new Worker("차승현"); // 직장인
HighStudent highStd1 = new HighStudent("김새롬"); // 고등학생
// 학생 강좌에 등록
cs.register(personCourse, person1); //일반인 강좌 등록
cs.register(personCourse, person2); // 일반인 강좌 등록
cs.register(studentCourse, student1);// 학생 강좌 등록
cs.register(workerCourse, worker1); // 직장인 강좌 등록
cs.register(workerCourse, worker2); // 직장인 강좌 등록
cs.register(highStudentCourse, highStd1); // 고등학생 강좌 등록
// 출력
CourseSystem.printCourse(personCourse); // 일반인 강좌 수강생 출력
CourseSystem.printCourse(studentCourse); // 학생 강좌 수강생 출력
CourseSystem.printCourse(highStudentCourse); // 고등학생 강좌 수강생 출력
CourseSystem.printCourse(workerCourse); // 직장인 강좌 수강생 출력
}
}
< 강의 클래스 >
package wildgenericstudy;
import java.util.ArrayList;
import java.util.List;
public class Course<T> {
private String name; // 강의 이름
private List<T> studentList; // 해당 강의를 신청한 학생 리스트
public Course(String name) {
this.name = name;
studentList = new ArrayList();// 학생 리스트 생성
}
public String getName() {
return name; // 해당 강의 이름
}
public List<T> getStudents(){
return studentList; // 해당 강의 학생리스트 접근주소
}
}
< 강의 관리 시스템 클래스 >
package wildgenericstudy;
import java.util.Arrays;
import java.util.List;
public class CourseSystem{
// 강의 등록 (거름망)
public <T> void register(Course<? extends Person> course, T t) {
// Person보다 큰 상위타입을 가진 Course는 들어올 수없음
List<T> students;
students = (List<T>)course.getStudents(); // 강의 학생리스트에 접근
//제네릭 타입 자료구조는 실행되기 전까지는 어떤 타입이 들어올지 모른다.
//그러므로 컴파일 과정에는 Object로 설정되어 있음 : List<Object>
//여기서는 Course 객체의 상한선을 Person으로 제한에 두었다.: List<Person>
//List<T>는 List<Person>보다 하위타입
//그러므로 (List<T>)로 캐스팅을 해주어야한다.
students.add(t);// 리스트에 학생 추가
}
// 현재 등록한 학생리스트 출력 (거름망)
// < ? > : Object부터 말단 클래스까지 모든타입의 Course 가능
public static void printCourse(Course<?> course) {
System.out.println(course.getName()+ " 수강생 : "+ course.getStudents().toString());
}
// < ? extends Student > : Student가 상한선, Student 포함 하위 타입을 가진 Course만 가능
public static void printCourseStudent(Course<? extends Student> course) {
System.out.println(course.getName()+ " 수강생 : "+ course.getStudents().toString());
}
// < ? super Worker > : Worker가 하한선, Worker 포함 상위 타입을 가진 Course만 가능
public static void printCourseWorker(Course<? super Worker> course) {
System.out.println(course.getName()+ " 수강생 : "+course.getStudents().toString());
}
}
< Person 클래스, 상위 클래스 >
package wildgenericstudy;
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
// 리스트.toString() 시 재정의된 이름이 출력
@Override
public String toString() { //Object 클래스 메소드
return name;
}
}
< Worker 클래스, 하위 클래스 >
package wildgenericstudy;
public class Worker extends Person {
private String name;
public Worker(String name) {
super(name);
this.name = name;
}
}
< Student 클래스, Person의 하위 클래스이자 HighStudent의상위클래스>
package wildgenericstudy;
public class Student extends Person {
private String name;
public Student(String name) {
super(name);
this.name = name;
}
}
< HighStudent 클래스, Student의 하위 클래스 >
package wildgenericstudy;
public class HighStudent extends Student {
private String name;
public HighStudent(String name) {
super(name);
this.name = name;
}
}
<출력 결과 >
이처럼 와일드 카드(?) 를 이용하면 제네릭으로 들어오는 타입에 제한을 걸어둘 수가 있어, 원하지 않는 타입이 들어오는 것을 방지할 수 있다.
상속
제네릭 클래스도 클래스이니 상속이 가능하다. 자식 클래스는 부모 클래스의 메소드와 필드를 상속받으니 자식도 부모와 같은 제네릭 타입을 가져야한다.
< 부모 클래스 >
package inheritancegeneric;
public class Parent<T, M> {
T t;
M m;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
public M getM() {
return m;
}
public void setM(M m) {
this.m = m;
}
}
<자식 클래스>
package inheritancegeneric;
public class Child<T,M,C> extends Parent<T,M> {
// T,M은 필수 C는 선택
C c;
public C getC() {
return c;
}
public void setC(C c) {
this.c = c;
}
}
< Main 클래스 >
package inheritancegeneric;
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
Child<Integer, String, Double> c1 = new Child();
c1.setT(100);
c1.setM("Hello");
c1.setC(345.6);
System.out.println(c1.getT());
System.out.println(c1.getM());
System.out.println(c1.getC());
}
}
정리
1. 제네릭에 와일드 카드를 사용하면 상한선, 하한선을 설정해 줄 수 있다.
2. 제네릭 클래스도 상속이 가능하다.
참고자료
'JAVA > JAVA Basic' 카테고리의 다른 글
[ JAVA ] 문자 인코딩(Character Encoding)이란? (0) | 2021.07.05 |
---|---|
[ JAVA ] 바이트 스트림 vs 문자 스트림 (0) | 2021.06.30 |
[ JAVA ] 제네릭 (Generic) (0) | 2021.06.20 |
[ JAVA ] 스레드(Thread) 동기화5 (Implicit Lock vs Explicit Lock ) (0) | 2021.06.20 |
[ JAVA ] 스레드(Thread) 동기화4 ( yield(), join() ) (0) | 2021.06.20 |