제네릭이란 데이터의 타입을 일반화(generalize) 하는것을 의미하며 클래스 및 메서드에서 사용할 내부 데이터 타입을 컴파일시에 미리 지정하는것을 의미한다.

조금더 상세하게 설명하면 1개의 값이 여러 타입의 데이터를 갖도록 하는 방법이다

우리가 메뉴판 프로그램을 만든다고 하면 이를 사용하는 가게주인은 가격란에 30,000을 입력할수도, 삼만원을 입력할수도 있는고 만일 데이터 타입을 int나 String 으로 정한다면 둘 중의 1개는 무조건 버그가 나타날것이다. 

이런식이다..

이런 문제를 해결하기 위해 제네릭이 사용되며 처음 작성할때는 어떤타입이든 올수 있도록 해놨다가 값(파라미터)를 넣을때 데이터의 형식을 정해주는게 사용하는게 제네릭이다.

 

제네릭을 사용하는 이유

1. 컴파일시 강한 타입체크이 가능하다.

2. 타입변환(Casting)을 제거

이것을 하단의 코드와 함께 설명하겠다.

class Studentinfo{
	public int grade;
	Studentinfo(int rank) { this.rank = rank; }
}

class Employeeinfo{
	public int rank;
	Employeeinfo(int rank) { this.rank = rank; }
}

class Person{	//(A)
	//Objcet타입은 추상적인 타입으로 어떤 타입이든 들어갈수 있다.
    public Object info;		//int, char등 모든 타입이 들어갈수 있다
    Person(Object info) { this.info = info; }
}

public class test {	//(B)
	public static void main(String[] args) {
    	Person p1 = new Person("개발자");	//Object형으로 되어있다.
        //Object가 최상위인건 알겠는데 지금 사용하려는게 어떤타입(int?String?)이냐?...
    	Employeeinfo ei = (Employeeinfo) p1.info;
    	System.out.println(ei.rank);
    }
}

여기서 (A)를 본다면 Object로 선언되있는데 Object는 각 파라미터 타입의 최상위객체이므로 어떤 타입(int,String,float...)을 넣어도 문제가 없게 만든다. -> 이것을 "타입이 불안전하다"고 표현한다.

(컴파일 단계에선 (문법적)에러가 없으나 코드의 원래목적과 다른 결과값이 나오기때문이다.)

이것은 (B)처럼 int값이 들어갈 부분인데 String값이 들어가도 문법상 오류는 없어 컴파일러는 캐치할수 없기에 찾기 어려운 버그가 생기게 되는것이다. (치킨 가격에 "15000"이 출력되야 하는데 "국내산 닭" 이라고 출력되는 버그..)

 

이것을 제네릭방식으로 바꾼 코드를 보겠다. (제네릭 예시 코드 참조)

//제네릭 예시 
class EmployeeInfo{
    public int rank;
    EmployeeInfo(int rank) { this.rank = rank; }
}
//T와S는 와일드 카드이다
//제네릭에서 복수의 파라미터를 받는경우 앞부분(T)은 참조 데이터타입만 가능
//복수의 파라미터를 받는경우 뒷부분(S)는 기본데이터 타입인 Integer, Char만 올수있다.
class Person<T, S>{	//멀티타입 파라미터
    public T info;
    public S id;
    Person(T info,s id) {
    	this.info = info;
        this.id = id;
    }
}
public class test2 {
	public static void main(String[] args) {
    	Integer id = new Integer(1);
    	Person<EmployeeInfo, Integer> p1 = new person<EmployeeInfo, Integer>(new EmployeeInfo(1), id);
    	System.out.println(p1.id.intValue());	//레퍼클래스에서 갖고있는 숫자를 int타입숫자1로 형변환
    }
}

//제네릭은 상속과 구현도 가능하다.

//부모클래스,자식클래스가 될수있다.
public class 자식클래스명<T,M> extends 부모클래스<T,M> {...}
//파라미터의 추가도 가능하다.
public class 자식클래스명<T,M,C> extends 부모클래스<T,M> {...}

 (와일드카드 - 어떤값이든 올수 있다는 의미로 여기서는 T나 S에 int, String등 어떤타입이든 올수있음을 의미한다)

 

상단의 코드를 어느정도 이해했다면 하단의 코드를 살펴보자

여기서 제네릭 타입은 타입(int,String)을 파라미터로 갖는 클래스와 인터페이스를 뜻한다.

