제네릭

KIMJONGEON·2022년 12월 30일
0

JAVA Practice

목록 보기
6/7

📌 제네릭의 개념

  • 컬렉션 클래스에 저장할 형(type)을 지정하고 데이터를 읽을 때 지정된 형으로 데이터를 입력받게 하는 방식.
  • 제네릭은 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법이다.

👉 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것을 의미한다. 한마디로 특정(Specific) 타입을 미리 지정해주는 것이 아닌 필요에 의해 지정할 수 있도록 하는 일반(Generic) 타입이라는 것이다.

📖 선언 및 생성예제

1. 클래스 및 인터페이스 선언

public class ClassName <T> { ... }
public Interface InterfaceName <T> { ... }

T 타입은 해당 블럭 { ... } 안에서까지 유효하다.

1-1 제네릭 타입을 2개로 두는 예제 (HashMap)

public class ClassName <T, K> { ... }
public Interface InterfaceName <T, K> { ... }

// HashMap의 경우 아래와 같이 선언되어있을 것이다.
public class HashMap <K, V> { ... }

이렇듯 데이터 타입을 외부로부터 지정할 수 있도록 할 수 있다.

생성된 제네릭 클래스를 사용. 즉, 객체를 생성해야하는데 이 때 구체적인 타입을 명시해주어야 한다.

public class ClassName <T, K> { ... }

public class Main {
	public static void main(String[] args) {
		ClassName<String, Integer> a = new ClassName<String, Integer>();
	}

위 예시에서는 T는 String이 되고, K는 Integer가 된다.

❗️주의❗️ 타입 파라미터로 명시할 수 있는 것은 참조 타입(Reference Type) 밖에 올 수 없다. 즉, int, double, char 같은 primitive type은 올 수 없다는 것이다. 그래서 int형 double형 등 primitive Type의 경우 Integer, Double 같은 Wrapper Type으로 쓰는 이유가 바로 위와같은 이유다.

public class ClassName <T> { ... }

public class Student { ... }

public class Main {
	public static void main(String[] args) {
		ClassName<Student> a = new ClassName<Student>();
	}
}

또한 참조 타입이 올 수 있다는 것은 사용자가 정의한 클래스도 타입으로 올 수 있다는 것을 의미한다.

class MyArray<T> {

    T element;

    void setElement(T element) { this.element = element; }

    T getElement() { return element; }

}

2. 제네릭 클래스

2-1 제네릭 클래스 예제

class ClassName<E> {
	
	private E element;	// 제네릭 타입 변수
	
	void set(E element) {	// 제네릭 파라미터 메소드
		this.element = element;
	}
	
	E get() {	// 제네릭 타입 반환 메소드
		return element;
	}
	
}
 
class Main {
	public static void main(String[] args) {
		
		ClassName<String> a = new ClassName<String>();
		ClassName<Integer> b = new ClassName<Integer>();
		
		a.set("10");
		b.set(10);
	
		System.out.println("a data : " + a.get());
		// 반환된 변수의 타입 출력 
		System.out.println("a E Type : " + a.get().getClass().getName());
		
		System.out.println();
		System.out.println("b data : " + b.get());
		// 반환된 변수의 타입 출력 
		System.out.println("b E Type : " + b.get().getClass().getName());
		
	}
}

위의 예제에서 'ClassName'이란 객체를 생성할 때 <> 안에 타입 파라미터(Type parameter)를 지정한다.

그러면 a객체의 ClassName의 E 제네릭 타입은 String으로 모두 변환된다.

반대로 b객체의 ClassName의 E 제네릭 타입은 Integer으로 모두 변환된다.
실제로 위 코드를 실행시키면 다음과 같이 출력된다.

출력

a data : 10
a E Type : java.lang.String

b data : 10
b E Type : java.lang.Integer

2-2 제네릭 클래스 예제 (두 개의 제네릭 사용)

class ClassName<K, V> {
    private K first;	// K 타입(제네릭)
    private V second;	// V 타입(제네릭)

