📚 참고한 책
- 기본적인 개념을 정리하고 추가적으로 궁금한 것들을 정리했습니다 -
🐥 🐥 🐥
✔️ 제네릭을 사용하지 않는 경우_ 585p
• 객체를 저장하는 클래스
class Onion { }
class Goods1 {
private Onion onion = new Onion();
public Onion get() {
return onionl
}
public void set(Onion onion) {
this.onion = onion;
}
}
Goods1 g1 = new Goods1();
g1.setOnion(new Onion());
Onion onion = (Onion)g1.get();
Onion 클래스와 Onion 클래스를 담을 수 있는 Goods1 클래스가 있다. Goods1은 Onion 클래스만 담을 수 있으니 Onion이 아닌 다른 것을 담고 싶어지면 새로운 클래스를 생성해야한다. 하지만 어떤 코드는 하나의 클래스에 다양한 클래스를 담고 싶을 수도 있다.
• Object로 정의한 클래스
class Onion {}
class Radish {}
class Goods {
private Object object = new Object();
public Object get() {
return object;
}
public void set(Object object) {
this.object = object;
}
}
Goods g1 = new Goods();
g1.set(new Onion());
Onion onion = (Onion)g1.get();
Goods g2 = new Goods();
g2.set(new Radish());
Radish radish = (Radish)g2.get();
Onion과 Radish는 모두 Object 타입의 자식이다. 왜냐하면 자바에서 모든 클래스의 최상위 부모는 Object를 상속받기 때문이다. 예제 코드는 set( ) 메서드를 통해 객체를 저장하고, get( ) 메서드를 통해 저장된 값을 꺼낸다.
맨 처음 제너릭 없이 객체를 저장한 클래스의 예시를 살펴보자. 객체를 저장할 때 g1.setOnion이라고 적었다. 하지만 Object로 정의한 코드를 살펴보면 g1.set(new Onion());으로 Onion이 Object 타입으로 저장되어있다. 이 점에서 두 코드의 차이를 확인할 수 있다.
Object 타입으로 저장했으니 get에서도 Object 타입으로 꺼낼 것 같지만 사실 그렇지 않다. 꺼낼 때는 객체를 사용하기 위해 Onion과 Radish 객체로 다운 캐스팅해야 한다. 만약 잘못 캐스팅이 된 경우 ClassCastException 실행 예외가 발생하며 프로그램이 종료된다. 이를 약한 타입 체크라고 한다.
✔️ 제네릭 문법_ 591p
• 제네릭
: 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법
• 강한 타입 체크
: 잘못된 캐스팅을 할 때 문법 오류를 발생시켜 잘못된 캐스팅으로 발생할 수 있는 문제를 사전에 예방
• 제네릭 타입 변수의 관례적 표기 및 의미
T | 타입(Type) |
K | 키(Key) |
V | 값(Value) |
N | 숫자(Number) |
E | 원소(Element) |
class KeyValue<K, V> {
private K key;
private V value;
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public void setValue(V value) {
this.value = value;
}
}
KeyValue<String, Integer> kv1 = new KeyValue<>();
kv1.setKey("사과");
kv1.setValue(1000);
String key1 = kv1.getKey();
int value1 = kv1.getValue();
System.out.println("key: " + key1 + "value: " + value1);
KeyValue<String, Void> kv2 = new KeyValue<>();
kv2.setKey("키 값만 사용");
String key2 = kv2.getKey();
System.out.println("key: " + key2);
kv1과 kv2의 타입이 다르게 설정할 수 있는 이유는 제네릭 클래스는 클래스를 정의하는 시점에 타입을 지정하는 것이 아닌 객체를 생성하는 시점에 타입을 지정하기 때문이다. 그래서 하나의 제네릭 클래스로 다양한 타입의 값을 저장 및 관리할 수 있는 객체를 생성할 수 있는 것이다.
Object 타입의 필드와 제네릭의 가장 큰 차이점은 setter 메서드를 사용할 때 입력 타입을 정확히 확인할 수 있고, 다운캐스팅을 하지 않아도 해당 타입으로 알아서 리턴된다는 것이다. 만약 kv2의 setKey에 문자열이 아닌 숫자를 대입하거나 get할 때 String이 아닌 다른 타입으로 꺼내는 경우 강한 타입 체크로 인해 문법 오류 발생이 일어난다.
코드를 살펴보자 KeyValue<String, Integer> kv1 = new KeyValue<>();에서 KeyValue<String, Integer> kv1 = new KeyValue<String, integer>();으로 작성해도 된다. 그러나 제네릭 타입은 항상 왼쪽 항과 동일하기 때문에 생략할 수 있다. 위의 코드에서는 제네릭 타입 변수를 2개 가진 경우로 작성되었는데, 만약 하나의 타입만 필요한 경우 Void를 적으면 된다.
✔️ 제네릭 메서드_ 597p
• 제네릭 메서드
: 일반 클래스 내부의 특정 메서드만 제네릭으로 선언한 경우
class GenericMethods{
public <K,V> method1(K k, V v) {
System.out.println(k + ":" + v);
}
}
GenericMethods gm = new GenericMethods();
gm.<String, Integer>method1("국어", 80);
gm.method1("국어", 80);
일반 클래스 GenericMethods 내에 제네릭 메서드로 2개의 제네릭 타입 변수를 사용하고 있다. 참조객체 gm을 활용한 코드를 보면 실제 제네릭 타입이 어떤 타입이지 정의해둔 코드와 생략한 코드를 볼 수 있다. 입력매개변수를 통해 제네릭 타입이 어떤 타입인지 알 수 있기 생략해도 무관하다. 다만 입력매개변수 자체가 없거나 입력매개변수에서 제네릭 타입 변수를 사용하지 않는 public static <T> void doSomething() 이런 예시 코드의 경우 입력값으로 예측할 수 없기 때문에 생략이 불가하다.
• 제네릭 Object 타입
public <T> void method(T t) {
System.out.println(t.equals("안녕"));
}
위의 코드는 타입에 제한이 없는 코드다. 이런 경우 컴파일 시 T를 Object로 간주하여 Object에 정의된 메서드 hashcode, eqauls, toString 등만 사용가능하다. 이렇게 되면 제네릭 메서드의 활용 범위가 매우 좁아지게된다.
아래에 정리되겠지만, 그래서 이런 경우 더 넓게 활용하기 위해 제네릭 타입의 범위를 제한한다. 현재 코드에서 t.equals가 아닌 t.length인 String 메서드를 사용하고 싶은 경우 <T>를 <T extends String>으로 변경하면 String 클래스에 있는 메서드도 사용가능해진다.
✔️ 제네릭 타입 범위 제한_ 602p
• 제네릭 클래스 타입 범위 제한
class A {}
class B extends A {}
class C extends B {}
class D <T extends B> {
private T t;
public T get() {
return t;
}
public void set(T t) {
this.t = t;
}
}
D<B> d2 = new D<>();
D<C> d3 = new D<>();
D d4 = new D();
d2.set(new B());
d2.set(new C());
d3.set(new C());
d4.set(new B());
d4.set(new C());
제네릭 클래스의 타입 제한하는 방법은 제네릭 타입으로 대입될 수 있는 최상위 클래스를 extends 키워드와 함께 정의하는 것이다. 그래서 <제네릭 타입 변수 extends 상위 클래스>의 형태로 사용하며 이때 extends는 상속하라는 의미가 아닌 최상위 클래스 혹은 인터페이스로 지정한다라는 의미로 가진다. 코드에서는 <T extends B> 로 작성했으며 A <- B <- C의 상속 구조를 갖고 있다.
제네릭 클래스 D를 살펴보자. 클래스 D는 클래스 B 또는 클래스 B의 자식 클래스만 오도록 제한했다. 그렇다는 것은 A는 제한에 걸려 D<A> d1 = new D<>(); 객체 생성이 불가하다는 의미이다. d2는 제네릭 타입으로 B를 지정하여 B와 C 객체가 입력가능하다. 그런데 d3는 C 타입으로 지정했기 때문에 C의 상위클래스 B객체를 입력할 수 없다. d4는 타입을 지정하지 않았지만 모든 타입의 최상위 클래스가 <B> 타입이므로 제네릭 타입으로 대입되었다. 그래서 d4는 B와 C 객체가 입력가능하다.
• 제네릭 메서드 타입 범위 제한
class A {}
class B extends A {}
class C extends B {}
class D extends C {}
class Goods <T> {
private T t;
public T get() {
return t;
}
public void set(T t) {
this.t = t;
}
}
void methods(Goods<A> g) {}
void methods(Goods<?> g) {}
void methods(Goods<? extends B> g) {}
void methods(Goods<? super B> g) {}
extends는 아까와 같이 해당 클래스와 자식 클래스만 가능하다. 그럼 코드에서 B라고 적혀있으니 B 클래스와 B의 자식 클래스인 객체가 들어갈 것이다. super는 해당 클래스와 부모 클래스인 객체만 가능하므로 B 또는 B의 부모 클래스만 가능할 것이다.
상속구조를 살펴보면 A <- B <- C <- D가 된다. Goods<A>는 A인 객체만 가능하고 Goods<?> 모든 타입인 객체가 가능하기에 A, B, C, D 타입이 모두 ? 자리에 올 수 있다. Goods<? extends B>는 B 또는 B의 자식 클래스인 객체만 가능하기에 B, C, D 타입이 가능하며 Goods<? super B>는 B 또는 B의 부모 클래스인 객체만 가능해서 A, B가 제네릭 타입으로 지정될 수 있다.
✔️ 제네릭의 상속_ 612p
• 제네릭 클래스의 상속
class Parent {
<T extends Number void print(T t) {
System.out.println(t);
}
}
class Child extends Parents {
}
Parent p = new Parent();
p.<Integer>print(10);
p.print(10);
Child c = new Child();
c.<Double>print(5.8);
c.print(5.8);
부모클래스가 제네릭 클래스인 경우 자식 클래스도 제네릭 클래스가 되어 제네릭 타입 변수를 자식 클래스가 그대로 물려받게 된다. 또한 자식 클래스에서 제네릭 타입 변수를 추가해 정의할 수도 있다. 그러므로 자식 클래스의 제네릭 타입 변수의 개수는 언제나 부모와 같거나 그 이상일 것이다.
코드를 보면 부모 클래스에 최상위 제네릭 타입이 Number로 제한되어 있다. Number 클래스는 boolean과 char을 제외한 Byte, Short, Integer, Long, Float, Double이 있다. 자식 클래스는 Number 클래스 내에 있는 6가지 자료형을 사용할 수 있으며 부모 클래스가 가지고 있는 print( )메서드도 이용할 수 있다.
'🍀 Java > 자바완전정복' 카테고리의 다른 글
[Java] 쓰레드 동기화, 쓰레드 상태 (0) | 2025.05.26 |
---|---|
[Java] 쓰레드, 쓰레드의 속성, 데몬쓰레드 (0) | 2025.05.20 |
[Java] 예외처리, 예외전가, 사용자 정의 예외 클래스 (0) | 2025.05.14 |
[Java] 일반 예외와 실행 예외 (0) | 2025.05.08 |
[Java] 익명 이너 클래스 (1) | 2025.05.02 |