[Java] Generic

ss9909·2022년 11월 24일
0

Java

목록 보기
4/5

제네릭에 대해 잘 정리해주신 분이 있어 쉽게 이해할 수 있었습니다.
https://st-lab.tistory.com/153
해당 글을 읽고 재 정리한 것입니다.

제네릭(Generic)의 의미

제네릭(Generic) : '일반적인'
제네릭을 사용하면 데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 할 수 있다.

우리가 흔히 쓰는 ArrayList, LinkedList 등을 생성할 때 다음과 같이 선언한다.

/*
* 객체<타입> 객체명 = new 객체<타입>();
*/
ArrayList<Integer> list1 = new ArrayList<Integer>();
ArrayList<String> list2 = new ArrayList<Integer>();
 
LinkedList<Double> list3 = new LinkedList<Double>():
LinkedList<Character> list4 = new LinkedList<Character>();

이렇게 <> 괄호 안에 들어가는 타입을 지정해준다.
String 타입도 지원하고싶고 Integer타입도 지원하고 싶고 많은 타입을 지원하는 자료구조를 구현하고자 한다.
자료형에 따라 클래스를 만드는 것은 좋은 방법이 아니다.
제네릭을 활용하자.

제네릭(Generic)의 사용

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

지정된다는 것 보다는 타입의 경계를 지정하고, 컴파일 때 해당 타입으로 캐스팅하여 매개변수화 된 유형을 삭제하는 것이다.

제네릭(Generic)의 장점

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

제네릭(Generic) 사용방법

보통 제네릭은 아래 표의 타입들을 많이 사용한다. 반드시 한 글자일 필요는 없으며 설명과 반드시 일치해야 할 필요도 없다.\
예를 들어 <E> 대신 <Element>라고 해도 문제가 발생하지 않는다. 다만 대중적으로 통하는 네이밍 컨벤션이 있을 뿐이다.

TYPEDESCRIPTION
<T>Type
<E>Element
<K>Key
<V>Value
<N>Number

상황별 선언 및 생성 방법

클래스 및 인터페이스 선언

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

기본적으로 제네릭 타입의 클래스나 인터페이스의 경우 위와같이 선언한다. T 타입은 해당 블럭 { ... } 안에서까지 유효하다.
더 나아가 제네릭 타입을 두 개로 둘 수도 있다.

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 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으로 모두 변환된다.

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

결과

제네릭 메소드 클래스

제네릭 메소드는 메소드에 한정한 제네릭도 사용할 수 있다.

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

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

  • 제네릭 메소드 선언
// 제네릭 클래스
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<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();
		
		// 제네릭 메소드 Integer
		System.out.println("<T> returnType : " + a.genericMethod(3).getClass().getName());
		
		// 제네릭 메소드 String
		System.out.println("<T> returnType : " + a.genericMethod("ABCD").getClass().getName());
		
		// 제네릭 메소드 ClassName b
		System.out.println("<T> returnType : " + a.genericMethod(b).getClass().getName());
	}
}

ClassName이란 객체를 생성할 때 <> 안에 타입 파라미터(Type parameter)를 지정한다.
그러면 a객체의 ClassName의 E 제네릭 타입은 String으로 모두 변환된다.
반대로 b객체의 ClassName의 E 제네릭 타입은 Integer으로 모두 변환된다.
genericMethod()는 파라미터 타입에 따라 T 타입이 결정된다.

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

제네릭 메소드가 필요한 이유 바로 '정적 메소드로 선언할 때 필요'하기 때문이다.
앞서 제네릭은 유형을 외부에서 지정해준다고 했다.
즉 해당 클래스 객체가 인스턴스화 했을 때, <> 괄호 사이에 파라미터로 넘겨준 타입으로 지정이 된다.

static 변수, static 함수 등 static이 붙은 것들은 기본적으로 프로그램 실행 시 메모리에 올라간다. 객체 생성을 통해 접근할 필요 없이 이미 메모리에 올라가 있기 때문에 클래스 이름을 통해 바로 쓸 수 있다.
static 메소드는 객체가 생성되기 전에 이미 메모리에 올라가는데 타입을 어디서 얻을 것인가?

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

제네릭이 사용되는 메소드를 정적메소드로 두고 싶은 경우 제네릭 클래스와 별도로 독립적인 제네릭이 사용되어야 한다

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