    void set(K first, V second) {
        this.first = first;
        this.second = second;
    }

    K getFirst() {
        return first;
    }

    V getSecond() {
        return second;
    }
}

// 메인 클래스
class Main {
    public static void main(String[] args) {

        ClassName<String, Integer> a = new ClassName<String, Integer>();

        a.set("10", 10);


        System.out.println("  fisrt data : " + a.getFirst());
        // 반환된 변수의 타입 출력
        System.out.println("  K Type : " + a.getFirst().getClass().getName());

        System.out.println("  second data : " + a.getSecond());
        // 반환된 변수의 타입 출력
        System.out.println("  V Type : " + a.getSecond().getClass().getName());
    }
}

출력

fisrt data : 10
K Type : java.lang.String
second data : 10
V Type : java.lang.Integer

3.제네릭 메소드

3-1 선언 방식

[접근 제어자] <제네릭타입> [반환타입] [메소드명]([제네릭타입] [파라미터]) {
	// 텍스트
}
public <T> T genericMethod(T o) {	// 제네릭 메소드
		...
}

반환타입 이전에 <> 제네릭 타입을 선언한다.

3-2 선언 예제

// 제네릭 클래스
 class ClassName<E> {

    private E element;	// 제네릭 타입 변수

    void set(E element) {	// 제네릭 파라미터 메소드
        this.element = element;
    }

    E get() {	// 제네릭 타입 반환 메소드
        return element;
    }

    <T> T genericMethod(T o) {	// 제네릭 메소드
        return o;
    }


}

public class Main {
    public static void main(String[] args) {

        ClassName<String> a = new ClassName<>();
        ClassName<Integer> b = new ClassName<>();

        a.set("10");
        b.set(10);

        System.out.println("a data : " + a.get());
        // 반환된 변수의 타입 출력
        System.out.println("a E Type : " + a.get().getClass().getName());

        System.out.println();
        System.out.println("b data : " + b.get());
        // 반환된 변수의 타입 출력
        System.out.println("b E Type : " + b.get().getClass().getName());
        System.out.println();

        // 제네릭 메소드 Integer
        System.out.println("<T> data : " +a.genericMethod(3));
        System.out.println("<T> returnType : " + a.genericMethod(3).getClass().getName());
        System.out.println();
        // 제네릭 메소드 String
        System.out.println("<T> data : " +a.genericMethod("ABCD"));
        System.out.println("<T> returnType : " + a.genericMethod("ABCD").getClass().getName());
        System.out.println();
        // 제네릭 메소드 ClassName b
        System.out.println("<T> returnType : " + a.genericMethod(b).getClass().getName());
    }
}

출력

a data : 10
a E Type : java.lang.String

b data : 10
b E Type : java.lang.Integer

<T> data : 3
<T> returnType : java.lang.Integer

<T> data : ABCD
<T> returnType : java.lang.String

<T> returnType : generic_03.ClassName

ClassName이란 객체를 생성할 때 <> 안에 타입 파라미터를 지정한다.

그러면 a객체의 ClassName의 E 제네릭 타입은 String으로 모두 변환된다.

반대로 b객체의 ClassName의 E 제네릭 타입은 Integer으로 모두 변환된다.

genericMethod()는 파라미터 타입에 따라 T 타입이 결정된다.

👉 즉, 클래스에서 지정한 제네릭유형과 별도로 메소드에서 독립적으로 제네릭 유형을 선언하여 쓸 수 있다.

📖 제네릭 메소드를 사용하는 이유?

'정적 메소드'로 선언할 때 필요하기 때문이다.

제네릭은 타입을 외부에서 지정해준다. 즉 해당 클래스 객체가 인스턴스화 했을 때, 쉽게 말해 new 생성자로 클래스 객체를 생성하고 <> 괄호 사이에 파라미터로 넘겨준 타입으로 지정이 된다.

하지만 static 정적이라는 뜻으로 static 변수, static 함수 등 static이 붙은 것들은 기본적으로 프로그램 실행시 메모리에 이미 올라 가 있다.

즉 객체 생성을 통해 접근할 필요 없이 이미 메모리에 올라가 있기 때문에 클래스 이름을 통해 바로 쓸 수 있다.

이때, static 메소드는 객체가 생성되기 전에 이미 메모리에 올라가게 되는데 타입을 어디서 얻어올 수 있을까?

아래 예제를 보면,

class ClassName<E> {
 
