ejyoo's 개발 노트

제너릭 이해 본문

BackEnd/Java

제너릭 이해

ejyoovV 2021. 3. 9. 21:21

            오늘 수업은 제너릭에 대해 배웠다.

흐름은 대충 이해가 갔는데 듬성듬성 부족한 지식을 채우기 위해 동영상을 보며 복습하겠다.

 

동영상보기 -> 구글 블로그 상위 5개 글 참고하기 -> 선생님코드 보기 -> 머릿속에 다시 정리하기

 

동영상은 생활코딩을 보며 복습했다.

 

💡 제네릭의 정의

제네릭(Generic)은 데이터의 타입(data type)을 일반화한다.(generalize)라는 의미로

클래스나 메소드에서 사용할 내부 데이터 타입을

컴파일 시에 미리 지정하는 방법이다.

 

=> 클래스, 메서드 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다. 

 

 

💡 제네릭 코드 예시

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는 아래 코드의 p1, p2<> 안에 지정된 데이터 타입에 의해서 결정된다.

아래 코드를 나누어서 보자.

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

아래 코드는 변수 p1의 데이터 타입을 정의하고 있다.

Person<String> p1

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

new Person<String>();

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

 

💡 제네릭을 사용하는 이유 : 타입의 안정성 보장받기 위함.

 

📝 테스트 코드 - 중복이 존재하는 코드(제너릭 사용 전)

JDK 1.5 이전에는 여러 타입을 사용하는 대부분의 클래스나 메소드에서 인수나 반환값으로 Object 타입을 사용했다.

하지만 이 경우에는 반환된 Object 객체를 다시 원하는 타입으로 타입 변환해야 하며,

이때 오류가 발생할 가능성도 존재한다.

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가 사실상 같은 구조를 가지고 있다.

즉 중복이 발생하고 있는 것이다.

중복을 제거하여 간략화 한다.

 

📝 테스트 코드 - 중복이 존재하는 코드(제너릭 사용 전) 간략화

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;//다운캐스팅 Object -> employeeInfo
        StudentInfo si = (StudentInfo) p1.info;
        System.out.println(ei.rank);
        System.out.println(si.grade);
    }
}

위의 코드는 저장할 때, 성공적으로 컴파일 된다. 하지만 실행하면 아래의 오류가 발생한다.

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to kr.or.ddit.basic.remind.EmployeeInfo
	at kr.or.ddit.basic.remind.GenericDemo.main(GenericDemo.java:22)

 

이 오류의 원인은 이 코드 때문이다.

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

클래스 Person의 생성자는 매개변수 info의 데이터 타입이 Object이다. 따라서 모든 객체가 될 수 있다.

원래는 저 Person 값에 EmployeeInfo나 StudentInfo 객체가 들어와야 하는데, 

그것이 아닌 String이 들어왔음에도 불구하고 컴파일 에러가 발생하지 않는다.

컴파일 입장에서는 저 코드가 Object로 정상적으로 들어왔기 때문에 문제를 발생시키지 않은 것이다.

위 코드로 인해 컴파일 에러는 발생하지 않으나

런타임 에러가 발생한다.

컴파일 언어의 기본은 모든 에러는 컴파일이 발견할 수 있도록 유도해야하는데, 런타임은 실제로 애플리케이션이 동작하고 있는 상황이기 때문에 런타임에 발생하는 에러는 항상 심각한 문제를 초래할 수 잇기 때문이다.

 

위와 같은 에러를 타입에 대해서 안전하지 않다고 한다.

즉 모든 타입이 올 수 있기 때문에 타입을 엄격하게 제한 할 수 없게 되는것이다.

 

그래서 제너릭을 사용하여 컴파일 시 미리 타입 검사(type check)를 수행하도록 하는 것이다.

 

📝 제네릭의 장점

  • 클래스나 메소드 내부에서 사용되는 객체의 타입 안정성을 높일 수 있다.
  • 반환값에 대한 타입 변환 및타입 검사에 들어가는 노력을 줄일 수있다.

 

💡 위의 코드 제네릭화

제너릭을 사용하면 컴파일 과정에서 문제를 잡아낼 수 있다.

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 타입을 지정하여 제너릭 T에 대한 타입을 설정한다.
//		EmployeeInfo 메서드를 호출하여 rank 값을 세팅한다.
        Person<EmployeeInfo> p1 = new Person<EmployeeInfo>(new EmployeeInfo(1));
//      ei1에 p1.info 데이터를 가져와 저장한다.
        EmployeeInfo ei1 = p1.info;
//      ei1에 있는 rank 데이터를 출력한다.
        System.out.println(ei1.rank);//성공
        
//		String 객체 생성한 뒤 생성자를 호출하여 "부장" 데이터를 사용하여 무엇을 처리한다.
        Person<String> p2 = new Person<String>("부장");
//		그 정보를 가져온다.
        String ei2 = p2.info;
//		rank 값을 출력한다.(값이없는데...)
        System.out.println(ei2.rank);
    }
}

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

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

