Generic이란?
클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법이다.
예시1
class Person<T> {
public T info;
}
Person<String> p1 = new Person<String>();
Person<StringBuilder> p2 = new Person<StringBuilder>();
Person의 Info를 String으로 받을 수도 있고, StringBuilder로 받을 수도 있다.
따라서 p1의 타입은 Person 클래스 타입이고 내부에서 사용하는 제네릭 변수의 타입이 String이라는 뜻이다.
Generic이 필요한 이유
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);
StudentPerson sp = new StudentPerson(si);
System.out.println(sp.info.grade); // 2
EmployeeInfo ei = new EmployeeInfo(1);
EmployeePerson ep = new EmployeePerson(ei);
System.out.println(ep.info.rank); // 1
}
}
Object 사용
위 코드에서 Student와 Employee는 매우 유사한 구조를 가졌다.
중복을 줄이고 하나의 Person 클래스로 만들고 싶다면 아래와 같이 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{
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);
}
}
하지만 Object로 타입이 불일치하는 코드 작성 시 런타임에는 ClassCastException이 발생하지만, 컴파일 시에는 에러를 발견하기 힘들다.
런타임에러는 찾기도 힘들고 심각한 문제를 초래할 수 있기 때문에 이러한 방식은 위험하고, 모든 타입이 올 수 있기 때문에 타입을 엄격하게 제한할 수도 없다.
Generic 사용
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); // 컴파일 실패
}
}
제네릭을 사용하면 중복을 제거하여 가독성 높은 코드를 짤 수 있고, 컴파일 타임에 타입에러를 발견할 수 있어 타입 안정성이 확보된다.
클래스 내에서 여러 개의 제네릭이 필요하다면?
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);
//Person p1 = new Person(e, i); //꺽쇄 괄호는 생략이 가능하다.
System.out.println(p1.id.intValue());
}
}
여러 개의 제네릭이 필요할 때는 T,S 처럼 다른 문자를 명시해서 구분한다.
또한 제네릭은 참조형만 올 수 있다. 즉 기본 데이터 타입인 int 대신 Integer를 사용해야한다.
제네릭 메소드
public <U> void printInfo(U info){
System.out.println(info);
}
매개변수에도 제네릭을 사용할 수 있다. 이 때 메서드의 접근제어자와 리턴타입 사이에 <U>라고 적어주면 되고, 사용은 일반 메서드와 같이 사용할 수 있다.
제한
제네릭은 어떤 타입이든 들어올 수 있기 때문에 아무 타입이나 다 들어올 수 있다는 문제가 있다.
이러한 문제를 해결하기 위해 extends를 사용해서 특정 클래스를 상속받은 클래스 타입만 들어올 수 있도록 제한할 수 있다.
반대로 특정 클래스의 부모 클래스로 제한하고 싶을 때는 <T super EmployeeInfo>와 같이 사용할 수 있다.
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>("부장");
}
}
제네릭 클래스 내부에서 extends로 상속받은 클래스의 메서드를 호출할 수 있지만, extends를 명시하지 않았다면, 사용할 수 없다.
와일드 카드
와일드 카드는 <?>로 표현하며 단순히 <?>만 사용했을 때는 Object로 받는 것과 같다.
따라서 제네릭과 같이 extends와 super로 타입을 제한할 수 있다.
제네릭 메서드는 다음과 같이 타입에 종속적인 메서드를 선언해놓고 외부에서 호출할 때 타입이 틀리다면, 컴파일 에러를 발생해주기 때문에 타입에 안정적이다. 하지만 매개변수의 타입을 알 수 없는 상황이라면? 제네릭을 사용하기 애매하다.
public <T extends Number> double addNumbers(T a, T b) {
return a.doubleValue() + b.doubleValue();
}
이런 상황에서 와일드 카드를 사용하면 다음과 같이 Person을 상속받는 클래스들에 대해 출력하는 메서드를 정의할 수 있다.
하지만 위 제네릭의 예시처럼 doubleValue()와 같은 타입에 종속적인 메서드를 메서드 내부에서 사용한다면, 컴파일 시점에는 걸리지 않지만 런타임 시점에 에러가 발생할 수 있다.
public void printListContents(List<? extends Person> list) {
for (Object item : list) {
System.out.println(item);
}
}
Reference
https://opentutorials.org/module/516/6237
https://mangkyu.tistory.com/241
자바의 정석
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!