제네릭 메소드는 제네릭 클래스 타입과 별도로 지정된다는 것을 확인할 수 있다.

제한된 Generic(제네릭)과 와일드 카드

타입을 T라고 하고 외부클래스에서 Integer을 파라미터로 보내면 T 는 Integer가 되고, String을 보내면 T는 String이 된다.
만약 Student 라는 클래스를 만들었을 때 T 파라미터를 Student로 보내면 T는 Student가 된다.
즉, 제네릭은 참조 타입 모두 될 수 있다.

만약 특정 범위 내로 좁혀서 제한하고 싶다면 어떻게 해야할까? 이 때 필요한 것이 바로 extendssuper, 그리고 ?다.

3가지 경우로 나눌 수 있다. super 키워드와 extends 키워드, 마지막으로 ? 하나만 오는 경우다.

<K extends T>	// T와 T의 자손 타입만 가능 (K는 들어오는 타입으로 지정 됨)
<K super T>	// T와 T의 부모(조상) 타입만 가능 (K는 들어오는 타입으로 지정 됨)
 
<? extends T>	// T와 T의 자손 타입만 가능
<? super T>	// T와 T의 부모(조상) 타입만 가능
<?>		// 모든 타입 가능. <? extends Object>랑 같은 의미

이 때 주의해야 할 게 있다. <K extends T><? extends T>는 비슷한 구조지만 차이점이 있다.

'유형 경계를 지정'하는 것은 같으나 경계가 지정되고 K는 특정 타입으로 지정이 되지만, ?는 타입이 지정되지 않는다는 의미다.

/*
 * Number와 이를 상속하는 Integer, Short, Double, Long 등의
 * 타입이 지정될 수 있으며, 객체 혹은 메소드를 호출 할 경우 K는
 * 지정된 타입으로 변환이 된다.
 */
<K extends Number>
 
 
/*
 * Number와 이를 상속하는 Integer, Short, Double, Long 등의
 * 타입이 지정될 수 있으며, 객체 혹은 메소드를 호출 할 경우 지정 되는 타입이 없어
 * 타입 참조를 할 수는 없다.
 */
<? extends T>	// T와 T의 자손 타입만 가능

위와 같은 차이가 있어 특정 타입의 데이터를 조작하고자 할 경우에는 K 같이 특정 제네릭 인수로 지정을 해주어야 한다.

다음과 같은 상속관계를 갖고 있다고 해보자.
class A <- class B <- class C
class A <- class D <- class E

<K extends T>, <? extends T>

T타입을 포함한 자식 타입만 가능하다는 의미다. 즉, 다음과 같은 경우들이 있다.

<T extends B>	// B와 C타입만 올 수 있음
<T extends E>	// E타입만 올 수 있음
<T extends A>	// A, B, C, D, E 타입이 올 수 있음
 
<? extends B>	// B와 C타입만 올 수 있음
<? extends E>	// E타입만 올 수 있음
<? extends A>	// A, B, C, D, E 타입이 올 수 있음

extends 뒤에 오는 타입이 최상위 타입으로 한계가 정해진다.

  • 제네릭 클래스에서 수를 표현하는 클래스만 받고 싶은 경우
    대표적인 Integer, Long, Byte, Double, Float, Short 같은 래퍼 클래스들은 Number 클래스를 상속 받는다.

즉, Integer, Long, Byte, Double, Float, Short 같은 수를 표현하는 래퍼 클래스만으로 제한하고 싶은 경우 다음과 같이 쓸 수 있다.

public class ClassName <K extends Number> { ... }

이렇게 특정 타입 및 그 하위 타입만 제한 하고 싶을 경우 쓰면 된다. Integer는 Number 클래스를 상속받는 클래스라 가능하지만, String은 Number클래스와는 완전 별개의 클래스이기 때문에 에러(Bound mismatch)를 띄운다.

public class ClassName <K extends Number> { ... }
 
public class Main {
	public static void main(String[] args) {
		ClassName<Double> a1 = new ClassName<Double>();	// OK!
		ClassName<String> a2 = new ClassName<String>();	// error!
	}
}

<T super T>, <? super T>

T 타입의 부모 타입만 가능하다

<K super B>	// B와 A타입만 올 수 있음
<K super E>	// E, D, A타입만 올 수 있음
<K super A>	// A타입만 올 수 있음
 
