Java

제네릭

제네릭이란?

제네릭(Generic)은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다. 말이 어렵다. 아래 그림을 보자.

위의 그림은 아래의 코드를 간략화한 것이다.

package org.opentutorials.javatutorials.generic;

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와 p2.info의 데이터 타입은 결과적으로 아래와 같다.

  • p1.info : String
  • p2.info : StringBuilder

그것은 각각의 인스턴스를 생성할 때 사용한 <> 사이에 어떤 데이터 타입을 사용했느냐에 달려있다. 

클래스 선언부를 보자.

public T info;

클래스 Person의 필드 info의 데이터 타입은 T로 되어 있다. 그런데 T라는 데이터 타입은 존재하지 않는다. 이 값은 아래 코드의 T에서 정해진다.

class Person<T>{

위 코드의 T는 아래 코드의 <> 안에 지정된 데이터 타입에 의해서 결정된다. 

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

위의 코드를 나눠보자. 아래 코드는 변수 p1의 데이터 타입을 정의하고 있다.

Person<String> p1

아래 코드는 인스턴스를 생성하고 있다. 

new Person<String>();

즉 클래스를 정의 할 때는 info의 데이터 타입을 확정하지 않고 인스턴스를 생성할 때 데이터 타입을 지정하는 기능이 제네릭이다. 

제네릭이 무엇인가를 알았으니까 이제 제네릭을 사용하는 이유를 알아보자. 

제네릭을 사용하는 이유

타입 안전성

아래 코드를 보자.

package org.opentutorials.javatutorials.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
	}
}

그리고 아래 코드를 보자. 위의 코드는 StudentPerson과 EmployeePerson가 사실상 같은 구조를 가지고 있다. 중복이 발생하고 있는 것이다. 중복을 제거해보자.

package org.opentutorials.javatutorials.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{
	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);
	}
}

위의 코드는 성공적으로 컴파일된다. 하지만 실행을 하면 아래와 같은 오류가 발생한다.

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to org.opentutorials.javatutorials.generic.EmployeeInfo
    at org.opentutorials.javatutorials.generic.GenericDemo.main(GenericDemo.java:17)

아래 코드를 보자.

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

클래스 Person의 생성자는 매개변수 info의 데이터 타입이 Object이다. 따라서 모든 객체가 될 수 있다. 그렇기 때문에 위와 EmployeeInfo의 객체가 아니라 String이 와도 컴파일 에러가 발생하지 않는다. 대신 런타임 에러가 발생한다. 컴파일 언어의 기본은 모든 에러는 컴파일이 발생할 수 있도록 유도해야 한다는 것이다. 런타임은 실제로 애플리케이션이 동작하고 있는 상황이기 때문에 런타임에 발생하는 에러는 항상 심각한 문제를 초래할 수 있기 때문이다. 

위와 같은 에러를 타입에 대해서 안전하지 않다고 한다. 즉 모든 타입이 올 수 있기 때문에 타입을 엄격하게 제한 할 수 없게 되는 것이다. 

제네릭화

이것을 제네릭으로 바꿔보자.

package org.opentutorials.javatutorials.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); // 컴파일 실패
	}
}

p1은 잘 동작할 것이다. 중요한 것은 p2다. p2는 컴파일 오류가 발생하는데 p2.info가 String이고 String은 rank 필드가 없는데 이것을 호출하고 있기 때문이다. 여기서 중요한 것은 아래와 같이 정리할 수 있다.

  • 컴파일 단계에서 오류가 검출된다.
  • 중복의 제거와 타입 안전성을 동시에 추구할 수 있게 되었다.

제네릭의 특성

복수의 제네릭

클래스 내에서 여러개의 제네릭을 필요로 하는 경우가 있을 것이다. 예제를 보자.

package org.opentutorials.javatutorials.generic;
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) {
		Person<EmployeeInfo, int> p1 = new Person<EmployeeInfo, int>(new EmployeeInfo(1), 1);
	}
}

위의 코드는 예외를 발생시키지만 문제는 다음 예제에서 처리하고 형식만 보자. 

즉, 복수의 제네릭을 사용할 때는 <T, S>와 같은 형식을 사용한다. 여기서 T와 S 대신 어떠한 문자를 사용해도 된다. 하지만 묵시적인 약속(convention)이 있기는 하다. 그럼 예제의 오류를 해결하자.

기본 데이터 타입과 제네릭

제네릭은 참조 데이터 타입에 대해서만 사용할 수 있다. 기본 데이터 타입에서는 사용할 수 없다. 따라서 아래와 같이 코드를 변경한다.

package org.opentutorials.javatutorials.generic;
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());
	}
}

