제네릭(Generic)이란?

wnajsldkf·2022년 6월 27일
0

Java

목록 보기
4/19
post-thumbnail

이 글은 생활코딩 제네릭 강의를 기반으로 작성하였습니다.

제네릭이란?

제네릭(Generic)은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 방법을 말한다.

이 코드는 p1.info의 데이터 타입을 String으로, p2.info의 데이터 타입은 StringBuilder로 지정한다.

좀 더 이해해보기 위해 코드를 살펴보겠다.

class Person <T>{
	public T info;
}

클래스를 선언하는 부분을 보면 info의 데이터 타입이 T이다. 하지만 T라는 데이터 타입은 존재하지 않는다. 이는 아래 코드에서 <>사이에 지정된 데이터 타입에 의해 결정된다.

Person<String> p1 = new Person<String>();

Person<String> p1은 변수 p1의 데이터 타입을 지정하고, new Person<String>();은 인스턴스를 생성한다.

이처럼 제네릭은 데이터 타입을 확정하지 않고 인스턴스를 생성할 때 데이터 타입을 지정하는 기능을 한다.

제네릭 왜 사용할까?

타입 안전성

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
	}
}

위 코드를 보면 StudentInfoEmployeePerson은 사실 같은 구조를 가진다. 즉 중복이 발생하고 있는 것이다.

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);
	}
}

StudentInfoEmployeeInfo를 하나의 Person 클래스로 합친다.

이제 info 변수의 타입을 지정한다. EmployeeInfoStudentInfo 클래스를 대체할 수 있는 것을 찾아야하는데, 모든 클래스의 상위인 Object를 사용한다. 이렇게 되면 info에 어떤 클래스든 지정할 수 있다.

그런데 사실 이 코드에는 문제가 있다. IDE에서 오류가 보이지 않아 성공적으로 컴파일된 것처럼 보이지만, 실행하면 오류가 발생한다.

아래의 코드를 보자.

Person p1 = new Person("부장");

info의 데이터 타입이 Object이므로 모든 객체가 들어갈 수 있다. 따라서 infoEmployeeInfo 객체가 아닌 String이 와도 컴파일 에러가 발생하지 않는 것이다. 문법적으로 오류가 없어보이지만 코드의 취지, 설계에는 부합하지 않다.

Java는 컴파일 언어와 인터프리터 언어를 혼합한 형태인 하이브리드 언어이다. 컴파일 언어라는 장점을 살려 모든 에러가 컴파일 단계에서 발생해야하지만 그렇지 않다. 에러가 던져지는 런타임 단계는 실제로 애플리케이션이 동작하고 있으므로 심각한 문제가 발생할 수 있는 것이다.

이를 모든 타입이 올 수 있기 때문에, 타입에 대해 안전하지 않다고 한다.

제네릭화 시켜볼까?

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은 제네릭을 이용해서 외부에서 p1의 타입을 정확하게 지정했기 때문에 잘 동작한다.

반면 p2는 컴파일 오류가 발생한다. p2.infoString인데 Stringrank필드가 없기 때문이다.

이처럼 Generic을 사용하면 1) 컴파일 단계에서 오류를 검출할 수 있고, 2) 중복의 제거와 타입 안전성을 동시에 추구할 수 있다.

제네릭 특징

복수의 제네릭 만들기

클래스 내에서 여러개의 제네릭 필요로 할 수 있다. 이때는 서로 다른 이름으로 , 를 사용해서 나타낼 수 있다.

+) 제네릭을 나타낼 때 T, S 문자들을 자주 볼 수 있는데, 제네릭도 컨벤션이 있다고 한다.

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;
	}
}

제네릭에서 기본 데이터 타입 사용하기

제네릭은 참조 데이터 타입만 사용할 수 있고, 기본 데이터 타입(int, char…)는 사용할 수 없다. 기본 데이터 타입을 사용하려면 Wrapper를 사용해야 한다.

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 id = new Integer(10);
		Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i);
		System.out.println(p1.id.intValue());
	}
}

Integer id = new Integer(10); 는 기본 데이터 타입인 int를 참조 데이터 타입으로 변환한다. 이러한 클래스를 wrapper 클래스라고 한다.

만약 id를 참조 데이터 타입인 String으로 변환했다면 String 형태의 id를 사용할 수 있다.

제네릭 생략하기

p1p2는 동일하게 동작하는데, 이미 ei의 데이터 타입을 알기 때문이다.

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 p2 = new Person(e, i);
	}
}

메서드에 적용하기

제네릭은 메서드 단계에서도 사용할 수 있다. 다음은 제네릭을 설명한 예시인데 코드로만 봐서 잘 이해가 가지 않기 때문에 그림으로 나타내겠다.

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 id = new Integer(10);
		Person p1 = new Person(e, i);
		p1.<EmployeeInfo>printInfo(e);
	}
}

제네릭의 제한

제네릭은 데이터 타입을 인스턴스화 할 때 지정하기 때문에 제네릭으로 들어갈 수 있다. 이를 제한할 수 있는 방법은 무엇일까?

extends(상속)

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>("부장");
	}
} 

class Person<T extends Info>{PersonTInfo 클래스나 그 자식 외에는 올 수 없다. 따라서 p2에서 StringInfo의 자식이 아니기 때문에 에러가 발생한다. 즉 컴파일 에러가 발생해서 런타임 때 발생하는 오류를 미연에 감지할 수 있다.

implements(구현)

제너릭 제한은 interface 관계에서도 사용할 수 있다.

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>("부장");
    }
}
profile
https://mywnajsldkf.tistory.com -> 이사 중

0개의 댓글