제네릭 (Generic, 일반화)
📍 제네릭 (Generic, 일반화)
– 클래스 정의 시 사용할 데이터타입을 미리 명시하지 않고, 객체 사용 전 사용할 타입을 객체 생성 시 명시하는 사용기법.
– 주로 Collection API의 클래스들이 제네릭이 적용되어 있으므로,
인스턴스 생성 시, 제네릭 타입으로 사용할 데이터타입으로 지정.
→ 지정된 데이터타입이 클래스 내의 임시 데이터타입을 대체하게 됨.
EX1 ) 제네릭을 사용하지 않을 경우 |
1. 사용할 데이터타입을 '특정 타입'으로 관리하는 경우
class NormalIntegerClass{
int data;
// 변수 data는 정수형 데이터만 저장 가능
// Geeter & Setter
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
}
– 멤버변수 data에 접근하기 위해 get & set 메서드를 정의함.
• main 메서드에서 실행
NormalIntegerClass nic = new NormalIntegerClass();
nic.data = 10; // 정수 저장 가능
//nic.data = 3.14; // 실수 저장 불가능
//nic.data = "홍길동"; // 문자열 저장 불가
– 여러 데이터타입 데이터를 모두 저장하려면 최소한 Object타입 변수를 선언해야 함.
2. 사용할 데이터타입을 'Object타입'으로 관리하는 경우
class NormalObjectClass{
Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
– 멤버변수의 데이터타입을 Object타입으로 선언할 경우, 자바에서 사용되는 모든 데이터타입을 저장할 수 있음.
→ 이 때, 각 데이터들은 Object타입으로 업캐스팅 되어 저장됨.
• Object타입의 멤버변수 data
NormalObjectClass nc = new NormalObjectClass();
nc.setData(1); // 정수형 데이터
nc.setData(3.14); // 실수형 데이터
nc.setData("홍길동"); // 문자형 데이터
– NormalObjectClass 인스턴스 nc의 멤버변수 data는 Object타입이므로 어떤 데이터타입의 데이터도 모두 저장 가능.
→ 단, 객체 내의 데이터를 꺼내서 사용할 때 타입 판별이 필수 !
• Object타입 저장 및 출력
Object o = nc.getData();
System.out.println(o);
//String name = o;
String name = (String) o;
System.out.println("이름 : " + o);
System.out.println("이름 : " + name);
코드 분석
1, 2 | – Object타입을 사용하여 관리하는 데이터는 Object타입으로 저장 가능. – 마지막으로 해당 변수에 "홍길동" 의 데이터를 저장했으므로 "홍길동" 이 출력됨. |
4~6 | – Object타입을 String타입에 저장할 수 없으므로 Type mismatch 에러 발생. → String타입으로 다운캐스팅 함. – 만약 Object타입을 실제 데이터타입으로 변환하는 경우, 잘못된 타입 변환 (다운캐스팅)으로 인해 ClassCastException 발생 가능. → Stirng타입이 아닌 다른 타입 데이터가 저장된 경우 예외가 발생하므로, 변환 전 반드시 instanceof 연산자를 통한 타입 체크 필수! ⇒ 다운캐스팅 필요 시, instanceof 연산자로 먼저 체크하는게 가장 안전함. |
✓ 실행 결과
홍길동
이름 : 홍길동
이름 : 홍길동
EX2 ) 제네릭을 사용할 경우 |
• 제네릭을 사용한 클래스 정의
– 클래스 정의 시점에서 클래스명 뒤에 <> 기호를 사용하여 "가상의 자료형" 명시.
→ 가상의 자료형은 보통 1글자 영문 대문자 사용 (주로 E 또는 T 사용)
– 가상의 자료형은 클래스 정의 시점에서 정확한 자료형을 명시하지 않지만, 데이터타입 대신 사용 가능함.
– 해당 클래스의 인스턴스 생성 시점에서 가상의 자료형을 대신할 실제 자료형을 지정하면,
클래스 내의 가상의 자료형이 실제 자료형으로 대체됨.
→ 즉, 인스턴스 생성 시점에서 어떤 자료형으로도 변형 가능함.
class GenericClass <T> {
T member;
public T getMember() {
return member;
}
public void setMember(T member) {
this.member = member;
}
}
코드 분석
1 | – 제네릭 타입으로 T 지정. – 클래스 내에서 데이터타입 대신 제네릭 타입 T를 타입으로 지정 가능함. |
3 | – 멤버변수 member의 데이터타입이 T로 지정됨. (실제 데이터타입 X) |
5 | – 리턴타입이 T로 적용됨. |
9 | – 파라미터가 T타입의 member로 적용됨. |
• 제네릭을 사용한 클래스의 인스턴스 생성
– 클래스명 뒤에 제네릭 타입을 참조 데이터타입으로 명시함.
(int 대신 Integer, char 대신 Character 사용)
1. 제네릭 타입 T를 Integer타입으로 지정
GenericClass<Integer> gc = new GenericClass();
gc.setMember(1);
//gc.setMember("홍길동");
int num = gc.getMember();
System.out.println(num); // 1이 출력됨.
– GenericClass내의 타입 T가 모두 Integer타입으로 대체됨.
– Integer타입으로 대체되었으므로 정수형 데이터는 전달 가능하지만 정수 타입 외에 모두 컴파일 에러 발생함.
2. 제네릭 타입 T를 Double타입으로 지정
GenericClass<Double> gc2 = new GenericClass<Double>();
gc2.setMember(3.14);
//gc2.setMember("홍길동");
– 타입 T가 모두 Double타입으로 대체됨.
– Double타입 데이터만 저장 가능, String타입은 저장 불가하므로 에러 발생.
3. 제네릭 타입 T를 String타입으로 지정
GenericClass<String> gc3 = new GenericClass<String>();
gc3.setMember("홍길동");
//gc3.setMember(1);
– 타입 T가 모두 String타입으로 대체됨.
4–1. Person 클래스 생성
public class Person {
String name;
int age;
// 파라미터 생성자
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
// toString() 메서드
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
4–2. 제네릭 타입 T를 Person타입으로 지정
Person p = new Person("홍길동", 20);
GenericClass<Person> gc4 = new GenericClass<Person>();
gc4.setMember(p);
//gc4.setMember("홍길동"); // 에러 발생
System.out.println(gc4.getMember());
Person person = gc4.getMember();
System.out.println(person); // toString() 메서드 생략
코드 분석
2 | – 타입 T가 모두 Person타입으로 대체됨. |
4, 5 | – p의 데이터타입은 Perosn. – 매개변수의 타입이 Person이므로 Person타입만 입력 가능. |
7, 8 | – 리턴타입이 있는 메서드는 직접 출력문에 넣어 출력하거나, 변수에 저장 후 변수를 출력할 수 있음. – 리턴 타입이 Person이므로 Person타입의 변수에 대입 (저장) 가능. – toString() 메서드 생략됨, Person [name=홍길동, age=20] 출력됨. |
9 | – 위와 같은 결과, Person [name=홍길동, age=20] 출력됨. |
5. 제네릭 타입을 지정하지 않을 경우
GenericClass gc5 = new GenericClass(); // 경고 메세지 O
gc5.setMember(1);
gc5.setMember("홍길동");
gc5.setMember(new Person("홍길동", 20));
GenericClass<Object> gc6 = new GenericClass<Object>(); // 경고 메세지 X
코드 분석
1 | – 컴파일 에러가 발생하지 않지만 '경고 표시'가 뜸. – 타입 T가 모두 Object타입으로 대체됨. → 즉, 다시 모든 데이터를 다룰 수 있게 됨. |
3~5 | – 위의 예제에서 본 Object타입 지정과 미지정의 기능상 차이는 없으나, 제네릭 타입 자체를 생략할 경우 경고메세지가 표시되므로 제네릭 사용 추천. |
7 | – 이렇게 제네릭을 사용하면 경고메세지 표시 안됨. |
EX3 ) Collection AIP |
• 실제 제네릭을 적용하여 정의된 Collection AIP 예시
List<String> list = new ArrayList<String>();
– 컬렉션 요소로 사용되는 데이터가 String타입으로 고정됨.
Set<Integer> set = new HashSet<Integer>();
– 컬렉션 요소로 사용되는 데이터가 Integer타입으로 고정됨.
Map<Integer, String> map = new HashMap<Integer, String>();
– 컬렉션 요소 중 키는 Integer, 값은 String타입으로 고정됨.
▸ 제네릭 타입 사용 시 주의사항
1. static 멤버 내에서 제네릭 타입 파라미터 사용 불가
– 제네릭 타입은 인스턴스 생성 시점에서 실제 데이터타입으로 변환되는데,
static 멤버는 인스턴스 생성 시점 보다 먼저 (클래스 로딩 시점) 로딩되므로,
데이터타입이 지정되지 않은 상태이기 때문에 사용이 불가능함.
2. new 연산자 사용 시, 제네릭 타입 파라미터 사용 불가.
3. instanceof 연산자 사용 시, 제네릭 타입 파라미터 사용 불가.
EX ) |
class GenericClass2<T> {
private T data;
// private static T staticMember; // static 멤버 사용 불가
// T instance = new T(); // new 연산자, 인스턴스 생성 불가
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
// public static void staticMethod(T data) {} // static 멤버 사용 불가
public void compare() {
Object o = new Object();
// if(o instanceof T) {} // instanceof 연산자 사용 불가
}
}
코드 분석
4 | 1. static 멤버 내에서 제네릭 타입 파라미터 사용 불가. – Cannot make a static reference to the non-static type T 컴파일 에러 발생. – static 멤버변수에 제네릭 타입 파라미터 사용 불가. → static 멤버는 인스턴스 생성 시점보다 먼저 메모리에 로딩되므로 타입 변경이 불가능함. |
6 | 2. new 연산자 사용 시 제네릭 타입 파라미터 사용 불가. – 인스턴스 생성 (new)시, 제네릭타입 파라미터로 생성자 호출 불가. – 컴파일 시점에서 생성자 타입이 확인 불가능하므로 사용할 수 없음. |
17 | – static 메서드에 제네릭 타입 파라미터 사용 불가. – 인스턴스 생성 시점보다 먼저 메모리에 로딩되므로 타입 변경 불가능. |
22 | 3. instanceof 연산자 사용 시, 제네릭 타입 파라미터 사용 불가 – 컴파일 시점에서 T의 데이터타입 확인이 불가능하므로, true와 false를 미리 판별할 수 없기때문에 형변환 등의 수행이 불가능. |
▸ 제네릭 타입의 상속과 구현
– 슈퍼클래스에 제네릭타입이 지정되어 있는 경우,
서브클래스에서 상속받을 때 부모의 타입 파라미터를 서브클래스 타입 파라미터로 명시해야 함.
– 서브클래스 자신만의 제네릭 타입도 추가할 수 있음.
▸ 제네릭 타입에 대한 사용 가능한 파라미터 타입 제한
– 제네릭 타입 파라미터 선언 시 Object타입과 그 자식 타입을 모두 사용 가능.
– 필요에 따라 파라미터 타입에 올 수 있는 데이터타입을 제한할 수 있음.
▸ 기본 문법
class 클래스명 < 타입파라미터 extends 클래스타입 > { }
– 파라미터에 대한 서브클래스 타입으로 제한하는 경우.
– 타입파라미터 (E 또는 T 등)는 extends 뒤의 클래스타입이거나 하위 타입만 지정 가능.
EX ) 제네릭 타입의 상속과 구현 |
• 슈퍼클래스 정의
class Class1<P> {}
interface Interface1<Q> {}
//class SubClass extends Class1 implements Interface1 {}
class SubClass<R, P, Q> extends Class1<P> implements Interface1<Q> {
P var1; // 슈퍼클래스 Class1의 타입 P
Q var2; // 슈퍼클래스 Interface1의 타입 Q
R var3; // 자신의 타입 R
}
코드 분석
1, 2 | – 슈퍼클래스에 제네릭타입이 지정되어 있는 경우, 서브클래스에서 상속받을 때 부모의 타입 파라미터를 서브클래스 타입 파라미터로 명시해야 함. |
4 | – Class1 is a raw type. References to generic type Class1<P> should be parameterized Interface1 is a raw type. References to generic type Interface1<Q> should be parameterized → Class1 과 Interface1의 파라미터에 각각 P와 Q를 명시해줄것을 권장하므로 경고 표시뜸. |
6 | – SubClass는 Class1<P>와 Interface1<Q>를 상속받으므로 SubClass<P, Q> 명시해야 함. → 제네릭 타입이 적용된 슈퍼클래스 형태를 사용하도록 권장. – 또한, 서브클래스 자신만의 제네릭 타입도 추가할 수 있음. |
• 상속 받는 클래스가 있을 경우, 가능한 제네릭 타입 판별
//class GenericClass3<T> {}
class GenericClass3<T extends Number> {}
GenericClass3<Integer> gc;
//GenericClass3<String> gc;
//GenericClass3<Object> gc3;
코드 분석
1 | – 타입 파라미터 T는 어떤 타입으로도 변경 가능함. |
2 | – Number타입 또는 Number 클래스 하위 타입 (Integer, Double 등)으로만 변환 가능. |
3 | – Integer는 Number의 하위타입이므로 지정 가능. (상속 관계) – 단어 사이에 커서를 두고 F1 누르면 해당 정보 볼 수 있음. → java.lang.Object → java.lang.Number → java.lang.Integer |
4 | – String은 Number 계열이 아니므로 지정 불가. → java.lang.Object → java.lang.String |
5 | – Number의 상위타입이므로 지정 불가. 하위타입만 파라미터타입으로 지정할 수 있음. |
TEST ) |
• Person 클래스 인스턴스 2개 생성 및 참조변수 출력
Person p1 = new Person("홍길동", 20);
Person p2 = new Person("이순신", 44);
System.out.println(p1);
System.out.println(p2);
– 직접 오버라이딩 한 toString() 메서드가 호출되었으나 코드상에는 생략됨.
✓ 실행 결과
Person [name=이순신, age=44]
• Person객체 여러개를 하나의 객체에 저장하여 관리하는 경우
1. Object[] 배열 (또는 Person[] 배열)을 통해 관리
– 생성된 배열의 크기가 불변이므로 추가되는 객체 저장 불가.
– Object타입으로 업캐스팅 된 객체는 다시 다운캐스팅이 필요함.
→ 또한, 다운캐스팅 전 타입 체크도 필요함.
1–1. Object[] 배열에 p1, p2 대입
– Person타입이 Object타입으로 업캐스팅 되어 관리됨.
Object[] objArr = {p1, p2};
1–2. for문을 통해 배열 객체 내의 Person 객체를 꺼내서 Person타입 변수에 저장 및 출력
ex ) 출력 형태 : 홍길동, 20
– 다운캐스팅이 필요하므로 instanceof 연산자 사용할 것.
for(int i=0; i < objArr.length; i++) {
// System.out.println(objArr[i].name);
// Person p = objArr[i];
// 다운캐스팅 필요
// 다운캐스팅 수행 전 타입 체크
if(objArr[i] instanceof Person) {
Person p = (Person) objArr[i];
System.out.println(p.name + ", " + p.age);
}
}
System.out.printlnt("-----------------------------------");
// Person[] 배열 사용
Person[] pArr = {p1, p2};
for(int i=0; i<pArr.length; i++) {
Person person = pArr[i];
System.out.println(person.name + ", " + person.age);
}
코드 분석
2 | – Object[] 배열에 저장된 객체를 직접 다룰 경우, 참조영역이 축소되어 Person타입 변수 접근 불가. → 배열 내의 객체를 꺼내서 Person타입 변수에 저장해야 Person타입 변수에 접근 가능. |
4 | – 배열 내의 객체를 꺼내서 Person타입 변수에 저장. – 다운캐스팅이 필요함. → 하지만 더 안전한 다운캐스팅을 위해 intanceof 연산자 사용 후 다운캐스팅 하기. → 다운캐스팅 수행 전 타입 체크 필요. |
✓ 실행 결과
홍길동, 20
이순신, 44
----------------------------------
홍길동, 20
이순신, 44
2. Collection API (ex. ArrayList)를 활용하여 Person객체 여러개를 관리할 경우
– 배열의 단점인 크기 불변이 해결되므로 객체 추가가 자유로움.
2–1. 제네릭 사용 X
– 파라미터 또는 리턴타입의 데이터타입이 Object타입이 되어 다양한 객체를 저장가능하게 됨.
– 저장 시점에서 타입 판별이 이루어지지 않으므로 편리하지만,
대신 데이터를 꺼내는 시점에서 타입 불일치에 따른 오류 발생 가능.
– 데이터를 꺼내기 전 instanceof 연산자를 통한 타입 판별 후, Object타입을 실제 데이터타입으로 다운캐스팅 해야 함.
List list = new ArrayList();
list.add(p1);
list.add(p2);
list.add(new Person("강감찬", 30));
// list.add("박보검");
코드 분석
1 | – 제네릭을 사용하지 않은 ArrayList 객체 생성. |
3~5 | – List에 add() 메서드를 호출하여 위에서 생성한 p1, p2 객체 추가. – 강감찬, 30의 파라미터를 갖는 Person 객체 추가. → Person 클래스 정의 당시 생성자로 파라미터 생성자를 정의함. |
7 | – 문자열 "박보검" 추가. – Object타입이므로 Person이 아닌 타입도 추가 가능. – 그러나 Person 객체 형태로 꺼내서 사용하는 시점에서 문제가 발생할 수 있음. → 결국 for문에서 에러 발생하므로 주석 처리함. |
• 일반 for문을 통한 객체 출력
for(int i=0; i < list.size(); i++) {
// Person p = list.get(i); // 다운캐스팅 필요
Person p = (Person) list.get(i);
System.out.println(p);
// Object o = list.get(i);
// System.out.println(o);
// 타입 판별 후 형변환 수행
if(list.get(i) instanceof Person){
// Person p = list.get(i);
Person p = (Person)list.get(i);
System.out.println(p);
System.out.println(p.name + ", " + p.age);
}
}
코드 분석
1 | – list의 크기는 length() 메서드가 아닌 size() 메서드로 알 수 있음. |
3 | – 제네릭 타입을 사용하지 않아 list는 Object타입으로 되어 있음. – Object타입을 Person타입으로 대입하려고 하면 컴파일 에러가 발생하며, 다운캐스팅이 필요함. |
5, 6 | – 실행하면 java.lang.ClassCastException 에러 발생. – "박보검"의 String 형태는 Person 객체로 변경할 수 없음. – 위의 코드 중 list.add("박보검"); 을 주석처리하고 다시 실행하면 에러가 없어짐. – line 14의 변수 p와 중복되므로 주석처리 함. |
8, 9 | – Object타입으로 객체를 저장할 경우 Person타입 멤버 접근 불가. |
12~17 | – Person타입으로 가져오기 전 타입 판별 후 형변환 수행. |
13 | – Object → Person 다운캐스팅 필요하므로 다운캐스팅 함. |
✓ 실행 결과
Person [name=이순신, age=44]
이순신, 44
Person [name=강감찬, age=30]
강감찬, 30
• 향상된 for문을 통한 객체 출력
– 우변의 list 객체에서 꺼낸 객체를 저장할 변수를 좌변에 선언 (Object)
for(Object o : list) {
if(o instanceof Person) {
Person p = (Person) o;
System.out.println(p.name + ", " + p.age);
}
}
✓ 실행 결과
이순신, 44
강감찬, 30
2–2. 제네릭 사용 O
– 저장할 객체의 타입이 Person타입이므로 제네릭 타입 <Person> 지정.
– 객체 저장 시점에서 Person타입 객체만 저장 가능하도록 자동 판별.
즉, 잘못된 객체 (또는 데이터)가 저장될 우려가 없음.
– 또한, 제네릭 타입 Person타입으로 고정되므로 Object타입으로 업캐스팅이 일어나지 않음.
→ 데이터를 꺼내는 시점에서도 Person타입 그대로 사용 가능.
→ 별도의 타입 체크 또는 다운캐스팅 작업 불필요.
• <Person> 제네릭을 사용하는 list2 ArrayList 생성
List<Person> list2 = new ArrayList<Person>();
list2.add(p1);
list2.add(p2);
list2.add(new Person("강감찬", 30));
//list2.add("박보검");
– Person타입이 아닌 객체 (데이터) 추가 시, 데이터 판별 과정에서 컴파일 에러가 발생함.
• 방법1 ) for문을 사용하여 List의 모든 요소에 접근
for(int i=0; i < list2.size(); i++) {
Person p = list2.get(i);
System.out.println(p.name + ", " + p.age);
}
– get() 메서드 리턴타입이 Person타입이 되므로 형변환 불필요.
✓ 실행 결과
이순신, 44
강감찬, 30
• 방법2 ) forEach문을 사용하여 List의 모든 요소에 접근
for(Person p : list2) {
System.out.println(p.name + ", " + p.age);
}
✓ 실행 결과
홍길동, 20
이순신, 44
강감찬, 30