일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 안드로이드
- 스터디
- 정처기
- 코틀린
- groupby
- 인프런
- java
- 오블완
- Kotlin
- 티스토리챌린지
- 정보처리기사
- 카카오코테
- SQL
- 알고리즘
- doitandroid
- CS
- 안드로이드스튜디오
- 혼공파
- join
- 자바
- select
- 기술면접
- 혼공단
- 프로그래머스
- 혼공챌린지
- Android
- 코테
- MySQL
- Til
- 자료구조
- Today
- Total
Welcome! Everything is fine.
[Java/Study] 김영한의 실전 자바 중급 2편 - 스터디 11회차 본문
인프런 강의 <김영한의 실전 자바 - 중급 2편>을 보고 정리한 내용입니다.
매주 모여 각자 정리한 내용을 기반으로 발표하고 질문 공유하는 스터디입니다.
📘제네릭이 필요한 이유
다음과 같이 Integer 타입을 담고, 꺼낼 수 있는 클래스가 있다고 하자. Integer 타입 말고도 Double, Boolean, String 등의 다양한 타입을 담아야 한다면 어떨까? 각 타입에 맞는 새로운 클래스를 계속 만들어야 할 것이다.
public class IntegerBox {
private Integer value;
public void set(Integer value) {
this.value = value;
}
public Integer get() {
return value;
}
}
만약 다음과 같이 Object 타입을 받는 ObjectBox를 만들면 여러 타입을 받는 것에 대한 문제는 해결할 수 있다. 그러나 몇 가지 문제가 발생할 수 있다.
1) 서로 다른 값을 넣어도 반환 타입은 항상 Object 타입이기 때문에 직접 다운 캐스팅 해야한다.
2) 잘못된 타입의 인수를 전달할 경우 꺼낼 때 잘못 캐스팅하여 예외가 발생할 수 있다.
public class ObjectBox {
private Object value;
public void set(Object object) {
this.value = value;
}
public Object get() {
return value;
}
}
잘못된 타입의 값을 전달하면 값을 꺼낼 때 문제가 발생한다. 아래와 같이 문자를 넣었는데 Integer로 캐스팅한다면 캐스팅을 할 수 없다는 예외가 발생하고 프로그램이 종료될 것이다.
// 잘못된 타입의 인수 전달 시
ObjectBox integerBox = new ObjectBox();
integerBox.set("문자100");
Integer result = (Integer) integerBox.get();
System.out.println("result = " + result);
이러한 문제들은 제네릭을 이용해 해결할 수 있다!
📘제네릭(Generic)
제네릭(Generic)은 '일반적인', '범용적인'이라는 의미를 가지고 있다. 특정 타입에 속하지 않고 일반적으로 사용할 수 있다는 뜻이다.제네릭을 이용하면 코드 재사용성과 타입 안정성을 모두 충족시킬 수 있다. 다음과 같이 <>(다이아몬드)를 사용한 클래스를 제네릭 클래스라고 한다.
- 클래스명 오른쪽에 <T>와 같이 선언한다.
- T는 타입 매개변수로, 다양한 타입으로 변할 수 있다.
- 클래스 내부에 T 타입이 필요한 곳에 적어둔다.
public class GenericBox<T> {
// T : 타입 매개변수
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
이렇게 만든 제네릭 클래스는 다음과 같이 원하는 타입을 넣어 객체를 생성해 사용한다.
GenericBox<Integer> integerBox = new GenericBox<Integer>();
GenericBox<String> stringBox = new GenericBox<String>();
GenericBox<Double> doubleBox = new GenericBox<Double>();
참고로 생성하는 제네릭 타입은 생략이 가능하다.
// 타입 추론: 생성하는 제네릭 타입 생략 가능
GenericBox<Integer> integerBox = new GenericBox<>();
따라서 생성 시점에 T의 타입을 결정하면 set()을 할 때나 get()을 할 때도 해당 타입만 허용된다. 따라서 다른 타입을 넣게 되더라도 Object 타입처럼 모두 허용되는 것이 아니라 생성 시점에 결정한 타입만 허용되는 것이어서 컴파일 오류가 발생한다.
public static void main(String[] args) {
GenericBox<Integer> integerBox = new GenericBox<>();
integerBox.set(10);
//integerBox.set("문자열"); // Integer 타입만 허용, 컴피일 오류
Integer integer = integerBox.get(); // Integer 타입 반환(캐스팅X)
System.out.println("integer = " + integer);
}
integer = 10
💡 제네릭 관련 용어 정리
- 제네릭(Generic) : 일반적인, 범용적인이라는 뜻.
- 제네릭 타입(Generic Type) : 클래스나 인터페이스 정의 시 타입 매개변수를 사용하는 것, 제네릭 클래스와 제네릭 인터페이스를 모두 통틀어 제네릭 타입이라고 함.
- 타입 매개변수(Type Parameter) : 제네릭 타입이나 메서드에서 사용되는 변수. GenericBox<T> 에서 T를 의미함.
- 타입 인자(Type Argument) : 제네릭 타입을 사용할 때 제공되는 실제 타입. GenericBox<Integer>에서 Integer를 의미함.
💡 제네릭 명명 관례
일반적으로 대문자를 사용하며, 용도에 맞는 단어의 첫글자를 사용함.
- E - Element
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
💡 기타
- 한 번에 여러 타입매개변수를 선언할 수 있다.
class Data<K, V> {}
- 타입 인자로 기본형은 사용할 수 없다.
💡 raw type
자바는 제네릭이 없던 시절 과거 코드와의 하위 호환을 위해 로 타입(raw type)을 지원한다. 결론부터 말하자면, 로 타입은 사용하지 않아야 한다. 로 타입은 <>을 지정하지 않는 것을 의미한다. <>을 지정하지 않으면 내부의 타입 매개변수가 Object로 사용된다.
GenericBox integerBox = new GenericBox();
제네릭 타입을 사용할 때는 반드시 <>를 사용해서 사용시점에 타입을 지정하도록 하자!
📘제네릭 활용
Animal 클래스와 Animal 클래스를 상속받는 Dog, cat 클래스가 있다고 하자. 그리고 해당 클래스 타입을 모두 담을 수 있는 제네릭 클래스를 만든다면 다음과 같이 만들 수 있다.
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
그렇게 하면 이렇게 각자 다른 클래스 타입을 타입 인자로 넣고 사용할 수 있다.
public class AnimalMain1 {
public static void main(String[] args) {
Animal animal = new Animal("동물", 0);
Dog dog = new Dog("멍멍이", 100);
Cat cat = new Cat("냐옹이", 50);
Box<Dog> dogBox = new Box<>();
dogBox.set(dog);
Dog findDog = dogBox.get();
System.out.println("findDog = " + findDog);
Box<Cat> catBox = new Box<>();
catBox.set(cat);
Cat findCat = catBox.get();
System.out.println("findCat = " + findCat);
Box<Animal> animalBox = new Box<>();
animalBox.set(animal);
Animal findAnimal = animalBox.get();
System.out.println("findAnimal = " + findAnimal);
}
}
📘타입 매개변수 제한
extends 키워드를 이용해 타입 매개변수를 특정 타입으로 제한할 수 있다.
다음과 같이 <T extends Animal>이라고 해두면, Animal과 그 자식들만 받을 수 있도록 제한을 둘 수 있다. 즉 T의 상한이 Animal이 된다. 기존에 그냥 <T>만 해두었을 때는 Animal 타입뿐만 아니라 전혀 상관없는 Integer나 String 타입도 들어올 수 있었다. 하지만 이렇게 타입 매개변수를 제한하면 이런 문제를 해결할 수 있다.
public class AnimalHospitalV3<T extends Animal> {
// Animal이나 Animal의 자식들만 올 수 있음.(Object도 못옴)
private T animal;
public void set(T animal) {
this.animal = animal;
}
public void checkup() {
System.out.println("동물 이름: " + animal.getName());
System.out.println("동물 크기: " + animal.getSize());
animal.sound();
}
public T bigger(T target) {
return animal.getSize() > target.getSize() ? animal : target;
}
}
📘와일드카드
와일드카드란 제네릭을 쉽게 쓸 수 있도록 도와주는 도구로, 타입 인자가 정해진 제네릭 타입을 전달 받아서 활용할 때 사용한다. 다음과 같이 <> 안에 ? 를 넣어 사용한다.
static void printWildcardV1(Box<?> box) {
System.out.println("? = " + box.get());
}
- ?의 뜻은 모든 타입을 다 받을 수 있다는 뜻이다.
- 매개변수로 제네릭 타입을 받을 수 있는 일반적인 메서드이다.
- 더 단순하기 때문에 사용이 권장된다.
와일드카드에도 다음과 같이 extends 키워드를 이용해 상한 제한을 둘 수 있다. Box<? extends Animal>과 같이 쓴다면, Animal과 그 하위 타입만 입력받을 수 있다.
static void printWildcardV2(Box<? extends Animal> box) {
Animal animal = box.get();
System.out.println("이름 = " + animal.getName());
}
또한 와일드카드는 super 키워드를 이용해 하한도 지정할 수 있다. Box<? super Animal> 과 같이 쓴다면, Animal과 그 상위 타입만 입력받을 수 있다. 즉, 아래 예시에서는 Animal과 Object 타입만 허용된다.
static void writeBox(Box<? super Animal> box) {
box.set(new Dog("망망이", 100));
}
📘타입 이레이저
타입 이레이저란 자바 컴파일 단계에서만 제네릭이 사용되고, 컴파일 이후에는 제네릭 정보가 삭제되는 것을 말한다.
예를 들어 다음과 같이 제네릭 타입을 선언한 후에 Integer 타입 인자를 전달했다면,
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
자바 컴파일러는 컴파일 시점에 다음과 같이 이해한다.
public class Box<Integer> {
private Integer value;
public void set(Integer value) {
this.value = value;
}
public Integer get() {
return value;
}
}
그리고나서 컴파일이 끝나면 제네릭 관련 정보를 모두 삭제해서 .class 파일에는 다음과 같은 정보가 생성된다.
public class Box {
private Object value;
public void set(Object value) {
this.value = value;
}
public Object get() {
return value;
}
}
값을 꺼낼 때도 자바 컴파일러가 Integer로 캐스팅하는 코드를 추가해주기 때문에 문제가 발생하지 않는다.
void main() {
GenericBox box = new GenericBox();
box.set(10);
Integer result = (Integer) box.get(); // 컴파일러가 캐스팅 추가
}
하지만, 이런 타입 이레이저 방식으로 인해 다음과 같은 코드를 작성할 수 없다. 여기서 T는 모두 런타임에 Object로 바뀌기 때문에, instanceof는 항상 Object와 비교하게 되어서 소용이 없어진다. new T() 또한 항상 new Object가 된다.
class EraserBox<T> {
public boolean instanceCheck(Object param) {
return param instanceof T; // 오류
}
public void create() {
return new T(); // 오류
}
}
📘 출처 - 김영한의 실전 자바 - 중급 2편
'Java' 카테고리의 다른 글
[Java/Study] 김영한의 실전 자바 중급 2편 - 스터디 13회차 (0) | 2025.01.18 |
---|---|
[Java/Study] 김영한의 실전 자바 중급 2편 - 스터디 12회차 (0) | 2025.01.12 |
[Java] 얕은 복사 vs 깊은 복사 - clone(), Arrays.copyof() (3) | 2025.01.02 |
[Java/Study] 김영한의 실전 자바 중급 1편 - 스터디 10회차 (0) | 2024.12.29 |
[Java/Study] 김영한의 실전 자바 중급 1편 - 스터디 9회차 (0) | 2024.12.18 |