https://sundaland.tistory.com/160
[ ▶ 제너릭 (Generics) ]
클래스, 엔터페이스 및 메서드를 정의 할때 타입(클래스 및 인터페이스)을 파라미터로 사용할 수 있다.
메서드 선언에 사용되는 formal 파라미터와 마찬가지로, 타입 파라미터는 다른 입력으로 동일한 코드를 재사용할 수 있는 방법을 제공한다. (템플릿이라고도 한다.)
formal 파라미터에 대한 입력은 값이지만, 타입 파라미터에 대한 입력은 타입이다.
- 컴파일 시간에 더 강력한 타입 검사. 자바 컴파일러는 제너릭 코드에 강력한 타입 검사를 적용하기에, 컴파일 오류를 발견하기 쉽다.
- 캐스트 제거. 해당 리스트의 엘리먼트 타입을 알 수 없다. (raw 타입은 제너릭 JPA에서 여전히 사용되고 있다.)
▼ 제너릭이 없는 아래의 코드 스니펫은 캐스팅이 필요하다.
List list = new ArrayList(); // raw 타입
list.add("hello"); // 엘리먼트 데이터 타입은 String 클래스 객체에 대한 참조
String s = (String) list.get(0);
▼ 제너릭을 사용하기 위해 다시 작성할 때, 코드는 캐스팅이 필요하지 않다.
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); // no cast
- 프로그래머가 제너릭 알고리즘을 구현할 수 있다. 제너릭을 사용하여 프로그래머는 다양한 타입의 컬렉션에서 작동하고, 사용자가 정의할 수 있으며, 타입이 안전하고 읽기 쉬운 제너릭 알고리즘을 구현할 수 있다.
[ ▷ 제너릭 타입 ]
타입에 대해 파라미터화된 제너릭 클래스 또는 제너릭 인터페이스, 제너릭 메서드를 말한다.
public class Box {
private Object object;
public void set(Object object) { this.object = object; }
public Object get() { return object; }
}
위 코드의 Box 객체 메서드는 객체를 받아들이거나 반환하기 때문에. primitive 타입이 아니라면 무엇이든 자유롭게 전달이 가능하다.
하지만 컴파일 타임에 클래스가 어떻게 사용되는지 확인할 수 없다. 만약 Integer 클래스 객체를 넣고 코드로 String을 전달한다면 컴파일러는 컴파일 타임때 오류를 감지하지 못해서 런타임때 오류가 발생한다.
[ ▷ 제너릭 클래스 선언 방법 ]
class name<T1, T2, ..., Tn> { /* ... */ }
괄호( < > )로 구분된 타입 파라미터 섹션은 클래스 이름 뒤에 온다. 타입 파라미터(변수)를 지정한다.
아래의 코드는 클래스 내 어디에서나 사용할 할 수 있는 타입 변수 T를 만든다.
/**
* Generic version of the Box class.
* @param <T> the type of the value being boxed
*/
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
Object의 모든 occurrences는 전부 T로 대체하였다. 타입 변수는 지정한 모든 non-primitive 타입이 될 수 있다.
# 모든 클래스 타입, 모든 인터페이스 타입, 모든 배열 타입, 다른 타입 변수 (제너릭 메서드 타입 파라미터)
이와 같은 기술은 제너릭 인터페이스를 만들때 적용할 수 있다.
[ ▷ 타입 파라미터 명명법 ]
관례에 따라, 타입 파라미터의 이름은 단일 대문자이다. 일반적인 변수 명명법과 대조된다.
이는 타입 변수와 제너릭 클래스 또는 인터페이스와 이름을 구별하기 어렵기 때문이다.
- E (Element) # 자바 컬랙션 프레임 워크에서 사용됨
- K (Key)
- N (Number)
- T (Type)
- V (Vaule)
- S, U, V etc (2nd, 3nd, 4th...)
[ ▷ Invoking and Instantiating a Generic Type ]
코드 내에서 제너릭 Box 클래스를 참조하려면, T를 정수와 같은 구체적인 값을 대체하는 제네릭 타입 호출을 수행해야 한다,
Box<Integer> integerBox;
제너릭 타입 호출은 일반 메서드 호출과 유사해 보이지만, 메서드에 아규먼트를 전달하는 대신에 타입 아규먼트를 Box 클래스 자체에 전달하는 것이다.
다른 변수 선언처럼 실제로 새로운 Box 객체를 만드는 것이 아니고, 단순히 integerBox가 Box<Integer>를 읽는 방법인 "Box of Integer"에 대한 참조를 보유할 것이라고 선언한다.
제네릭 타입의 호출을 일반적으로 파라미터화된 타입으로 알려져 있다.
해당 클래스의 인스턴스화는 이렇다.
Box<Integer> integerBox = new Box<Integer>();
[ ▷ 타입 파라미터와 타입 아규먼트 ]
많은 개발자들은 타입 파라미터와 타입 아규먼트라는 용어를 서로 바꿔서 사용하지만, 이 용어들은 동일하지 않다. 코딩시 파라미터화된 타입을 만들기 위해 타입 아규먼트를 제공한다. 따라서 Foo<T>는 T의 파라미터이고 Foo<String> f의 String은 타입 아규먼트이다.
[ ▷ The Diamond ]
자바 SE 7 이상에서 컴파일러가 컨텍스트에서 타입 아규먼트를 결정하거나 추론(Interface)할 수 있는 한, 제너릭 클래스의 생성자를 호출하는데 필요한 타입 아규먼트를 텅빈 아규먼트( < > )로 대체할 수 있다.
이 한 쌍의 각 괄호는 비공식적으론 다이아몬드라고 불린다.
Box<Integer> integerBox = new Box<>();
[ ▷ 다중 타입 파라미터 ]
제네릭 클래스는 여러 타입 파라미터를 가질 수 있다.
▼ Pair 인터페이스를 구현하는 제너릭 OrderedPair 클래스
public interface Pair<K, V> {
public K getKey();
public V getValue();
}
public class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String, String> p2 = new OrderedPair<String, String>("hello", "world");
새로운 OrderedPair<String, Inteter> 코드는 K를 String 클래스로, V를 Integer 클래스로 인스턴스화 한다. 따라서 OrderedPair의 생성자의 파라미터 타입은 String과 Integer이다. 오토박싱으로 인해, 클래스에 문자열 리터럴과 int를 전달하는 것이 유효하다.
자바 컴파일러는 OrderedPair<String, Integer> 선언에서 K와 V 타입을 추론할 수 있기 때문에 다이아몬드 표기법을 사용하여 단축할 수 있다.
OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8);
OrderedPair<String, String> p2 = new OrderedPair<>("hello", "world");
제너릭 인터페이스는 제너릭 클래스를 만드는 것과 동일한 규칙을 따른다.
타입 파라미터(K, V)를 파라미터화된 타입(Box<Integer>로 대체할 수 있다.
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
[ ▷ Raw Type ]
타입 아규먼트가 없는 제너릭 클래스 또는 인터페이스의 이름이다.
Box rawBox = new Box();
위 코드의 Box는 제너릭 타입 Box<T>의 raw 타입이다. 그러나 비제너릭 클래스 또는 인터페이스 타입은 raw 타입이 아니다.
많은 API 클래스가 JDK 5.0 이전에 제너릭하지 않았기 때문에 raw 타입은 레거시 코드에 표시된다.
raw 타입을 사용한다면 본질적으론 사전에 제너릭 동작을 얻을 수 있다.
Box는 우리에게 객체를 제공하고 이전 버전과의 호환성을 위해, 파라미터화된 타입을 raw 타입에 할당하는 것이 혀용된다.
Box<String> stringBox = new Box<>();
Box rawBox = stringBox; // OK
하지만 파라미터화된 타입에 raw 타입을 할당하면 경고가 발생한다.
Box rawBox = new Box(); // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox; // warning: unchecked conversion
또한 제너릭 타입에 정의된 제너릭 메서드를 호출하기 위해 raw 타입을 사용하는 경우에도 경고가 발생한다.
Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8); // warning: unchecked invocation to set(T)
warning은 raw 타입이 제너릭 타입 체크를 우회하여, 안전하기 않은 코드의 캐치를 런타임으로 연기한다를 보여준다
그러므로 raw 타입은 사용하지 말아야한다.
Unchecked Error Message
레거시 코드와 제너릭 코드를 혼합할 때, 다음과 유사한 경고 메세지가 출력된다.
Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
다음 예시 코드처럼 raw 타입에서 작동하는 오래된 API를 사용할 때 발생할 수 있다.
public class WarningDemo {
public static void main(String[] args){
Box<Integer> bi;
bi = createBox();
}
static Box createBox(){
return new Box();
}
}
unchecked 라는 용어는 컴파일러가 타입 안전을 보장하는데 필요한 모든 타입 검사를 수행하기에 충분한 타입 정보를 가지고 있지 않다는 것을 의미한다.
컴파일러가 힌트를 제공하지만, unchecked 경고는 기본적으로 비활성화되어 있다.
모든 경고를 보려면 Xlint:unchecked로 다시 컴파일해야한다.
[ ▷ 제너릭 메서드 ]
자체적인 타입 파라미터를 도입하는 메서드이다. 이는 제너릭 타입을 선언하는 것과 유사하지만, 제너릭 메서드의 타입 파라미터 범위는 해당 선언된 메서드로 제한된다.
정적 및 비정적 제너릭 메서드뿐만 아니라 제너릭 클래스 생성자도 허용된다.
제너릭 메서드의 구문에는 메서드의 리턴 타입 앞에 나타나는 꺽쇠 괄호 <>내에 타입 파라미터 리스트가 포함된다.
정작 제너릭 메서드의 경우 타입 파라미터 섹션은 메서드의 반환 타입 앞에 나와야 한다.
아래의 Util 클래스에는 두 개의 Pair 객체를 비교하는 제너릭 메서드인 compare가 포함되어 있다.
public class Util {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
아래의 코드는 이 메서드를 호출하기 위한 완전한 구문이다.
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
하지만 제너릭 메서드의 호출 타입은 호출시 타입 파라미터가 명시적으로 지정되어 있다. 일반적으론 이를 생략하고 컴파일러가 필요한 타입을 추론하게 할 수 있다.
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);
이 기능은 타입 추론(Type Inference)라고 하며, 각 괄호 사이에 타입을 지정하지 않고 일반적인 메서드처럼 제너릭 메서드를 호출할 수 있게 해준다.
[ ▷ 제한된 타입 파라미터 (Bounded) ]
타입 아규먼트로 사용할 수 있는 타입을 제한하고자 할 때 사용한다.
제한된 타입 파라미터를 선언하려면, 타입 파라미터 이름 다음에 extends 키워드를 작성한 후 상한제한을 지정한다.
# 여기서는 extends(상속) 또는 implements(구현)을 의미하는 것으로 사용된다.
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem)
해당 코드는 T가 Comparable 인터페이스를 구현한 구체로 제한한다는 의미이다.
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public <U extends Number> void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(new Integer(10));
integerBox.inspect("some text"); // 에러: 여전히 String입니다!
}
}
제한된 타입 파라미터를 사용하도록 제너릭 메서드를 수정하여 main 메서드의 inspect 메서드 호출이 여전히 String 클래스 객체 참조를 전달하고 있기 때문에 컴파일에 실패한다.
제너릭 타입을 인스턴스화하는데 사용할 수 있는 타입을 제한하는 것 의외에도 제한된 타입 파라미터를 사용하면 범위에 정의된 메서드를 호출할 수 있다. (n.intValue:n은 Integer 또는 Integer 클래스를 상속한 서브 클래스)
public class NaturalNumber<T extends Integer> {
private T n;
public NaturalNumber(T n) { this.n = n; }
public boolean isEven() {
return n.intValue() % 2 == 0;
}
// ...
}
isEven 메서드는 n을 통해 Integer 클래스에 정의된 intValue 메서드를 호출한다.
# 짝수를 찾는 코드이다.
[ ▷ Multiple Bounds ]
타입 파라미터에는 여러 개의 상한을 지정할 수도 있다.
<T extends B1 & B2 & B3>
여러개의 상한을 가진 타입 변수는 상한에 나열된 모든 하위 타입이다. 상한 중 하나가 클래스인 경우, 첫 번째로 지정되어야 한다.
class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }
상한 A가 먼저 지정되지 않으면 컴파일 오류가 발생한다.
class D <T extends B & A & C> { /* ... */ } // 컴파일 오류
[ ▷ 제너릭 메서드와 제한된 타입 파라미터 ]
제한된 타입 파라미터는 제너릭 알고리즘 구현의 핵심이다.
다음 코드는 호출시 전달된 elem보다 큰 배열 T[ ]의 엘리먼트 e 갯수를 계산하는 방법이다.
public static <T> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e > elem) // compiler error
++count;
return count;
}
큼 ( > ) 연산자는 기본(Primitive) 타입에만 적용되기 때문에 컴파일되지 않는다. 그래서 컴파일 오류가 발생한다.
이를 해결하려면 Comparable<T> 인터페이스로 묶인 타입 파라미터를 사용한다.
public interface Comparable<T> {
public int compareTo(T o);
}
Integer 클래스는 Comparable<T> 인터페이스를 구현한다.
public final class Integer extends Number
implements Comparable<Integer>, Constable, ConstantDesc {
// 코드 생략 ...
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
}
countGreaterThan 메서드를 다음과 같이 수정한다.
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e.compareTo(elem) > 0)
++count;
return count;
}
countGreaterThan 메서드를 호출하는 코드를 실행한다.
public class Main {
public static void main(String[] args) {
// Integer 배열 사용 예제
Integer[] intArray = {1, 2, 3, 4, 5};
Integer intElem = 3;
int intCount = countGreaterThan(intArray, intElem);
System.out.println("Number of elements greater than " + intElem + ": " + intCount);
// String 배열 사용 예제
String[] strArray = {"apple", "banana", "cherry", "date"};
String strElem = "banana";
int strCount = countGreaterThan(strArray, strElem);
System.out.println("Number of elements greater than \"" + strElem + "\": " + strCount);
// Double 배열 사용 예제
Double[] doubleArray = {1.1, 2.2, 3.3, 4.4, 5.5};
Double doubleElem = 3.3;
int doubleCount = countGreaterThan(doubleArray, doubleElem);
System.out.println("Number of elements greater than " + doubleElem + ": " + doubleCount);
}
public static <T extends Comparable<T>> int countGreaterThan(
T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e.compareTo(elem) > 0)
++count;
return count;
}
}
Number of elements greater than 3: 2
Number of elements greater than "banana": 2
Number of elements greater than 3.3: 2
[ ▷ 제너릭, 상속, 서브 타입 ]
타입이 호환되는 경우 한 타입의 객체를 다른 타입의 객체에 할당할 수 있다.
예를 들어 Object는 Integer의 조상 클래스이므로 Integer를 Object에 할당할 수 있다.
Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // OK
객체지향 용어에서는 이것을 is a 관계라고 한다. Integer는 일종의 Object 이므로 대입이 허용된다.
그러나 Integer도 일종의 Number 이므로 아래의 코드도 유효하다.
public void someMethod(Number n) { /* ... */ }
someMethod(new Integer(10)); // OK
someMethod(new Double(10.1)); // OK
제너릭도 마찬가지로 Number를 타입 아규먼트로 전달하여 일반 타입 호출을 수행할 수 있으며, 아규먼트가 Number와 호환되는 경우 후속 add 호출이 허용된다.
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
public void boxTest(Box<Number> n) { /* ... */ }
해당 메서드의 시그니처는 타입이 Box<Number>인 단일 아규먼트를 허용하는 것을 알 수 있다.
하지만 Box<Integer> 및 Box<Double>은 Box<Number>의 하위 타입이 아니기 때문에 전달할 수 없다.
제너릭 클래스와 하위 타입 (Subtyping)
제너릭 클래스 또는 인터페이스를 확장하거나 구현하여 하위 타입을 지정할 수 있다. 특정 클래스 또는 인터페이스의 타입 파라미터와 다른 클래스 또는 인터페이스의 타입 파라미터 간의 관계는 extends 및 implements 절에 의해 결정된다.
타입 파라미터를 변경하지 않는 한 하위 타입 지정 관계는 타입간에 유지된다.
'자바 튜토리얼' 카테고리의 다른 글
제너릭 (Generics) [3] (0) | 2024.07.17 |
---|---|
제너릭 (Generics) [2] (0) | 2024.07.16 |
인터페이스와 상속 (Interface and Inheritance) [2] (0) | 2024.07.12 |
리팩토링 (Refactoring) (0) | 2024.07.11 |
인터페이스와 상속 (Interface and Inheritance) [1] (0) | 2024.07.08 |