	/*
	 * 클래스와 같은 E 타입이더라도
	 * static 메소드는 객체가 생성되기 이전 시점에
	 * 메모리에 먼저 올라가기 때문에
	 * E 타입을 클래스로부터 얻어올 방법이 없다.
	 */
	static E genericMethod(E o) {	// error!
		return o;
	}
	
}
 
class Main {
 
	public static void main(String[] args) {
 
		// ClassName 객체가 생성되기 전에 접근할 수 있으나 유형을 지정할 방법이 없어 에러남
		ClassName.getnerMethod(3);
 
	}
}

클래스와 같은 E 타입이더라도 static 메소드는 객체가 생성되기 이전 시점에 메모리에 먼저 올라가기 때문에 E 타입을 클래스로부터 얻어올 방법이 없다.
따라서 제네릭이 사용되는 메소드를 정적 메소드로 두고 싶은 경우 제네릭 클래스와 별도로 독립적인 제네릭이 사용되어야 한다.

📖 제네릭 메소드 사용 예제

// 제네릭 클래스
class ClassName<E> {
 
	private E element; // 제네릭 타입 변수
 
	void set(E element) { // 제네릭 파라미터 메소드
		this.element = element;
	}
 
	E get() { // 제네릭 타입 반환 메소드
		return element;
	}
 
	// 아래 메소드의 E타입은 제네릭 클래스의 E타입과 다른 독립적인 타입이다.
	static <E> E genericMethod1(E o) { // 제네릭 메소드
		return o;
	}
 
	static <T> T genericMethod2(T o) { // 제네릭 메소드
		return o;
	}
 
}
 
public class Main {
	public static void main(String[] args) {
 
		ClassName<String> a = new ClassName<String>();
		ClassName<Integer> b = new ClassName<Integer>();
 
		a.set("10");
		b.set(10);
 
		System.out.println("a data : " + a.get());
		// 반환된 변수의 타입 출력
		System.out.println("a E Type : " + a.get().getClass().getName());
 
		System.out.println();
		System.out.println("b data : " + b.get());
		// 반환된 변수의 타입 출력
		System.out.println("b E Type : " + b.get().getClass().getName());
		System.out.println();
 
		// 제네릭 메소드1 Integer
		System.out.println("<E> returnType : " + ClassName.genericMethod1(3).getClass().getName());
 
		// 제네릭 메소드1 String
		System.out.println("<E> returnType : " + ClassName.genericMethod1("ABCD").getClass().getName());
 
		// 제네릭 메소드2 ClassName a
		System.out.println("<T> returnType : " + ClassName.genericMethod1(a).getClass().getName());
 
		// 제네릭 메소드2 Double
		System.out.println("<T> returnType : " + ClassName.genericMethod1(3.0).getClass().getName());
	}
}

출력

a data : 10
a E Type : java.lang.String

b data : 10
b E Type : java.lang.Integer

<E> returnType : java.lang.Integer
<E> returnType : java.lang.String
<T> returnType : generic_04.ClassName
<T> returnType : java.lang.Double

이렇듯 제네릭 메소드는 제네릭 클래스 타입과 별도로 지정된다는 것을 볼 수 있다.

👉 장점

  • 제네릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다.
  • 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없다. 즉, 관리하기가 편하다.
  • 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다.
profile
Gongbuhaja

0개의 댓글