클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법
(매개 변수처럼 동작)
ex 1)
class Person<T>{
public T info;
}
public class GenericDemo {
public static void main(String[] args) {
Person<String> p1 = new Person<String>();
Person<StringBuilder> p2 = new Person<StringBuilder>();
}
}
데이터 타입
p1.info : String
p2.info : StringBuilder
T는 아래 코드의 <> 안에 지정된 데이터 타입에 의해서 결정
클래스를 정의 할 때는 info의 데이터 타입을 확정하지 않고 인스턴스를 생성할 때 데이터 타입을 지정하는 기능이 제네릭이다.
제네릭을 사용하는 이유
class StudentInfo{
public int grade;
StudentInfo(int grade){ this.grade = grade; }
}
class StudentPerson{
public StudentInfo info;
StudentPerson(StudentInfo info){ this.info = info; }
}
class EmployeeInfo{
public int rank;
EmployeeInfo(int rank){ this.rank = rank; }
}
class EmployeePerson{
public EmployeeInfo info;
EmployeePerson(EmployeeInfo info){ this.info = info; }
}
public class GenericDemo {
public static void main(String[] args) {
StudentInfo si = new StudentInfo(2); // StudentInfo(int grade) 생성자
StudentPerson sp = new StudentPerson(si); // StudentPerson(StudentInfo info) 생성자
System.out.println(sp.info.grade); // 2
EmployeeInfo ei = new EmployeeInfo(1); // EmployeeInfo(int rank) 생성자
EmployeePerson ep = new EmployeePerson(ei); // EmployeePerson(EmployeeInfo info) 생성자
System.out.println(ep.info.rank); // 1
}
}
위의 코드는 StudentPerson과 EmployeePerson가 사실상 같은 구조를 가지고 있다. 중복이 발생하고 있는 것이다.
중복을 제거
class StudentInfo{
public int grade;
StudentInfo(int grade){ this.grade = grade; }
}
class EmployeeInfo{
public int rank;
EmployeeInfo(int rank){ this.rank = rank; }
}
class Person{
public Object info;
Person(Object info){ this.info = info; }
}
public class GenericDemo {
public static void main(String[] args) {
Person p1 = new Person("부장");
EmployeeInfo ei = (EmployeeInfo)p1.info;
System.out.println(ei.rank);
// 타입이 안전하지 않다
}
}
클래스 Person의 생성자는 매개변수 info의 데이터 타입이 Object이다. 따라서 모든 객체가 될 수 있다. 그렇기 때문에 위와 EmployeeInfo의 객체가 아니라 String이 와도 컴파일 에러가 발생하지 않는다. 대신 런타임 에러가 발생한다.
person 클래스의 info는 object 이여서 모든 타입이 올 수 있기 때문에 타입을 엄격하게 제한 할 수 없게 되는 것이다.
제네릭화
위에 코드를 제네릭으로 바꿔보겠다.
class StudentInfo{
public int grade;
StudentInfo(int grade){ this.grade = grade; }
}
class EmployeeInfo{
public int rank;
EmployeeInfo(int rank){ this.rank = rank; }
}
class Person<T>{
public T info;
Person(T info){ this.info = info; }
}
public class GenericDemo {
public static void main(String[] args) {
Person<EmployeeInfo> p1 = new Person<EmployeeInfo>(new EmployeeInfo(1));
EmployeeInfo ei1 = p1.info;
System.out.println(ei1.rank); // 성공
Person<String> p2 = new Person<String>("부장");
String ei2 = p2.info;
System.out.println(ei2.rank); // 컴파일 실패
}
}
p1은 잘 동작할 것이다. 중요한 것은 p2다. p2는 컴파일 오류가 발생하는데 p2.info가 String이고 String은 rank 필드가 없는데 이것을 호출하고 있기 때문 ( rank는 int형)
정리하면
컴파일 단계에서 오류가 검출된다.
중복의 제거와 타입 안전성을 동시에 추구할 수 있게 되었다.
제네릭의 특성
복수의 제네릭
클래스 내에서 여러개의 제네릭을 필요로 하는 경우가 있다.
class EmployeeInfo{
public int rank;
EmployeeInfo(int rank){ this.rank = rank; }
}
class Person<T, S>{ // 구분 할 수 있도록 T,S 이름이 달라야한다
public T info;
public S id;
Person(T info, S id){
this.info = info;
this.id = id;
}
}
public class GenericDemo {
public static void main(String[] args) {
Person<EmployeeInfo, int> p1 = new Person<EmployeeInfo, int>(new EmployeeInfo(1), 1);
// int으로 인한 오류 발생
// 래퍼클래스를 사용해야한다. (기본 데이터 타입을 객체 인것처럼 사용하는 클래스)
// int 의 래퍼클래스는 Interger
}
}
복수의 제네릭을 사용할 때는 <T, S>와 같은 형식을 사용한다. 여기서 T와 S 대신 어떠한 문자를 사용해도 된다. 하지만 묵시적인 약속(convention)이 있기는 하다
기본 데이터 타입과 제네릭
class EmployeeInfo{
public int rank;
EmployeeInfo(int rank){ this.rank = rank; }
}
class Person<T, S>{
public T info;
public S id;
Person(T info, S id){
this.info = info;
this.id = id;
}
}
public class GenericDemo {
public static void main(String[] args) {
EmployeeInfo e = new EmployeeInfo(1);
Integer i = new Integer(10);
Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i);
System.out.println(p1.id.intValue());
// 래퍼스클래스가 담고있는 원래의 값을 int형으로 가져온다 = 10
}
}
new Integer는 기본 데이터 타입인 int를 참조 데이터 타입으로 변환해주는 역할
이러한 클래스를 래퍼(wrapper) 클래스라고 한다
제네릭의 생략
제네릭은 생략 가능
EmployeeInfo e = new EmployeeInfo(1);
Integer i = new Integer(10);
Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i);
// 생략을 안한 제네릭
Person p2 = new Person(e, i);
// 생략 한 제네릭
// 2개의 코드는 동일하다.
메소드에 적용
class EmployeeInfo{
public int rank;
EmployeeInfo(int rank){ this.rank = rank; }
}
class Person<T, S>{
public T info;
public S id;
Person(T info, S id){
this.info = info;
this.id = id;
}
public <U> void printInfo(U info){
System.out.println(info);
}
}
public class GenericDemo {
public static void main(String[] args) {
EmployeeInfo e = new EmployeeInfo(1);
Integer i = new Integer(10);
Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i);
p1.<EmployeeInfo>printInfo(e);
p1.printInfo(e);
}
}
제네릭의 제한
extends
제네릭으로 올 수 있는 데이터 타입을 특정 클래스의 자식으로 제한할 수 있다.
abstract class Info{ // 추상 클래스
// 인터페이스여도 상관없다
public abstract int getLevel(); // 선언
}
class EmployeeInfo extends Info{
public int rank;
EmployeeInfo(int rank){ this.rank = rank; }
public int getLevel(){ // 구현
return this.rank;
}
}
class Person<T extends Info>{
public T info;
Person(T info){ this.info = info; }
}
public class GenericDemo {
public static void main(String[] args) {
Person p1 = new Person(new EmployeeInfo(1));
Person<String> p2 = new Person<String>("부장"); // 컴파일 에러 info 의 자식이 아니기 때문
}
}
중요한 부분
class Person<T extends Info>{
Person의 T는 Info 클래스나 그 자식 외에는 올 수 없다.
또한
extends는 상속(extends)뿐 아니라 구현(implements)의 관계에서도 사용할 수 있다
interface Info{
int getLevel();
}
class EmployeeInfo implements Info{
public int rank;
EmployeeInfo(int rank){ this.rank = rank; }
public int getLevel(){
return this.rank;
}
}
class Person<T extends Info>{
public T info;
Person(T info){ this.info = info; }
}
public class GenericDemo {
public static void main(String[] args) {
Person p1 = new Person(new EmployeeInfo(1));
Person<String> p2 = new Person<String>("부장"); // 컴파일 에러 info 의 자식이 아니기 때문
}
}
'Java' 카테고리의 다른 글
컬렉션즈 프래임워크란? (0) | 2023.03.10 |
---|---|
배열과 컬렉션즈 프레임워크 (ArrayList) (0) | 2023.03.10 |
참조 (0) | 2023.03.09 |
상수와 enum (0) | 2023.03.09 |
Object (0) | 2023.03.09 |