제네릭(Generic, 일반화)
- 클래스 정의 시 사용되어질 데이터타입을 미리 명시하지 않고 객체 사용 전 사용할 타입을 객체 생성 시 명시하여 사용하는 기법
- 주로, Collection API 의 클래스들이 제네릭이 적용되어 있으므로
인스턴스 생성 시 제네릭 타입으로 사용할 데이터타입을 지정
=> 지정된 데이터타입이 클래스 내의 임시 데이터타입을 대체하게 됨
제네릭을 사용한 클래스 정의
- 클래스 정의 시점에서 클래스명 뒤에 <> 기호를 사용해서 "가상의 자료형" 명시
=> 가상의 자료형은 보통 1글자 영문 대문자 사용(주로 E 또는 T 사용)
- 가상의 자료형은 클래스 정의 시점에서 정확한 자료형을 명시하지 않지만 클래스 정의 시점에서 데이터타입 대신 사용 가능함
- 해당 클래스의 인스턴스 생성 시점에서 가상의 자료형을 대신할 실제 자료형을 지정하면 클래스 내의 가상의 자료형이 실제 자료형으로 대체됨
=> 즉, 인스턴스 생성 시점에서 어떤 자료형으로도 변형 가능함!
class GenericClass<T> {
// 클래스 내에서 데이터타입 대신 제네릭 타입 T를 타입으로 지정 가능함
T member; // 멤버변수 member 의 데이터타입이 T 로 지정됨(실제 데이터타입X)
public T getMember() {
return member;
}
public void setMember(T member) {
this.member = member;
}
}
// 제네릭을 적용하지 않는 일반 클래스 정의
// 1) 사용할 데이터타입을 특정 타입으로 관리하는 일반 클래스
class NormalIntegerClass {
int data; // data 변수에는 정수형 데이터만 저장이 가능함
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
}
// 2) 클래스 내에서 사용되어질 데이터에 대한 타입을 Object 타입으로 관리하는 경우
class NormalObjectClass {
// 멤버변수의 데이터타입을 Object 타입으로 선언할 경우
// 자바에서 사용되는 모든 데이터들을 저장할 수 있다!
// => 이 때, 각 데이터들은 Object 타입으로 업캐스팅 되어 저장됨
Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
NormalIntegerClass nic = new NormalIntegerClass();
nic.data = 10; // 정수 저장 가능
// nic.data = 3.14; // 실수 저장 불가
// nic.data = "홍길동"; // 문자열 저장 불가
// => 여러 데이터타입 데이터를 모두 저장하려면 최소한 Object 타입 변수 선언해야 함
NormalObjectClass nc = new NormalObjectClass();
// NormalObjectClass 인스턴스의 멤버변수 data 는 Object 타입이므로
// 어떤 데이터타입의 데이터도 모두 저장이 가능함
// => 단, 객체 내의 데이터를 꺼내서 사용할 때 타입 판별이 필수!
nc.setData(1); // 정수형 데이터
// nc.setData(3.14); // 실수형 데이터
// nc.setData("홍길동"); // 문자열 데이터
// Object 타입을 사용하여 관리하는 데이터는 Object 타입으로 저장 가능
Object o = nc.getData();
// 만약, Object 타입을 실제 데이터타입으로 변환하는 경우
// 잘못된 타입 변환(다운캐스팅)으로 인해 ClassCastException 발생 가능
// String name = (String)o; // 다운캐스팅 필요
// System.out.println("이름 : " + name);
// => String 타입이 아닌 다른 타입 데이터가 저장된 경우 예외 발생하므로
// 변환 전 반드시 instanceof 연산자를 통한 타입 체크 필수!
// => java.lang.ClassCastException: class java.lang.Integer
// cannot be cast to class java.lang.String
package generic;
public class Person {
String name;
int age;
// Alt + Shift + S -> O : 파라미터 생성자
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
// Alt + Shift + S -> S : toString() 오버라이딩
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
제네릭을 사용한 클래스의 인스턴스 생성
// => 클래스명 뒤에 제네릭 타입을 참조 데이터타입으로 명시함
// (int 대신 Integer, char 대신 Character 사용)
// 1. 제네릭 타입 T 를 Integer 타입으로 지정
GenericClass<Integer> gc = new GenericClass<Integer>();
// => GenericClass 내의 타입 T 가 모두 Integer 타입으로 대체됨
gc.setMember(1); // 정수형 데이터 전달 가능하지만
// gc.setMember("홍길동"); // 정수 타입 외에 모두 컴파일 에러 발생!
// => The method setMember(Integer) in the type GenericClass<Integer>
// is not applicable for the arguments (String)
int num = gc.getMember(); // 오토 언박싱 적용
// 2. 제네릭 타입 T 를 Double 타입으로 지정
GenericClass<Double> gc2 = new GenericClass<Double>();
// => 타입 T 가 모두 Double 타입으로 지정
gc2.setMember(3.14);
// gc2.setMember("홍길동");
// 3. 제네릭 타입 T 를 String 타입으로 지정
GenericClass<String> gc3 = new GenericClass<String>();
// => 타입 T 가 모두 String 타입으로 지정
gc3.setMember("홍길동");
// 4. 제네릭 타입 T 를 Person 타입으로 지정
Person p = new Person("홍길동", 20);
GenericClass<Person> gc4 = new GenericClass<Person>();
// => 타입 T 가 모두 Person 타입으로 대체됨
gc4.setMember(p);
System.out.println(gc4.getMember());
Person person = gc4.getMember();
System.out.println(person);
// --------------------------------------------
// 5. 제네릭 타입을 지정하지 않을 경우
GenericClass gc5 = new GenericClass();
// => 타입 T 가 모두 Object 타입으로 대체됨
// 즉, 다시 모든 데이터를 다룰 수 있게 됨
gc5.setMember(1);
gc5.setMember(3.14);
gc5.setMember("홍길동");
gc5.setMember(new Person("이순신", 44));
// => Object 타입 미지정과 지정의 기능상의 차이는 없으나
// 제네릭 타입 자체를 생략할 경우 경고메세지가 표시되므로 제네릭 사용 추천
GenericClass<Object> gc6 = new GenericClass<Object>(); // 경고메세지 미표시됨
// ===========================================================
// 실제 제네릭을 적용하여 정의된 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 연산자 사용 시 제네릭 타입 파라미터 사용 불가!
class GenericClass2<T> {
private T data;
// private static T staticMember; // 컴파일 에러 발생!
// => Cannot make a static reference to the non-static type T
// => 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 메서드에 제네릭 타입 파라미터 사용 불가!
// => Cannot make a static reference to the non-static type T
// => 인스턴스 생성 시점보다 먼저 메모리에 로딩되므로 타입 변경 불가능
public void compare() {
Object o = new Object();
// if(o instanceof T) { // instanceof 연산자에 제네릭타입 파라미터 사용 불가!
// => 컴파일 시점에서 T 의 데이터타입 확인이 불가능하므로
// true, false 를 미리 판별할 수 없으며, 형변환 등의 수행이 불가능!
}
}
package generic;
public class Ex3 {
public static void main(String[] args) {
GenericClass3<Integer> gc; // Integer 는 Number 의 하위타입이므로 지정 가능
// GenericClass3<String> gc2; // Number 계열이 아니므로 지정 불가!
// GenericClass3<Object> gc3; // Number 의 상위타입이므로 지정 불가!
}
}
// ---------------------------------------------------
// 제네릭 타입의 상속과 구현
class Class1<P> {}
interface Interface1<Q> {}
// 부모 타입에 제네릭타입이 지정되어 있을 경우
// 서브클래스에서 상속받을 때 부모의 타입 파라미터를 서브클래스 타입파라미터로 명시해야 한다!
class SubClass<P, Q, R> extends Class1<P> implements Interface1<Q> {
// => Class1<P>, Interface<Q> 를 상속받으려면 최소한 SubClass 뒤에 P 와 Q 를 명시 필수!
// 또한, 서브클래스 자신만의 제네릭 타입도 추가할 수 있다!
P var1; // 슈퍼클래스 Class1 의 타입 P
Q var2; // 슈퍼클래스 Interface1 의 타입 Q
R var3; // 자신의 타입 R
}
// --------------------------------------------------------------------------
/*
* 제네릭 타입에 대한 사용 가능한 파라미터 타입 제한
* - 제네릭 타입 파라미터 선언 시 Object 타입과 그 자식 타입들 모두 사용 가능
* - 필요에 따라 파라미터 타입에 올 수 있는 데이터타입을 제한할 수 있음
*
* < 기본 문법 >
* 파라미터에 대한 서브클래스 타입으로 제한하는 경우
* class 클래스명<타입파라미터 extends 클래스타입> {}
* => 타입파라미터(ex. E 또는 T등)는 extends 뒤의 클래스 타입이거나 하위타입만 지정 가능
*/
//class GenericClass3<E> {} // 타입 파라미터 E는 어떤 타입으로 변경 가능함
class GenericClass3<E extends Number> {}
// => Number 타입 또는 Number 클래스 하위 타입(Integer, Double 등)으로만 변환 가능
TEST
package generic;
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
// Person 클래스 인스턴스 2개 생성 및 참조변수 출력
Person p1 = new Person("홍길동", 20);
Person p2 = new Person("이순신", 44);
System.out.println(p1);
System.out.println(p2);
System.out.println("==============================");
// Peron 객체 여러개를 하나의 객체에 저장하여 관리하는 경우
// 1. Object[] 배열(또는 Person[] 배열)을 통해 관리
// => 생성된 배열의 크기가 불변이므로 추가되는 객체 저장 불가
// => Object 타입으로 업캐스팅 된 객체는 다시 다운캐스팅이 필요함
// (또한, 다운캐스팅 전 타입 체크도 필요함)
// Object[] 배열에 p1, p2 대입
Object[] objArr = {p1, p2}; // Person -> Object 업캐스팅 되어 관리됨
// for문을 통해 배열 객체 내의 객체를 꺼내서 Person 타입 변수에 저장 및 출력
// ex) 출력 형태 : 홍길동, 20
// instanceof 연산자 활용
for(int i = 0; i < objArr.length; i++) {
// Object[] 배열에 저장된 객체를 직접 다룰 경우 참조영역이 축소됨
// System.out.println(objArr[i].name); // Person 타입 변수 접근 불가
// 배열 내의 객체를 꺼내서 Person 타입 변수 저장
// Person p = objArr[i]; // 다운캐스팅 필요
// 다운캐스팅 수행 전 타입 체크 필요
if(objArr[i] instanceof Person) {
Person p = (Person)objArr[i];
System.out.println(p.name + ", " + p.age);
}
}
Person[] pArr = {p1, p2};
for(int i = 0; i < pArr.length; i++) {
Person person = pArr[i];
System.out.println(person.name + ", " + person.age);
}
System.out.println("=======================================");
/*
* 2. Collection API(ex. ArrayList) 를 활용하여
* Person 객체 여러개를 관리할 경우
* => 배열의 단점인 크기 불변을 해결하게 되므로 객체 추가가 자유로움
* 1) 제레릭을 사용하지 않을 경우
* - 파라미터 또는 리턴타입의 데이터타입이 Object 타입이 되어
* 다양한 객체를 저장 가능하게 됨
* - 저장 시점에서 타입 판별이 이루어지지 않으므로 편리하지만
* 대신, 데이터를 꺼내는 시점에서 타입 불일치에 따른 오류 발생 가능
* - 데이터를 꺼내기 전 instanceof 연산자를 통한 타입 판별 후
* Object 타입을 실제 데이터타입으로 다운캐스팅 해야 한다!
*/
// 제네릭을 사용하지 않은 ArrayList 객체 생성
List list = new ArrayList();
// p1, p2 객체 추가
list.add(p1);
list.add(p2);
// 강감찬, 30의 파라미터를 갖는 Person 객체 추가
list.add(new Person("강감찬", 30));
// 문자열 "김우주" 추가
list.add("김우주"); // Object 타입이므로 Person 이 아닌 타입도 추가 가능
// => Person 객체 형태로 꺼내서 사용하는 시점에서 문제가 발생할 수 있다!
// 일반 for문을 통한 객체 출력
// for(int i = 0; i < list.size(); i++) {
//// Person p = list.get(i);
// // => 제네릭 타입을 사용하지 않아 list는 Object 로 되어 있다.
// // => Object 타입을 Person 타입으로 대입하려고 하면 컴파일 에러가 나며
// // 다운캐스팅이 필요함을 알 수 있다.
//// Person p = (Person)list.get(i);
//// System.out.println(p);
// // => 실행을 시키면 83번 라인에서 에러가 발생한다!
// // => java.lang.ClassCastException: class java.lang.String
// // cannot be cast to class generic.Person
// // => "김우주" 의 String 형태는 Person 객체로 변경할 수 없음을 알 수 있다.
// // => 위의 코드 중 "list.add("김우주"); 을 주석처리하고 다시 실행하면 오류가 없어진다!
//
// // Person 타입으로 가져오기 전 타입 판별 후 형변환 수행
// if(list.get(i) instanceof Person) {
// Person p = (Person)list.get(i); // 다운캐스팅
// System.out.println(p);
// System.out.println(p.name + ", " + p.age);
// }
//
// }
// 향상된 for문 사용 시
// 우변의 list 객체에서 꺼낸 객체를 저장할 변수를 좌변에 선언(Object)
for(Object o : list) {
if(o instanceof Person) {
Person p = (Person)o;
System.out.println(p.name + ", " + p.age);
}
}
System.out.println("-----------------------------------------");
/*
* 제네릭 타입을 사용할 경우
* - 저장할 객체의 타입이 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));
// Person 타입이 아닌 객체(데이터) 추가 시
// 데이터 판별 과정에서 오류(컴파일에러) 가 발생하게 된다!
// list2.add("여창현"); // 컴파일 에러 발생!
// => The method add(Person) in the type List<Person>
// is not applicable for the arguments (String)
// 반복문을 통해 List 객체의 모든 요소 꺼내기
for(int i = 0; i < list2.size(); i++) {
// get() 메서드 리턴타입이 Person 타입이 되므로 형변환 불필요!
Person p = list2.get(i);
System.out.println(p.name + ", " + p.age);
}
for(Person p : list2) {
System.out.println(p.name + ", " + p.age);
}
}
}
'⛏️ > JAVA' 카테고리의 다른 글
[JAVA] 38. 예외(Execption) (1) | 2023.11.07 |
---|---|
[JAVA] 37. Collection Framework(컬렉션 프레임워크) (0) | 2023.10.31 |
[JAVA] 35. enum_type (0) | 2023.10.25 |
[JAVA] 34. 날짜 및 시각 정보에 대한 형식화(Formatting) (0) | 2023.10.25 |
[JAVA] 33. java.time 패키지 (0) | 2023.10.25 |