//class<T>, interface<T> -> <>제네릭 의미, T -> 파라미터의 타입을 의미
//제네릭타입 기본적인 양식
public class test1<Sting> { ... }	//string형 제네릭 클래스
public class test2<Integer> { ... }	//int형 제네릭 클래스
public interface test3<T>{ ... }	//타입 파라미터의 이름은 T이며 제네릭 인터페이스

//CASE A
List<String> list = new ArrayList<String>();	//String만 받도록 생성
list.add("help");			//리스트에 글자삽입
String a = list.get(0);		//String으로의 타입변환 없이 출력됨

//CASE B
List list = new ArrayList();
list.add("hello");
String b = (String)list.get(0)	//String으로 Casting 필요

 

CASE A는 제네릭을 활용하여 캐스팅이 필요없는 코드이고

CASE B는 제네릭 없이 사용한 일반적인 코드이다.

 

B에 대하여 설명하자면 이전의 포스팅에서 우리는 자료형을 선언할때 아무런 조치를 하지 않는경우 JVM에서 Object를상속받는다 이야기했는데 여기서도 이개념이 동일하게 적용된다.

List를 저장할때 앞에 자료형선언이 없었기에 Object를 상속받았는데 Object는 최상위클래스지만 위에서 이야기한 Object는 타입이 불안전한 이유로 인하여 Casting을 진행한 이유다.

 

추가로 제네릭 타입은 static변수를 사용할수 없는데 이것은 클래스가 인스턴스가 되기전 static은 메모리에 먼저 올라감과 동시에 T의 타입이 정해지는데 이때 T는 타입이 결정되지 않았기때문에 에러가 나타난다. (후술할 제네릭 메서드에서 부연설명 예정)

멀티타입 파라미터

//제네릭에서 복수의 파라미터를 받는경우 앞부분(T)은 참조 데이터타입만 가능
//복수의 파라미터를 받는경우 뒷부분(S)는 기본데이터 타입인 Integer, Char만 올수있다.
class Person<T, S>{	//T,S는 임의로 붙인 이름
    public T info;
    public S id;
    Person(T info,s id) {
    	this.info = info;
        this.id = id;
    }
}
public class test2 {
	public static void main(String[] args) {
    	Integer id = new Integer(1);
    	Person<EmployeeInfo, Integer> p1 = new person<EmployeeInfo, Integer>(new EmployeeInfo(1), id);
      //자바7 이상은 타입 파라미터 부분을 유추하여 자동으로 설정해준다. (코드양이 줄었다!)
      //Person<EmployeeInfo, Integer> p1 = new person<>(new EmployeeInfo(1), id);
        System.out.println(p1.id.intValue());	//레퍼클래스에서 갖고있는 숫자를 int타입숫자1로 형변환
    }
}

해당 코드는 상단의 코드를 가져온것으로  복수의 파라미터를 사용할수 있는데 각 파라미터(T,S)를 콤마로 구분한다.

 

제네릭메서드

제네릭메서드는 매개타입, 리턴타입으로 타입(int,string)파라미터를 갖는 메서드를  의미한다.

상단의 제너릭타입은 static이 불가능했지만 제네릭 메서드는 static이 가능하다

이것은 호출시 매개타입을 지정하기때문인데 하단의 코드를 확인해보자

//public <타임파라미터,..> 리턴타입 메서드명(매개변수, ...) { ... }
//public      <T> 			Box<T>  boxing      (T t)	   { ... }
public <T> Box<T> boxing(T t) { ... }

//제네릭 메서드 호출방식 1
//리턴타입 변수 = <구체적타입> 메서드명 (매개값);
Box<Integer> box = <Integer>boxing(100);	//타입 파라미터를 명시적으로 Integer로 지정

//제네릭 메서드 호출방식 2
//리턴타입 변수 = 메서드명 (매개값);
Box<Integer> box = boxing(100);	//타입 파라미터를 Integer로 지정

 

 상단에서 제네릭 타입은 static이 사용 불가능하다고 이야기했지만 제네릭 메서드는 static의 사용이 가능하다

그 이유는 static이 갖는 특징과 타입이 결정되는 시점을 생각하면 된다.

제네릭 타입은 인스턴스가 생성될때 타입이 결정된다 ( 인스턴스 생성 예시로 "cat = new Animal()" 에서 Animal에 들어가는 데이터 타입(int,String)에 따라 제네릭타입이 갖는 타입(int,String)이 정해진다.)