JDK 1.5부터 도입된 제네릭을 사용하면 컴파일 시에 미리 타입이 정해지므로 타입 검사나 타입 변환과 같은 번거로운 작업을 생략할 수 있다.

 

📝 제네릭의 선언 및 생성

제네릭은 클래스와 메소드에만 선언할 수 있다.

class MyArray<T> {
	T element;
	void setElement(T element) { this.element = element; }
	T getElement() { return element; }
}

위에서 사용한 'T'를 타입 변수(type variable) 라고 하며, 임의의 참조형 타입을 의미한다.

꼭 'T' 뿐만 아니라 어떠한 문자를 사용해도 상관없다.

타입변수는 클래스에서뿐만 아니라 메소드의 매개변수나 반환값으로도 사용할 수 있다.

이 제네릭 클래스를 생성할 때에는 타입 변수 자리에 사용할 실제 타입을 명시해야 한다.

MyArray<Integer> myArr = new MyArray<Integer>();

클래스 객체를 생성하자 마자 클래스에 사용된 타입변수가 결정된다.

MyArray 클래스에 사용딘 타입 변수로 Integer 타입을 사용하는 예제이다.

위처럼 제네릭 클래스를 생성할 때 사용할 실제 타입을 명시하면, 내부적으로는 정의된 타입 변수가 명시된 실제 타입으로 변환되어 처리된다.

 

 

💡 복수개의 제네릭 사용 - 기본 데이터 타입과 제네릭

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

아래의 코드를 먼저 보자

 

복수의 제네릭을 사용할 때 <T, S> 와 같은 형식을 사용하는데, 여기서 T와 S대신 어떠한 문자를 사용해도 된다.

하지만 묵시적인 약속(convention)이 있다.

여러개의 타입 변수는 쉼표(,)로 구분하여 명시할 수 있다.

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

위의 코드는 예외를 발생시킨다.

위의 코드에서

제네릭은 참조 데이터 타입에 대해서만 사용할 수 있다. 기본 데이터 타입에서는 사용할 수 없다.

따라서 int는 사용할 수 없으므로 int의 참조 데이터인 래퍼클래스 Integer 를 사용한다.

즉, 자바에서 타입 변수 자리에 사용할 실제 타입을 명시할 때 기본타입을 바로 사용할 수없다.

이때, Integer와 같이 래퍼(wapper) 클래스를 사용해야만 한다.

 

아래와 같이 코드를 변경한다.

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) {
//    	int 기본 데이터형은 제너릭을 사용할 수 없다.
//    	Person<EmployeeInfo, int> p1 = new Person<EmployeeInfo, int>(new EmployeeInfo(1),1);
    	
//    	EmployeeInfo 객체를 생성하여 rank 값을 가지고 있는다.
    	EmployeeInfo e = new EmployeeInfo(1);
//    	Integer 래퍼 클래스 객체를 생성하여 10을 가지고 있는다.
    	Integer i = new Integer(10);
//    	Person객체를 생성하여 제너릭 타입을 EmployeeInfo, Integer로 정의한 뒤 값을 넣는다.
    	Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i);
//    	래퍼클래스 상태인 값을 그냥 출력해도 되지만 id의 원래 데이터는 int이므로 int 형태로 변환한 뒤 출력한다.
    	System.out.println(p1.id.intValue());
    }
}

new Integer는 기본 데이터 타입인 int를 참조 데이터 타입으로 변환해주는 역할을 한다. 

이러한 클래스를 래퍼(wrapper) 클래스라고 한다.

덕분에 기본 데이터 타입을 사용할 수 없는 제네릭에서 int를 사용할 수 있다.

 

💡 제네릭의 생략

Java SE 7 부터 인스턴스 생성 시 타입을 생략할 수 있다. 

MyArray<Integer> myArr = new MyArray<Integer>();

위의 코드에서 Integer를 생략하면

MyArray<Integer> myArr = new MyArray<>();

 

다음 예제는 제네릭에서 적용되는 타입 변수의 다형성을 보여주는 예제이다.

 

package kr.or.ddit.basic.remind;

import java.util.ArrayList;

class LandAnimal {
	public void crying() {
		System.out.println("육지동물");
	}
}

class Cat extends LandAnimal {
	public void crying() {
		System.out.println("냐옹냐옹");
	}
}

class Dog extends LandAnimal {
	public void crying() {
		System.out.println("멍멍");
	}
}

class Sparrow {
	public void crying() {
		System.out.println("짹쨱");
	}
}

class AnimalList<T> {
	ArrayList<T> al = new ArrayList<T>();
	
	void add(T animal) { 
		al.add(animal);
	}
	
	T get(int index) {
		return al.get(index);
	}
	
	boolean remove(T animal) {
		return al.remove(animal);
	}
	
	int size() {
		return al.size();
	}
}

public class Generic01 {
	public static void main(String[] args) {
		AnimalList<LandAnimal> landAnimal = new AnimalList<>();//Java SE7 부터 생략 가능함.
		
		landAnimal.add(new LandAnimal());
		landAnimal.add(new Cat());
		landAnimal.add(new Dog());
		landAnimal.add(new Sparrow());
		
		for(int i = 0 ; i < landAnimal.size(); i++) {
			landAnimal.get(i).crying();
		}
	}
}