<? super B>	// B와 A타입만 올 수 있음
<? super E>	// E, D, A타입만 올 수 있음
<? super A>	// A타입만 올 수 있음

super 뒤에 오는 타입이 최하위 타입으로 한계가 정해진다.
대표적으로 해당 객체가 업캐스팅(Up Casting)이 될 필요가 있을 때 사용한다.
'사람'이라는 클래스가 있고 이 클래스를 각각 상속받는 '학생'클래스와 '직장인'클래스가 있다.
통계를 내야하는 경우 둘 다 '사람'로 보고 자료를 조작해야 할 수도 있다.
'학생'를 '사람'로 캐스팅 해야 하는데, 사람이 상위 타입이므로 super를 통해 업캐스팅을 한다.

제네릭 타입에 대한 객체 비교

public class ClassName <E extends Comparable<? super E>> { ... }

PriorityQueue(우선순위 큐), TreeSet, TreeMap 같이 값을 정렬하는 클래스에서 특정 제네릭에 대한 자기 참조 비교를 하고 싶을 경우 대부분 공통적으로 위와 같은 형식을 취한다.

<E extends Comparable>
extends는 앞서 말했듯 extends 뒤에 오는 타입이 최상위 타입이 되고, 해당 타입과 그에 대한 하위 타입이다. 즉, E 객체는 반드시 Comparable을 구현해야한다.

public class GenericClass <E extends Comparable<E>> { ... }
 
public class Student implements Comparable<Student> {
	@Override
	public int compareTo(Person o) { ... };
}
 
public class Main {
	public static void main(String[] args) {
		GenericClass<Student> generic = new GenericClass<Student>();
	}
}

위와 같이 작성한 경우 E extends Comparable<E>까지만 써도 무방하다. 즉, GenericClass의 E 는 Student가 되어야 하는데, Comparable의 하위 타입이어야 하므로 Student 클래스는 Comparable을 구현해야 한다

  • Comparable<E> 가 아닌 <? super E>로 작성하는 이유
    super E는 E를 포함한 상위 타입 객체들이 올 수 있다. 만약에 위의 예제에서 학생보다 더 큰 범주의 클래스인 사람(Person)클래스를 두는 경우 코드는 다음과 같다.
public class GenericClass <E extends Comparable<E>> { ... }	// Error가능성 있음
public class GenericClass <E extends Comparable<? super E>> { ... }	// 안전성이 높음
 
public class Person {...}
 
public class Student extends Person implements Comparable<Person> {
	@Override
	public int compareTo(Person o) { ... };
}
 
public class Main {
	public static void main(String[] args) {
		GenericClass<Student> genericClass = new GenericClass<Student>();
	}
}

Person을 상속받고 Comparable 구현부인 compareTo에서 Person 타입으로 업캐스팅(Up-Casting) 한다.

만약 <E extends Comparable<E>>라면 GenericClass<Student> genericClass 객체가 타입 파라미터로 Student를 주지만, Comparable에서는 그보다 상위 타입인 Person으로 비교하기 때문에 Comparable<E>E인 Student보다 상위 타입 객체이기 때문에 제대로 정렬이 안되거나 에러가 날 수 있다.
그렇기 때문에 E 객체의 상위 타입, 즉 <? super E> 을 해줌으로써 그런 상황을 방지할 수가 있다.

즉, <E extends Comparable<? super E>> 는 쉽게 말하자면 E 타입 또는 E 타입의 슈퍼클래스가 Comparable을 의무적으로 구현해야한다는 뜻으로 슈퍼클래스타입으로 UpCasting이 발생하더라도 안정성을 보장받을 수 있다.

<E extends Comparable<? super E>> : "E 자기 자신 및 조상 타입과 비교할 수 있는 E"

<?>

<?><? extends Object>와 같다.
Object는 자바에서의 모든 API 및 사용자 클래스의 최상위 타입이다.

public class ClassName { ... }
public class ClassName extends Object { ... } 

이는 다음을 묵시적으로 상속 받는 것

public class ClassName extends Object {} 

<?>은 어떤 타입이든 관심이 없는 경우다. 보통 데이터가 아닌 '기능'의 사용에만 관심이 있는 경우에 <?>로 사용

REF

https://st-lab.tistory.com/153

profile
이름 짓는 게 어려운 사람

0개의 댓글