그에 반해 static은 인스턴스 생성과 별도로 이미 메모리에 올라가있다. (어떤타입인진 몰라도 Object형으로 이미 올라가있어 제네릭타입을 사용이 불가능한것이다.)

즉, static으로 선언된 메서드는 인스턴스가 생성되는 시점에 결정되는 제네릭 타입을 매개변수로 받을수 없는데 반해

제네릭 메서드는 생성과 별도로 메모리에 올라가있는데 제네릭으로 시그니처된 메서드는 호출된 시점에 타입이 결정되기 때문에 에러가 발생하지 않는다 (메서드를 호출시 여기에 들어갈 값(파라미터)가 정해지거나 값이 넣어진 상태로 호출되기 때문에 static을 사용해도 문제없다)

 

제한된 타입 파라미터 (T extends 최상위타입)

public <T extends 상위타입> 리턴타입 메서드(매개변수, ...) { ... }

타입파라미터에서 구체적인 타입을 제한할 필요가 종종 있다.(숫자만 들어가는경우 int,float의 상위타입인 Number타입만 가능하도록 지정 (EX : 가격표)) 

이것을 제한된 타입 파라미터(bounded type parameter)라고 표현한다.

//제한된 타입 파라미터 예시
public <T extends 상위타입> 리턴타입 메서드명(매개변수,...) { ... }

//사용예시 (A)
public class Util {
	public <T extends Number> int compare(T t1, T t2) {
		double v1 = t1.doubleValue();	//Number의 하위타입은 double로 변형
		double v2 = t2.doubleValue();	
		return Double.campare(v1,v2);
            //compare() => 좌우가 동일할경우 = 0, 좌측크면 1, 우측이 크면 -1
	}
}

//사용예시
public class BoundedTypeParameterExample {
	public static void main(String[] args) {
    //타입 파라미터로 배열을 생성하려면 "(T[]) (new Object[n])"형태로 생성한다.
    	//String str = Util.compare("a","b");
        //"a","b"는 Number의 하위타입이 아니기에 에러가 출력된다
    	
        int result1 = Util.compare(10,20);	//20은 int형에서 Integer 형으로 Boxing된다.
        System.out.println(result1);		//-1 출력
        
       int result2 = Util.compare(4.5,3);	//4.5는 double형에서 Double 형으로 Boxing된다.
       System.out.println(result2);			//1 출력
    }
}

(int형과 Integer의 차이점)

int는 변수의 타입(data type)을 의미한다. (변수는 값을 저장할수 있는 메모리상의 공간을 의미하며 4바이트를 의미한다.)

Integer는 wrapper class를 의미한다.(매개변수를 객체로 필요할때, 기본형값(int)이 아닌 객체로 저장해야할때, 객체간 비교가 필요할때 사용된다.) 

int to Integer -> boxing한다는 표현을 사용한다.

boxing - 기본타입 데이터를 래퍼클래스 인스턴스로 변환하는것

int - 산술연산이 가능하며 null로 초기화가 불가능하다.

Integer - Unboxing하지 않는경우 산술연산이 불가능하며 null값이 가능하다.

래퍼클래스 (wrapper class)는 기본타입 데이터를 객체로 다루기 위하여 사용하는 클래스

 

와일드카드 타입

코드에서 일반적으로 제네릭 타입을 

(와일드카드 - "?"로 어떤값이든 올수 있다는 뜻이다.)

제네릭<?> - 제한없음(모든 클래스, 인터페이스가 올수있다)
제네릭타입<? extends 상위타입> - 상위클래스 제한 (상위타입, 하위타입만 올수있다)
제네릭타입<? super 하위타입> - 하위클래스 제한 (하위타입이나 상위타입만 올수있다)
//상위타입 <-> 하위타입 가능한건 OOP의 특징에서 다형성 참조

 

마지막으로 범용적으로 쓰이는 제네릭 타입변수는 하단과 같은 방식으로 쓰인다고 한다

타입 설명
<T> Type
<E> Element
<K> Key
<V> Value
<N> Number

 

참고사이트 : https://vvshinevv.tistory.com/55

 

'CS' 카테고리의 다른 글

프로세서, 프로세스  (0) 2023.03.21
주소 바인딩  (0) 2023.03.18
객체 지향 프로그래밍 (OOP) - 2 (5대원칙)  (0) 2023.03.02
스택트레이스(Stack Trace)  (0) 2023.02.26
객체 지향 프로그래밍 (OOP) - 1  (0) 2022.12.25

+ Recent posts