위의 예제를 실행하면 오류가 발생한다.

Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
	The method add(LandAnimal) in the type AnimalList<LandAnimal> is not applicable for the arguments (Sparrow)

	at kr.or.ddit.basic.remind.Generic01.main(Generic01.java:56)

문제는 이부분이다.

landAnimal.add(new Sparrow());

landAnimal 변수는 LandAnimal 클래스이다. 

LandAnimal과 LandAnimal을 상속받는 자식 클래스 Cat, Dog만 가능하다.

하지만 Sparrow 클래스는 타입이 다르므로 추가할 수 없다.

 

 

또다른 예제로 타입 변수의 다형성을 보여주는 예제이다.

제네릭은 생략 가능하다.

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 class GenericDemo {
    public static void main(String[] args) {
//    	int 기본 데이터형은 제너릭을 사용할 수 없다.
//    	Person<EmployeeInfo, int> p1 = new Person<EmployeeInfo, int>(new EmployeeInfo(1),1);
    	
//    	EmployeeInfo 객체를 생성하여 rank 값을 가지고 있는다.
    	EmployeeInfo e = new EmployeeInfo(1);
//    	Integer 래퍼 클래스 객체를 생성하여 10을 가지고 있는다.
    	Integer i = new Integer(10);
//    	Person객체를 생성하여 제너릭 타입을 EmployeeInfo, Integer로 정의한 뒤 값을 넣는다.
    	Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i);
//    	컴파일러는 제네릭 사용 시 제네릭 타입을 생략하더라도 제네릭에 넘어가는 파라미터 타입을 확인하여 정상적인 컴파일이 되도록 한다.
    	Person p2 = new Person(e,i);
//    	래퍼클래스 상태인 값을 그냥 출력해도 되지만 id의 원래 데이터는 int이므로 int 형태로 변환한 뒤 출력한다.
    	System.out.println(p1.id.intValue());
    }
}

 

💡 제네릭은 메소드에서도 사용할 수 있다.

클래스에만 적용하는 것이 아니라 메소드에도 적용할 수 있다.

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) {
//    	int 기본 데이터형은 제너릭을 사용할 수 없다.
//    	Person<EmployeeInfo, int> p1 = new Person<EmployeeInfo, int>(new EmployeeInfo(1),1);
    	
//    	EmployeeInfo 객체를 생성하여 rank 값을 가지고 있는다.
    	EmployeeInfo e = new EmployeeInfo(1);
//    	Integer 래퍼 클래스 객체를 생성하여 10을 가지고 있는다.
    	Integer i = new Integer(10);
//    	Person객체를 생성하여 제너릭 타입을 EmployeeInfo, Integer로 정의한 뒤 값을 넣는다.
    	Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i);
//    	컴파일러는 제네릭 사용 시 제네릭 타입을 생략하더라도 제네릭에 넘어가는 파라미터 타입을 확인하여 정상적인 컴파일이 되도록 한다.
    	Person p2 = new Person(e,i);
//    	래퍼클래스 상태인 값을 그냥 출력해도 되지만 id의 원래 데이터는 int이므로 int 형태로 변환한 뒤 출력한다.
    	System.out.println(p1.id.intValue());
    	
//    	제너릭이 구현된 메소드를 호출한다.
    	p1.<EmployeeInfo>printInfo(e);
//    	넘겨지는 파라미터로 인하여 컴파일러가 제너릭 타입을 알 수 있기 때문에 생략할 수 있다.
    	p1.printInfo(e);
    }
}

💡 제네릭의 제한

제네릭도 제한을 둘 수 있다.

자바에는 상속 또는 Implements 가 되면 하나의 부모가 여러명의 자식을 소유할 수 있는데,

이때 제한을 두어야 다른 관계없는 클래스가 올 수 없다.

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>{//extends Info를 사용하지 않는 경우 <T extends Object>와 같다.
    public T info;
    Person(T info){
    		this.info = info;
    		info.getLevel();
    }
    
    public <U> void printInfo(U info) {
    	System.out.println(info);
    }
}

public class GenericDemo {
    public static void main(String[] args) {
    	Person<EmployeeInfo> p1 = new Person<EmployeeInfo>(new EmployeeInfo(1));
    }
}

위의 코드를 사용하여 Info 인터페이스 하위에 있는 EmployeeInfo 까지만

제너릭 타입으로 사용할 수 있다.

 

 

 

참고 사이트

더보기
  • 생활코딩
  • www.tcpschool.com/java/java_generic_concept

 

'BackEnd > Java' 카테고리의 다른 글

싱글톤 패턴에 대해서 (Keyword : 싱글톤 디자인 패턴)  (0) 2021.03.15
MAC / ORACLE / DOCKER / OJDBC / Eclipse 연동  (0) 2021.03.14
Enum(열거형)  (0) 2021.03.08
Properties  (0) 2021.03.08
Map  (0) 2021.03.08