new Integer는 기본 데이터 타입인 int를 참조 데이터 타입으로 변환해주는 역할을 한다. 이러한 클래스를 래퍼(wrapper) 클래스라고 한다. 덕분에 기본 데이터 타입을 사용할 수 없는 제네릭에서 int를 사용할 수 있다.

제네릭의 생략

제네릭은 생략 가능하다. 아래 두 개의 코드가 있다. 이 코드들은 정확히 동일하게 동작한다. e와 i의 데이터 타입을 알고 있기 때문이다.

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

메소드에 적용

제네릭은 메소드에 적용할 수도 있다. 

package org.opentutorials.javatutorials.generic;
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

제네릭으로 올 수 있는 데이터 타입을 특정 클래스의 자식으로 제한할 수 있다.

package org.opentutorials.javatutorials.generic;
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>{

즉 Person의 T는 Info 클래스나 그 자식 외에는 올 수 없다.

extends는 상속(extends)뿐 아니라 구현(implements)의 관계에서도 사용할 수 있다.

package org.opentutorials.javatutorials.generic;
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>("부장");
	}
}

이상으로 제네릭의 기본적인 사용법을 알아봤다. 

댓글

댓글 본문
  1. labis98
    20230204 수강완료.
  2. wwwqiqi
    몰랐던것들을 많이 알게됐습니다 완료
  3. Alan Turing
    22/10/11
  4. PassionOfStudy
    복습 8일차!
  5. PassionOfStudy
    제네릭!
  6. 코드파괴자
    22.05.09 Form Ride. Generic..

    (이론만 ok, 제네릭 활용 문법 연습 필요)
  7. aesop0207
    22.03.08. Tue.
    다시 봐야할 듯
  8. 모찌말랑카우
    22.03.02
  9. 드림보이
    2021.12.29. 제네릭 파트 수강완료
  10. syh712
    2021-12-15
    <제네릭>
    1. 제네릭이란?
    - 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법
    - 클래스를 정의 할 때는 info의 데이터 타입을 확정하지 않고 인스턴스를 생성할 때 데이터 타입을 지정하는 기능이 제네릭

    2. 제네릭을 사용하는 이유
    컴파일 단계에서 오류가 검출된다.
    중복의 제거와 타입 안전성을 동시에 추구

    3. 제네릭의 특성
    - 복수의 제네릭: 복수의 제네릭을 사용할 때는 <T, S>와 같은 형식을 사용한다. 여기서 T와 S 대신 어떠한 문자를 사용해도 된다. 하지만 묵시적인 약속(convention)이 있기는 하다.- 기본 데이터 타입과 제네릭: 제네릭은 참조 데이터 타입에 대해서만 사용할 수 있다. 기본 데이터 타입에서는 사용할 수 없다. new Integer는 기본 데이터 타입인 int를 참조 데이터 타입으로 변환해주는 역할을 한다. 이러한 클래스를 래퍼(wrapper) 클래스라고 한다.

    4. 제네릭의 생략
    5. 메소드에 적용
    6. 제네릭의 제한
    - extends
    제네릭으로 올 수 있는 데이터 타입을 특정 클래스의 자식으로 제한할 수 있다.
  11. H4PPY
    1122
  12. IaaS
    2021.11.04 수강완료

    제네릭은 몇번씩 들어봐야겠네요
  13. 노오오오옹
    많이 어렵네요.. 충분히 이해될 때까지 몇번 더 봐야겠습니다!
  14. 김수빈
    p1.info 데이터타입이 오브젝트라서 그렇습니다.
    오브젝트는 모든 클래스의 부모격이라 명시적 형변환이 가능한 것 같습니다.
    대화보기
    • 악어수장
      2021-05-11 수강완료 1회독
    • ㅈㅇㅅㅇ
      14 public class GenericDemo {
      15 public static void main(String[] args) {
      16 Person p1 = new Person("부장");
      17 EmployeeInfo ei = (EmployeeInfo)p1.info;
      18 }
      19}

      에서 ei는 new를 통해 생성된 적이 없는데 어떻게 바로 형변환이 가능한건가요??
      가능한 자세히 설명 부탁드리겠습니다...ㅠㅠㅠ
    • 김태현
      3회완료
      1, <> 미리 정하지 않은 데이터 타잎을 받아서 쓴다.
      2, Person<EmployeeInfo, int> p1 = new Person<EmployeeInfo, int>(new EmployeeInfo(1), 1);
      ~ 는

      EmployeeInfo e = new EmployeeInfo(1);
      Integer i = new Integer(10);
      Person p2 = new Person(e, i);

      int를 데이터 타잎으로 쓸려면 Integer로 Integer i = new Integer(10) 객체를 만들어 쓴다. ;
      new Integer는 기본 데이터 타입인 int를 참조 데이터 타입으로 변환해주는 역할을 한다. 이러한 클래스를 래퍼(wrapper) 클래스라고 한다. 덕분에 기본 데이터 타입을 사용할 수 없는 제네릭에서 int를 사용할 수 있다

      즉 Person의 T는 Info 클래스나 그 자식 외에는 올 수 없다.
      extends는 상속(extends)뿐 아니라 구현(implements)의 관계에서도 사용할 수 있다.

      extends는 제네릭에서 부모 클래스에 있는 데이터 타잎이 자식클래스에 들어 오면 ok 다르면 에러발생시킨다

      ~ 여기까지만 우선 암기
    • 행복코딩
      오늘도 복습 잘하고 갑니다~
      감사합니다 :)
    • 김요한
      제네릭 복습 완료!
      *5번은 개념적 이해는 완료가 되었으나, 사용법에 대해서는 약간 헷갈리니 다시 봐야할 듯.
    • 애욷
      처음 접하는데 너무 어렵게 느껴지네요,,ㅠ
      이해될 때 까지 보겠습니다.!!!
    • 김승민
      2020-05-14
      감사합니다.
    • 흐무
      실전 Spring에 비하면 넘나 쉬운 기초인 제네릭...
    • silver94
      감사합니다
    • 허공
      감사합니다!
    • 홍주호
      20191005 완료
    • doevery
      수강완료
    • 아롱범
      데이터 타입의 안전성을 중시하는 java에서는 편의성과 안정성 두 마리 토끼를 잡기 위한 제네릭 기법을 제공하는군요.
    • 라또마니
      와~ 어렵네요!! 그래도 마지막까지 강의 듣고 다시 처음부터 해야겠어요~
    • ㅁㅇㄹ
      이게무슨 기본적인 사용법이야 ㅈㄴ어려운데;;
    • 전민희
      제네릭 4,5 다시보기!
    • 조서호
      제네릭은 다시봐야대
    • 이태호
      7/17
    • 뀨우
      멘붕했네요.. 나중에 필요하게 되면 다시 보러 올게요 ㅜㅜ
      좋은 강의 감사합니다.
    • 김진홍
      감사합니다.
    • 곧고수
      질문입니다..2번째 동영상에서
      17번과 그리고 특히 18번 어떻게 static도 아닌 인스턴스맴버를 클래스맴버처럼 ei.rank 로 바로 사용할수있는건가요??
      인스턴스화 시킨후에만 저렇게 사용가능한것이 아닌가요??ㅠㅠㅜ밑에 댓글중에 new person("부장")설명과는 상관없는 것 같은데요....
      아님 제가 무언가 착각하고있는거같은데 알려주세요~

      14. public class GenericDemo {
      15. public static void main(String[] args) {
      16. Person p1 = new Person("부장");
      17. EmployeeInfo ei = (EmployeeInfo)p1.info;
      18. System.out.println(ei.rank);
      19. }
      20. }


      아..! 그리고 17번 라인에서 object라는 부모클래스의 형이 employeeInfo라는 자식클래스의 형으로 변환할 수 있다는것도 이해가 안되네요....ㅠㅠ자식클래스만이 부모의 형태로 변환될수 있는 것이 아닌가요??..ㅠㅠㅠㅠㅠ
    • 하면된다하자
      제네릭 어렵네요.;;
    • 돌침대에서덤블링
      16 Person p1 = new Person("부장");

      여기서 Person 생성자 안에서 p1.info 가 인스턴스화 되잖아요.
      대화보기
      • 코딩천재
        훌륭한 강의 감사합니다
      • LazoYoung
        좋은 팁 감사합니다.
        대화보기
        • darkflame
          궁금한게 있습니다. 두 번째 동영상, 두 번째 코드 중,

          14 public class GenericDemo {
          15 public static void main(String[] args) {
          16 Person p1 = new Person("부장");
          17 EmployeeInfo ei = (EmployeeInfo)p1.info;
          18 System.out.println(ei.rank);

          17번 줄인, EmployeeInfo ei = (EmployeeInfo)p1.info; 에서 어떻게 인스턴스화 하지 않은 EmployeeInfo형 변수인 ei에 형변환된 값을 입력할수 있는가요?
          인스턴스변수는 인스턴스해야만 사용할 수 있는 것이 아닌가요?
        • popinbompin
          잘봤습니다
        • CalvinJeong
          아.. 마지막 Generic의 제한에서 의문점이 풀렸네요.
          Object 클래스를 부모로 사용했을때, class의 parameter로 개나소나 오만가지 다 올 수 있다는 문제점이 있다는 이유로 Generic을 사용하기 시작했는데,
          사실상 Generic의 선언 단계에서도 Generic 타입으로 오만가지가 다 올 수 있어서 .. 뭐가 의미가 있나 생각했는데
          Generic의 제한으로 이 문제를 해결할 수 있었던 거군요.

          egoing님의 강좌로 JAVA 정주행 완료해갑니다. 너무 감사해요
        • 효근
          나중에 다시 봐야겠습니다.
          자바는 협업이 아주 중요한 것 같네요
        • 민갱
          와.. 진짜 제네릭은 역대급이다.. 라고 느껴지는군요.. 이상하게 제네릭이 다른 파트보다 더 어렵게 느껴지는 1인입니다
        • 1234
          명강의 감사합니다
          강의 듣다가 궁금증이 든게
          Person클래스에서 제네릭을 사용안하고 생성자함수 인자타입을 인터페이스Info로주면 제네릭을 사용안하고도 똑같은 효과를 낼수있는건가 싶어서요 그렇다면 제네릭을 꼭 사용해야되나 하는 궁금증이드네요
        • GoldPenguin
          감사합니다.
        • 궁금이
          아래코드에 왜 에러가 발생하는지 잘 모르겠어요... 생략을 하면 왜 오류가 나는거죠?

          https://ideone.com/cpUGe7

          class EmployeeInfo4{
          public int rank;
          EmployeeInfo4(int rank){ this.rank = rank; }
          }
          class Person4<T, S>{ //generic으로 원시 기본 data type 을 사용할 수 없다. 사용하고 싶다면 wrapper class 을 사용해야한다. 원시 기본 data type을 객체화 시키는것
          public T info;
          public S id;
          Person4(T info, S id){
          this.info = info;
          this.id = id;
          }
          public <U> void printInfo (U info) {
          System.out.println(info);
          }
          }
          public class GenericDemo4 {
          public static void main(String[] args) { //Integer => int 에 대한 wrapping class

          System.out.println("");
          System.out.println("Multiple Generic Demo\n");

          EmployeeInfo4 e = new EmployeeInfo4(1);
          Integer i = new Integer(10);
          Person4<EmployeeInfo4, /*int*/Integer> p1 = new Person4<EmployeeInfo4, /*int*/Integer>(e, i);
          Person4 p2 = new Person4(e, i);
          //Person4 p1 = new Person4(new EmployeeInfo4(1), new Integer(10));

          //p1
          System.out.println(p1.info.rank);
          System.out.println(p1.id);
          System.out.println(p1.id.intValue());
          System.out.println("end\n");

          p1.<Integer>printInfo(e.rank);
          p1.printInfo(e.rank);
          p1.<Integer>printInfo(i);
          p1.printInfo(i);
          p1.<Integer>printInfo(i.intValue());
          p1.printInfo(i.intValue());
          System.out.println("end\n");

          //p2
          System.out.println(p2.info.rank); //=>오류 why?
          System.out.println(p2.id);
          System.out.println(p2.id.intValue()); //=>오류 why?
          System.out.println("end\n");

          p2.<Integer>printInfo(e.rank);
          p2.printInfo(e.rank);
          p2.<Integer>printInfo(i);
          p2.printInfo(i);
          p2.<Integer>printInfo(i.intValue());
          p2.printInfo(i.intValue());
          System.out.println("end\n");

          }
          }
        • 오징돌
          temp를 Integer로 수정하고 data의 타입을 오브젝트말고 T 로 바꿔보세요
          대화보기
          • 오징돌
            person<String> A = person<>은
            person클래스한테 스트링의객채만 전달할수있다고 제한하는 것입니다 그러니까 A는 person클래스의 객채인거죠
            대화보기
            • 감자돌이
              궁금한게 있습니다.

              Person A = new person();
              이렇게 인스턴스를 생성하면 A의 데이터타입은 기본적으로 Person이 되잖아요

              그런데 제네릭을 사용해서
              Person <string>A = new Person<string>();
              하게되면 A의 데이터타입을 string으로 정하는게 맞나요?

              그럼 Person은 더이상 A의 데이터타입 기능을 하지 못하는 건가요??

              컬랙션즈 프레임워크 강의를 듣다가, Collection<integer>A = new Hashset<Integer>(); 를 보며 hashset이 컬렉션 인터페이스를 구현하므로 좌항의 데이터타입을 컬렉션으로 해도 된다고 말씀하시는 부분에서 혼동이 와서 질문합니다.
            버전 관리
            egoing
            현재 버전
            선택 버전
            graphittie 자세히 보기