[Java] Type Casting, Generic
Published:
In this post, concept of Type Casting and Generic is introduced.
Type Casting
Java의 Type Casting 규칙은 크게 두 가지로 나눌 수 있다.
Primitive Type Casting
Implicit Casting (자동 형변환)
작은 type -> 큰 type 으로 자동 변환. 데이터 손실 없음.
int i = 10; long l = i; // int → long 자동 변환 float f = l; // long → float 자동 변환 double d = f; // float → double 자동 변환Explicit Casting (강제 형변환)
큰 타입 → 작은 타입 변환 시 반드시
(타입)을 붙여야 함. 데이터 손실, 오버플로우 가능.double d = 9.8; int i = (int) d; // 소수점 손실 → 9 int big = 1000; byte b = (byte) big; // 오버플로우 발생 → -24
Reference Type Casting
Up Casting
하위 클래스 → 상위 클래스 변환. 자동 형변환 가능
class Animal {} class Dog extends Animal {} Dog dog = new Dog(); Animal a = dog; // 자동 업캐스팅Down Casting
상위 클래스 → 하위 클래스 변환. 명시적 캐스팅 필요
(타입)잘못된 캐스팅 시
ClassCastException발생class Animal {} class Dog extends Animal {} Animal a = new Dog(); // 업캐스팅 Dog d = (Dog) a; // 안전한 다운캐스팅 (a의 실제 type이 dog나, dog의 하위 type이면 됨) /* new Dog()가 실행되면 힙(Heap) 메모리에 Dog 객체가 생성. 이 객체는 Animal 부분 + Dog가 추가한 부분을 모두 포함. 즉, 실제 메모리 크기는 Animal보다 커질 수 있음. 하지만 변수 a는 스택(Stack) 영역에 존재하며, 참조(주소)만 저장. a는 "Dog 객체의 시작 주소"만 알고 있을 뿐. Animal a라고 선언했으므로, 컴파일러는 a가 Animal이 가진 멤버만 접근 가능하다고 제한. 실제 객체는 Dog지만, 참조 변수의 타입이 어떤 멤버를 쓸 수 있는지를 결정. */ Animal a2 = new Animal(); Dog d2 = (Dog) a2; // 런타임 오류 (ClassCastException) /* 컴파일러는 표면상의(정적) 타입만 확인. (Animal a2 = ...; Dog d2 = (Dog) a2; 로 해석) a2의 정적 타입은 Animal, 캐스트 대상은 Dog. 위 예처럼 캐스팅이 가능한 경우도 있으므로 컴파일 타임 에러는 안남. */instanceof 연산자와 함께 사용
if (a instanceof Dog) { Dog d = (Dog) a; } /* Animal a = new Dog(); 인 경우에는 casting 됨. Animal a = new Animal(); 인 경우에는 casting 안됨. */
Boxing, UnBoxing
Primitive Type 과, Wrapper class 간의 자동 형 변환.
int i = 10; Integer obj = i; // 오토박싱 (int → Integer) int j = obj; // 오토언박싱 (Integer → int)Example
class Animal {} class Dog extends Animal {} class Puppy extends Dog {} Animal a = new Puppy(); // OK Dog d = (Dog) a; // OK
Generic
Generic 이란, 클래스, 인터페이스, 메서드 정의 시 타입을 매개변수처럼 받는 문법이다. **컴파일 타임**에 타입을 강제하여, 잘못된 타입 사용을 막고 형변환(casting)을 줄인다. 다음 예시를 통해 밑줄 친 부분의 의미를 이해해보자.
다음과 같이, Box 클래스를 정의하고, 여기 안에 어떤 Object 도 저장할 수 있다고 하자.
class Box {
private Object value;
public void set(Object value) {
this.value = value;
}
public Object get() {
return value;
}
}
아래 코드의 동작을 살펴보자.
Box b = new Box();
b.set(123);
Integer i = (Integer) b.get();
set 함수에 123 을 전달하면, 우선 auto-boxing이 일어나, Integer.valueOf(123) 을 통해 Integer 객체가 만들어진다.
이후, 파라미터에 전달되면 Object value = new Integer(123) 와 같이 동작한다고 이해할 수 있고 이는 Up casting 이므로 가능하다.
이제 get 함수를 통해 이를 변수에 저장하는데, 이때 (Integer) casting 을 반드시 해야한다. 컴파일러는 get 의 return type이 Object 라고 알고 있으므로, (Integer) 를 적지 않으면, Integer i = Object ... 로 이해해 컴파일 에러가 난다. 따라서, Down casting 을 해야하고 이 경우 런타임 에러가 나지 않고 정상적으로 동작할 것이다.
이렇게 Generic 을 사용하지 않고 코드를 작성하게 되면 다음의 문제점들이 생긴다.
함수 parameter 의 type이
Object이므로 어떤 type 도 들어갈 수 있다.잘못된 타입으로 Casting 하면, 런타임 에러 (
ClassCastException) 가 난다. 예를 들어, 위 예제에서 두 번째 줄만b.set("123")으로 바꾸면, 런타임 단계에서(Integer)부분과 일치하지 않으므로 에러가 난다.값을 꺼낼 때마다 명시적으로
(Integer)같은 casting 을 해야한다.
Generic 을 이용하면 이러한 문제점을 해결할 수 있다.
class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
// stringBox.set(123); // 컴파일 에러: String만 가능
String s = stringBox.get(); // 캐스팅 필요 없음
- 타입 강제:
Box<String>이라 선언하면String만 저장 가능. 잘못된 타입은 컴파일 단계에서 에러 발생. - 캐스팅 제거: 꺼낼 때
(String)같은 강제 형변환이 필요 없음.
Advanced Generic Grammar
1. Bounded Type Parameter
extends (상한 제한)
class Box<T extends Number> { // T는 Number 또는 그 하위 타입만 가능 private T value; public void set(T value) { this.value = value; } public T get() { return value; } } Box<Integer> b1 = new Box<>(); // OK Box<Double> b2 = new Box<>(); // OK Box<String> b3 = new Box<>(); // Compile Error. String은 Number 아님extends는 “상속”뿐 아니라 “구현(implements)”도 포함. 즉,T extends Interface도 가능
super (하한 제한)
// 특정 타입 이상의 부모 타입만 받을 수 있음 class Writer<T> { public void write(T data) { System.out.println("Write: " + data); } } // 사용 예시 Writer<? super Integer> w; w = new Writer<Number>(); // OK w = new Writer<Object>(); // OK
2. WildCard
Unbound WildCard
class Box<T> { private T value; public void set(T value) { this.value = value; } public T get() { return value; } } class Animal {} class Dog extends Animal {} class Cat extends Animal {} Box<?> b = new Box<Dog>(); Object obj = b.get(); // OK b.set(new Dog()); // Compile error b.set(new Cat()); // Compile errorUnbound WildCard 는 T 자리에 어떤 type 이든 올 수 있다는 것이다. 이렇게 되면
set함수는 사용할 수 없게 되는데, 컴파일러 입장에서는b.set(new Dog())가,? value = new Dog()로 받아들여지는데?type 을 알 수 없으니 컴파일 에러가 난다.Upperbounded WildCard
Box<? extends Animal> b = new Box<Dog>(); Animal a = b.get(); // OK 최소한 Animal 보장 Dog d = b.get(); // Compile error (사실은 Dog일 수도 있지만 보장 불가) b.set(new Dog()); // Compile error b.set(new Animal()); // Compile errorLowererbounded WildCard
Box<? super Dog> b = new Box<Animal>(); b.set(new Dog()); // OK b.set(new Cat()); // Compile error Object obj = b.get(); // OK Dog d = b.get(); // Compile error
3. Genric Method
public static <T> T echo(T value) {
return value;
}
String s = echo("Hi"); // T = String
Integer i = echo(123); // T = Integer
컴파일러는 호출 시점에 타입을 추론한다. 하지만 실제로 바이트코드로 변환되면 T는 존재하지 않고, Object로 바뀐다.
public static Object echo(Object value) {
return value;
}
generic method를 사용하지 않는다면, 다음 둘 중 하나의 불편함을 겪어야 한다.
- 같은 역할을 하는 함수를 type 마다 Overlaoding 해서 여러 번 적어야 한다.
- 그렇지 않다면 매개변수, 리턴 type을 Object 로 하고, 그 결과를
String s = (String) echo("Hi");처럼 반드시 Down casting 해야 한다.
4. Generic Array is Banned
자바의 배열은 런타임에 타입이 보존(reified) 된다.
String[] arr = new String[10]; arr[0] = "hi"; // OK arr[1] = 123; // ArrayStoreException (컴파일러도 잡아냄)배열은 실행 중에도 “나는 String 배열이다”라는 정보가 유지된다. 따라서, JVM이 타입을 검사할 수 있고, 잘못된 값이 들어오면 예외를 던진다. 물론 위 예시에서는 컴파일 타임에 에러가 난다.
제네릭은 타입 소거(Type Erasure) 때문에 런타임에 타입 정보가 사라진다.
Box<String> b1 = new Box<>(); Box<Integer> b2 = new Box<>(); System.out.println(b1.getClass() == b2.getClass()); // true둘 다 단순히
Box일 뿐,Box<String>인지Box<Integer>인지는 알 수 없다.배열은 런타임에 “나는 Box
배열"이어야 한다는 정보를 가져야 한다. 그런데 제네릭은 런타임에 타입이 지워져서 `Box`로만 존재한다. 즉, "`Box ` 배열"이라는 런타임 타입을 만들 수 없기 때문에 **컴파일 에러**가 난다. Box<String>[] arr = new Box<String>[10]; // 금지만약 이러한 문법이 허용된다고 가정하고 어떤 문제가 생기는지를 알아보자.
Box<String>[] arr = new Box[10]; // 타입 소거 때문에 사실상 이렇게 동작 Object[] objArr = arr; // 배열은 covariant이므로 가능 objArr[0] = new Box<Integer>(); // 런타임 시 타입 안전 위배!objArr에Box<Integer>를 넣어버릴 수 있게 된다. 그런데 원래는Box<String>[]이었으므로, 이후arr[0].get()이 String일 거라 믿고 코드가 작성되었을 수 있음. 이렇게 되면 타입 안정성이 완전히 깨져버린다.참고로, 배열이 Covariant 하다는 것은 만약
S가T의 하위 타입이면,S[]도T[]의 하위 타입임을 의미한다. 반대로 제네릭은 불공변(invariant)이므로,Box<String>은Box<Object>의 하위 타입이 아니다.그럼에도 불구하고, Generic 을 이용해 배열을 만들고 싶다면 WildCard를 아래처럼 이용할 수 있다.
Box<?>[] arr = new Box<?>[10];Box<?>는 내부 타입을 모르는 “모든Box“를 의미.arr[0] = new Box<String>();도 가능,arr[1] = new Box<Integer>();도 가능. 대신arr[0].set(...)같은 쓰기 작업은 불가능 하게 되어 (위에 WildCard 예제 참고) 안전성이 확보된다. 즉, ?를 쓰면 “타입을 모른다”는 걸 명시했기 때문에, 컴파일러가 위험한 연산을 막아준다.
Final Example
public class BinarySearchTree<Key extends Comparable<? super Key>, E> {
...
}
다음과 같은 Java Class의 선언부를 보자. 이해를 위해 우선 Comparable 인터페이스에 대해 알아보자.
Comparable Interface
Comparable<T> 는 “자기 자신을 T 타입과 비교할 수 있다”는 인터페이스이다.
public interface Comparable<T> {
int compareTo(T o);
}
아래와 같이 Comparable<Dog> 라 적으면 컴파일러에 의해 compareTo 함수의 인자가 Dog type 이 되도록 강제된다.
class Dog implements Comparable<Dog> {
private int age;
public Dog(int age) { this.age = age; }
@Override
public int compareTo(Dog other) {
return this.age - other.age; // 나이 기준 오름차순
}
}
이제 다시, 선언문을 보자.
Key또는Key의 상위 타입만을 generic type으로 허용하는Comparable인터페이스를 구현한 클래스Key와E를 generic type으로 사용.Key extends Comparable: 기본적으로Key는Comparable을 구현해야 한다는 뜻. (즉, 정렬 가능한 타입만 키로 사용할 수 있다는 제약)<? super Key>처럼 복잡하게 쓰는 이유 : 하위 타입을 상위 타입과 비교할 수 있게 하기 위한 유연성 확보class Animal implements Comparable<Animal> { ... } class Dog extends Animal { ... }Dog extends Comparable<? super Dog>가 성립한다. 왜냐하면, 이것이 성립하려면Dog가Comparable<T>를 구현하고 있어야 하는데, 이 때T는 구체적으로,Dog와 그의 부모 클래스에 해당한다. 그런데,Animal이Comparable<Animal>을 구현하고 있으므로 그 자식 클래스인Dog는 당연히Comparable<Animal>을 구현하고 있는 상태이다. 그러므로 BST의Key로Dog를 사용가능하다. 참고로Dog는Animal의 자식 클래스이므로Dog는Animal과 비교 가능하다 (Animal의compareTo메서드를 이용하여).<? super Key>가 없었다면,Dog extends Comparable<Dog>가 성립하지 않으므로(컴파일 에러가 나므로),Dog를Key로 사용할 수 없다.

Leave a Comment