https://github.com/whiteship/live-study/issues/14
목차
- 제네릭 사용법
- 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
- 제네릭 메소드 만들기
- Erasure
1. 제네릭 사용법
제네릭의 장점
- 자료형을 매게 변수로 컴파일 타임에 타입 체크
- 다룰 객체의 타입을 미리 명시해줘서 번거로운 형변환 제거.
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);
위와 같이 제네릭을 사용하면, 불필요한 형변환을 생략하고 타입을 안정적으로 사용할 수 있다.
제네릭 사용법
Box 클래스를 non 제네릭과 제네릭 클래스로 작성해본다.
public class Box {
private Object objectl
public void set(Object object) { this.object = object; }
public Object get() { return object; }
}
public class Box<T> {
private T t;
public void set(T t) {this.t = t; }
public T get() { return t; }
}
제네릭 용어 정리
- Box<T> : 지네릭 클래스. T의 Box 혹은 T Box라고 부른다.
- T : Type parameter
- Box : 원시타입 ( raw type )
위의 예제의 경우 원시타입의 Box를 지네릭 클래스 Box<T>로 교체했고, Object 타입을 원하는 타입의 type parameter T로 대체했다. type parameter T는 class type, interface type, array type등 non primitive type의 어느 것이든 사용할 수 있다.
제네릭 클래스를 사용, 작성할 때 주의할 점
- 제네릭 타입으로 primitive type을 사용할 수 없다.
- 제네릭 타입으로 배열을 생성할 수 없다.
- 스태틱 멤버로 제네릭 타입을 사용할 수 없다.
배열을 생성할 때, 제네릭을 사용할 수 없는 이유
T[] myArray = new T[size];
T[] myArray = (T[]) new Object[size];
제네릭 타입을 사용해 배열을 생성할 때 클래스가 받은 T 타입으로 정의하고 싶지만, 아래줄 코드처럼 object으로 정의한 후 T[] 타입으로 형변환해야 한다.
이유는 new 연산자는 동적 메모리 할당 영역 heap영역에 객체를 할당하지만, 제네릭은 컴파일 타임에 타입을 체크해서 런타임이 돼야 실제로 T가 어떤 타입인지 알 수 있기 때문이다. 그래서 Object 배열로 생성 후 타입 캐스팅을 하는 것이다.
static 멤버로 제네릭을 사용할 수 없는 이유
static 멤버는 특정 객체와 상관 없이 모든 클래스가 공통으로 사용하고 클래스 이름으로 접근해서 사용할 수 있다. 예를 들어 위 예제에서 Box<Orange>, Box<Apple>과 같은 인스턴스를 만들었는데, 공유하는 멤버가 인스턴스마다 다른 타입으로 사용될 수가 없다.
그런데, static 메서드에는 제네릭을 사용할 수 있다. 왜냐하면, static 변수의 경우 사용하려면 해당 값의 타입을 알아야하지만, 제네릭 메서드의 타입 변수는 메서드 안에서만 사용되는 지역 변수이기 때문에 static으로 선언돼도 사용할 수 있다.
Type Parameter Naming Conventions
- E-Element
- K-Key
- N-Number
- T-Type
- V-Value
- S,U,V,etc - 2nd, 3rd, 4th types
기타
- primitive type을 사용하고 싶으면 Integer, Double 등 Wrapper class를 사용하면 된다.
- Box<Ineger> integerBox = new Box<>();
- 위와 같이 new 키워드 뒤의 type parameter는 생략가능하다.
2. 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
바운디드 타입
제네릭 파라미터로 사용하고 싶은 타입을 명시하면 그 타입만을 클래스 저장할 수 있게 제한할 수 있다.
그런데 모든 종류의 타입을 지정하는 것보다 타입의 종류를 제한해야할 경우가 있다.
제네릭 타입에 extends를 사용해서 특정 타입의 자손들만 대입할 수 있다.
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.getClass().getName());
System.out.println(u.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<>();
integerBox.set(new Integer(10));
integerBox.inspect("some text"); // error
}
}
inspect 메서드에서 타입 파라미터를 Number의 자손으로 제한했다. 따라서 inspect에서 "some text"의 string을 넣었을 때 에러가 발생한 것을 확인할 수 있다.
Multiple Bounds
<T extends A & B & C> 와 같이 여러가지 클래스 혹은 인터페이스로 제한을 둘 수 있다.
와일드 카드
와일드카드는 '?' 기호로 나타낼 수 있다. 제네릭으로 구현된 메서드는 선언된 타입으로만 매개변수를 입력해야하는데, 선언된 타입 뿐만 아니라 부모 혹은 자식 클래스를 사용하고 싶을때 활용할 수 있다.
<? extends T> : 와일드 카드의 상한 제한. T와 그 자손들이 가능하다.
<? super T> : 와일드 카드의 하한 제한. T와 그 조상들만 가능하다.
<?> : 제한이 없다.
와일드카드를 사용하지 않았을 때와 사용할 때 차이를 보면
class Juicer {
static Juice makeJuice(FruitBox<Fruit> box) {
String tmp = "";
for (Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
static Juice makeJuice(FruitBox<Apple> box) { //메서드 중복정의
String tmp = "";
for (Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
}
지네릭 타입만을 다르게 메서드를 정의하면 오버로딩이 성립하지 않아서 중복 정의로 에러가 발생한다. 만약 <Fruit>가 아닌 Fruit의 자손인 Apple, Banana 등 다른 타입을 사용하고 싶으면 와일드카드를 사용해서 다음과 같이 중복을 제거할 수 있다.
static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp = "";
for (Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
3. 제네릭 메소드 만들기
Generic methods는 고유의 type parameter를 가진 메서드다. 제네릭 타입을 선언하는 것과 비슷하지만, type의 범위가 제네릭 클래스와 다르게 선언된 메서드 내부에서만 유효하다.
Static, non-static 모두 가능하고 메서드의 return type 앞에 지네릭 타입을 선언해준다.
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());
}
}
Juicer의 makeJuice를 지네릭 메서드로 바꿔보겠다.
static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {
String tmp = "";
for (Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
복잡하게 선언된 지네릭 클래스의 이해
다음 예제는 Collections 클래스의 sort()이다.
public static <T extends Comparable<? super T>> void sort(List<T> list)
1. List<T>를 정렬한다.
2. T는 Comparable<T>의 구현체이다. 인터페이스라고 해서 implements를 사용하는 것이 아니라 extends를 사용한다.
3. 즉 List<T> list의 구성요소는 Comparable<T>를 구현한 객체이다.
4. 예를 들어 T가 Student이고 Student가 Person의 자손이라면 list의 구성요소는 Comparable<Student> 혹은 Comparable<Person>, Comparable<Object>를 구현한 클래스가 된다.
4. Erasure
Erasure란
컴파일러는 컴파일 타임에 타입 파라미터를 Object 혹은 제한하는 클래스 타입으로 교체한다.
bounded generic type의 경우 한계 타입으로 변경하고,
unbounded generic type일 경우 Object 타입으로 변경한다.
위에서 generic type으로 primive type을 사용할 수 없다고 했는데, Object 클래스를 상속받고 있지 않기 때문이다.
Erasure를 사용하는 이유는 하위 호환성을 지키기 위해서인데, 제네릭을 사용하지 않은 하위 버전에서도 동일하게 동작할 수 있다.
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
컴파일러는 T가 unbounded이기 때문에 다음과 같이 Object로 대체한다.
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; }
// ...
}
만약 다음과 같이 Comparable<T>로 bounded 됐을 경우
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
다음과 같이 Comparable로 대체한다.
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() { return data; }
// ...
}
Bridge Methods
public class Node<T> {
public T data;
public Node(T data) { this.data = data; }
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node<Integer> {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
Bridge method를 설명하기 전에 위의 예시에서 bridge 메서드 없이 Erasure 단계를 거치면 어떻게 변환될지 살펴본다.
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<Integer> {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
이렇게 되면 문제는 Erasure 후에 MyNode의 setData와 Node의 setData가 다른 파라미터 타입을 가진다는 것이다. 그렇다면 setData가 오버라이딩되지 않는다.
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);
}
// ...
}
그래서 위와 같이 Bridge Method를 생성하면 setData에 Object로 받은 data를 Intger로 생성해서 본래의 set(Integer data) 메서드를 호출할 수 있게 된다.
Reference
1. 자바 오라클 튜토리얼
2. 자바의 정석, 3판
'자바 > 백기선 자바스터디' 카테고리의 다른 글
[자바 스터디] 멀테쓰레드 프로그래밍 (0) | 2023.01.18 |
---|---|
[백기선 자바스터디] 람다식 (0) | 2022.12.30 |
[백기선 자바스터디] enum (0) | 2022.10.10 |
[백기선 자바스터디] 예외 처리 (0) | 2022.09.21 |
[백기선 자바스터디] 인터페이스 (0) | 2022.08.10 |