https://github.com/whiteship/live-study/issues/14
목표
자바의 제네릭에 대해 학습하세요.
학습할 것 (필수)
- 제네릭 사용법
- 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
- 제네릭 메소드 만들기
- Erasure
제네릭 사용법
2004년 J2SE(Java 2 Platform, Standard Edition. 6.0 버전 이후 'Java SE'로 변경) 5.0 버전에 추가된 기능입니다. 자바의 타입 시스템을 확장해 "컴파일 시 타입 안정성을 제공하면서 다양한 타입의 객체에서 작동하는 타입 또는 메소드"를 제공하도록 설계되었습니다.
Java 컬렉션 프레임 워크는 컬렉션 인스턴스에 저장된 오브젝트 타입을 지정하는 제네릭을 지원합니다.
다음 Java 코드는 제네릭을 사용하지 않을 때 존재하는 문제를 보여줍니다.
import java.util.List;
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
List list = new ArrayList();
list.add("hello"); // 문자열은 정수 타입으로 형 변환이 불가능합니다.
Integer i = (Integer) list.get(0); // 런타임 오류
}
}
Exception in thread "main" java.lang.ClassCastException:
class java.lang.String cannot be cast to class java.lang.Integer
(java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
at com.testcompany.Main.main(Main.java:11)
코드는 오류 없이 컴파일되지만, 메인 코드의 세 번째 줄을 실행할 때 런타임 예외(java.lang.ClassCastException)가 발생합니다. 이러한 유형의 논리 오류를 제네릭을 사용하여 컴파일 시간에 감지할 수 있으며 이를 사용하는 주요 동기입니다.
import java.util.List;
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
List<String> v = new ArrayList<String>();
v.add("test");
Integer i = (Integer) v.get(0); // (타입 오류) 컴파일 오류
}
}
cast(형 변환) 제거
다음 코드는 제네릭을 사용함으로써 캐스팅이 불필요해짐을 보여줍니다.
import java.util.List;
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
List list = new ArrayList();
list.add("hello");
Strign s = (String) list.get(0);
}
}
import java.util.List;
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); // 형 변환 불필요
}
}
- 간단한 상자 클래스
public class Box {
private Object object;
public void set(Object object) { this.object = object; }
public Object get() { return object; }
}
컴파일 시간에는 클래스가 어떻게 사용됐는지 확인할 방법이 없습니다. Integer 타입이 통과하고 다른 파트에서 실수로 String 값을 통과 시켜 런타임 에러를 발생할 수 있습니다.
- 제네릭 버전 상자 클래스
제네릭 클래스는 다음과 같은 형식으로 정의합니다.
class name<T1, T2, ..., Tn> { /* ... */ }
클래스 이름 다음으로 홑화살괄호(부등호 기호)로 구분된 타입 매개변수(parameter)가 있습니다. 타입 매개변수(타입 변수라고도 합니다.)는 'T1, T2, ..., Tn'처럼 열거합니다.
public class Box<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
Object는 모두 T로 대체됩니다. 똑같은 테크닉으로 제네릭 인터페이스도 만들 수 있습니다.
- 타입 파라미터 명명 규칙
규칙에 따라 타입 파라미터(유형 매개변수) 이름은 단일 대문자입니다. 이미 알고 있는 변수 명명 규칙과 다르지만 그럴만한 이유가 있습니다. 이 규칙이 없으면 타입 변수와 일반 클래스 또는 인터페이스 이름의 차이를 구분하기 어려울 겁니다.
가장 일반적으로 사용되는 타입 파라미터 이름은 다음과 같습니다.
- E - Element (자바 컬렉션 프레임워크에서 광범위하게 사용)
- K - Key
- N - Number
- T - Type
- V - Value
- S, U, V etc. - 2nd, 3rd, 4th types
- 제네릭 유형 호출 및 인스턴스화
코드 내에서 제네릭 박스 클래스를 참조하려면 T를 Integer와 같은 구체적인 값으로 대체해 호출해야 합니다.
Box<Integer> integerBox;
클래스를 인스턴스화 하려면 평소처럼 new 키워드를 사용하되 클래스 이름과 괄호 사이에 <Integer>를 넣습니다.
Box<Integer> integerBox = new Box<Integer>
- 다이아몬드
Java SE 8 이상부터 컴파일러가 컨텍스트에서 타입 인수(arguments)를 결정하거나 추론할 수 있으면 제네릭 클래스에서 생성자를 호출하는데 필요한 타입 인수를 (<>) 세트로 바꿀 수 있습니다. 비공식적으로 다이아몬드라고 합니다.
Box<Integer> integerBox = new Box<>();
출처:
https://en.wikipedia.org/wiki/Generics_in_Java
https://docs.oracle.com/javase/tutorial/java/generics/why.html
제네릭 주요 개념 (바운디드 타입, 와일드 카드)
- 바운디드 타입 파라미터
타입 인수(type arguments)를 제한하려는 경우에 사용합니다. 예를 들어 숫자에 작동하는 메소드는 Number 또는 subclass 인스턴스만 허용하려고 할 수 있습니다. 이것이 바운디드 타입 파라미터(경계 유형 매개변수)의 용도입니다.
바운디드 타입 파라미터를 선언하려면 타입 파라미터의 이름, extends 키워드, 상위 경계(upper bound)(예시에서는 Number)를 나열합니다.
이 컨텍스트에서 extends는 확장(extends;클래스에서 사용되는) 또는 구현(implements;인터페이스에서 사용되는)의 일반적인 의미로 사용됩니다.
public <U extends Number> void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
바운드에 정의된 메소드 실행도 가능합니다.
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 메소드를 호출합니다.
- 와일드 카드
제네릭 코드에서 와일드카드라고 하는 물음표(?)는 알 수 없는 타입을 나타냅니다. 와일드카드는 파라미터, 필드, 지역 변수의 타입 등 다양한 상황에서 사용할 수 있습니다.
public static void process (List <? extends Number> list) {/ * ... * /}
상한 제한 와일드카드(Upper Bounded Wildcards)는 예시처럼 Number 타입 또는 상속받는 subclass를 사용합니다.
public static void printList (List <?> list) {/ * ... * /}
무제한 와일드카드(Unbounded Wildcards)는 <?>를 사용합니다.
public static void addNumbers (List <? super Integer> list) {/ * ... * /}
하위 제한 와일드카드(Lower Bounded Wildcards)는 해당 타입과 superclass로 제한합니다.
제네릭 메소드 만들기
제네릭 메소드는 자체 타입 파라미터(형식 매개변수)를 사용합니다. 제네릭 타입을 선언하는 것과 비슷하지만 타입 파라미터 제한은 메소드 범위까지만 제한됩니다. 제네릭 클래스 생성자뿐만 아니라 static, non-static 제네릭 메소드도 사용 가능합니다.
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());
}
// ...
}
메소드를 호출하는 구문은 다음과 같습니다.
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
// 타입을 생략 가능하며 컴파일러가 필요한 타입을 추론합니다.
boolean same = Util.compare(p1, p2);
Erasure
제네릭을 구현하기 위해 Java 컴파일러는 다음과 같은 상황에서 타입 제거(type erasure)를 적용합니다.
- 제네릭 타입의 모든 타입 파라미터를 해당 바운드나 Object(unbounded 경우)로 교체합니다. 그러므로 생성된 바이트 코드에는 일반 클래스, 인터페이스 및 메소드만 포함됩니다.
- 타입 안전을 유지하기 위해 필요한 경우 타입 캐스트를 삽입합니다.
- 확장된 제네릭 타입의 다형성(polymorphism)을 보존하는 브릿지(bridge) 메소드를 생성합니다.
타입 제거(type erasure)는 parameterized(매개변수화) 타입에 새 클래스가 생성되지 않도록 합니다. 결과적으로 런타임 오버헤드를 발생시키지 않습니다.
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
// ...
타입 파라미터 T가 unbounded이기 때문에 컴파일러는 Object로 교체합니다.
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
// ...
다음 예제는 제한된(bounded) 타입 파라미터를 사용합니다.
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
// ...
컴파일러는 bounded 타입 파라미터 T를 첫번째 바운드 클래스 Comparable로 교체합니다.
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
// ...
타입 제거 프로세스 일부로 브리지 메소드를 만들어야 할 수 있습니다. 일반적으로 브리지 메소드에 대해 걱정할 필요는 없지만, 스택 추적에 나타나는 경우 당황할 수 있습니다.
public class Node {
public Object data;
public Node(Object data) { this.data = data; }
public void setData(Object data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
타입 삭제 후 메소드의 시그니쳐가 일치하지 않습니다. Node 메소드는 setData(Object)가 되고 MyNode 메소드는 setData(Integer)가 됩니다. 따라서 MyNode setData 메소드는 Node setData 메소드를 재정의하지 않습니다.
이 문제를 해결하고 다형성을 보존하기 위해 컴파일러는 subtype이 예상대로 작동하는지 확인하는 브릿지 메소드를 생성합니다.
class MyNode extends Node {
// Bridge method generated by the compiler
//
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
// ...
}
출처: https://docs.oracle.com/javase/tutorial/java/generics/erasure.html