자바의 정석 책을 공부하고 정리한 글입니다!
혹시라도 틀린 부분이 있다면 친절하게 알려주세요.
감사합니다!
CH12. 지네릭스, 열거형, 애너테이션
제네릭(Generics)
제네릭이란 클래스가 가질 타입을 미리 명시해주는 기능을 말합니다. 예를 들어, ArrayList를 생성할 때는 다음과 같이 생성할 수 있습니다.
ArrayList<String> list = new ArrayList<>();
ArrayList를 보면 <> 를 사용하여, ArrayList에 들어갈 요소의 타입을 명시해주었습니다.
이때 <>를 제네릭이라 하며, <타입>을 넣음으로써 타입을 미리 명시해주는 것입니다.
이렇게 객체의 타입을 명시해주었으므로, 명시한 타입과 다른 값을 넣을라고 하면 컴파일 에러가 발생하게 됩니다.
이런 제네릭을 사용했을 때의 장점은 다음과 같습니다:
- 타입 안정성을 제공한다.
- 타입체크와 형변환을 생략할 수 있어 코드가 간결해진다.
제네릭 타입을 클래스에 적용할 때는 다음과 같이 사용할 수 있습니다.
class Box<T> { // 제네릭 타입 T 선언
T item;
void setItem(T item) { this.item = item; }
T getItem() { return item; }
}
위와 같이 클래스 옆에 <T>를 붙이면 됩니다.
이때, T는 타입 변수라고 부르며, 이 타입 변수는 T가 아닌 다른 것을 사용해도 됩니다.
타입 변수 기호 예시
타입변수 | 설명 |
<T> | 타입 (Type) |
<E> | 요소 (Element) |
<K> | 키(Key) |
<V> | 값(Value) |
기호의 종류만 다를 뿐 모두 임의의 참조형 타입을 의미합니다.
따라서 무조건 <T>를 사용하기 보다는 상황에 맞게 의미있는 문자를 선택하는 것이 좋습니다.
또한, Box클래스의 객체를 생성할 때는 <T> 대신에 실제로 사용할 타입명을 작성하면 됩니다.
Box<String> b = new Box<String>();
Box<String> b = new Box<>(); // jdk 1.7버전 이후부터는 new생성자 부분의 제네릭 타입 생략 가능
이렇게 되면 T 대신, 실제 타입을 지정해주어, 다음과 같이 정의된 것과 똑같이 됩니다.
lass Box { // 제네릭 타입 T 선언
String item;
void setItem(String item) { this.item = item; }
String getItem() { return item; }
}
제네릭스의 제한
- static 멤버에는 타입 변수를 사용할 수 없다: static은 모든 객체에 대해 동일하게 동작하기 때문이다.
- 배열 생성할 때 타입 변수를 사용할 수 없다. 제네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만 new T[]의 형태가 안된다는 뜻이다.
class Box<T> {
T[] itemArr; // T타입의 배열을 위한 참조변수 (가능하다!)
T[] toArray() {
T[] tmpArr = new T[itemArr.length]; // 불가능! 배열 생성은 안됨
return tmpArr;
}
}
new 연산자 같은 경우엔 컴파일 타임에 타입이 확정된 경우메만 객체 혹은 배열을 생성할 수 있습니다. 그래야만 메모리 크기, 구조, 생성자 등을 정확히 결정할 수 있기 때문입니다.
하지만 Box<T> 클래스가 정의될 때, 컴파일러는 T가 어떤 구체적인 타입이 될지 알 수 없습니다. 따라서 new T()나 new T[] 와 같이 정확한 타입을 알아야 하는 작업은 불가능합니다.
타입의 종류 제한하기
타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 때는 다음과 같이 extends 키워드를 사용하면 됩니다.
class FruitBox<T extends Fruit> {
ArrayList<T> list = new ArrayList<>();
void add(T item) {
list.add(item)
}
}
이는 FruitBox<T> 에서 T는 반드시 Fruit나 Fruit를 상속한 클래스만 올 수 있다는 뜻입니다.
만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요할 때에도 extends 키워드를 사용합니다. (implements 키워드 사용하지 않는다는 점을 주의해주세요!)
와일드 카드
Juicer라는 클래스가 다음과 같이 정의되어 있다고 가정하겠습니다.
class Juicer {
static Juice makeJuice(FruitBox<Fruit> box) {
String tmp = "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Jucie(tmp);
}
}
Juicer클래스는 제네릭 클래스가 아니지만, 만약 제네릭 클래스라 해도 static메서드에는 타입 변수를 매개변수로 사용할 수 없으므로 위와 같이 특정 타입을 지정해줘야 합니다.
이렇게 FruitBox<Fruit> 로 타입을 고정해 놓으면, FruitBox<Apple>타입의 객체는 makeJuice()의 매개변수가 될 수 없습니다.
FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>();
Juicer.makeJuice(fruitBox); // 가능
Juicer.makeJuice(appleBox); // 오류
따라서 FruitBox<Apple>타입의 객체가 makeJuice()의 객체가 되려면 다음과 같이 메서드 오버로딩을 하는 수 밖에 없습니다.
static Juice makeJuice(FruitBox<Fruit> box) {...}
static Juice makeJuice(FruitBox<Apple> box) {...} // 컴파일 오류
하지만 제네릭 타입이 다른 메서드로는 오버로딩이 성립하지 않습니다. 왜냐하면 타입 소거 특성 때문에 두 메서드는 모두 FruitBox로 컴파일되어 시그니처가 중복됩니다. 즉, 다음과 같이 처리하는 것이죠.
static Juice makeJuice(FruitBox box) { ... }
static Juice makeJuice(FruitBox box) { ... } // 중복 정의로 간주
타입 소거란?
Java의 제네릭은 컴파일 타임에만 타입 정보를 유지하고, 런타임에는 모든 타입의 정보가 소거되는데 이것을 타입 소거라고 합니다.
따라서 위의 코드는 메서드 오버로딩이 아닌 메서드 중복 정의가 되어 컴파일 오류가 발생합니다.
이럴 때 사용하기 위해 고안된 것이 바로 '와일드 카드'입니다. 와일드 카드는 ? 로 표현하는데, 제네릭에서 타입을 정확히 모를 때나, 다양한 타입을 허용할 때 사용하는 타입 변수입니다.
와일드 카드는 어떤 타입이든 올 수 있으므로 Object타입과 다를게 없습니다. 따라서 extends와 super로 상한과 하한을 제한할 수 있습니다.
- <? extends T> : 와일드 카드의 상한 제한. T와 T의 자식들만 가능
- <? super T> : 와일드 카드의 하한 제한. T와 T의 부모들만 가능
- <?> : 제한 없으므로 모든 타입 가능. <? extends Object>와 동일
와일드 카드를 makeJuice()에 적용하면 다음과 같이 사용할 수 있습니다.
static Juice makeJuice(FruitBox<? extends Fruit> box) {...}
이렇게 작성하면 매개변수로 FruitBox<Fruit> 뿐만 아니라, Fruit를 상속한 타입(FruitBox<Apple>, FruitBox<Grape> 등)을 사용할 수 있게 됩니다.
제네릭 메서드
메서드의 선언부에 제네릭 타입이 선언된 메서드를 제네릭 메서드라 하며, 다음과 같이 선언할 수 있습니다.
static<T> void sort(List<T> list, Comparator<? super T> c)
제네릭 클래스에 정의된 타입 매개변수와 제네릭 메서드에 정의된 타입 매개변수는 전혀 다른 것입니다!
같은 타입 문자 T를 사용해도 같은 것이 아니니 주의하셔야 합니다.
열거형(enums)
열거형이란 서로 관련된 상수를 편리하게 선언하기 위한 것으로 JDK1.5부터 새로 추가되었습니다. 이러한 열거형은 여러 상수를 정의할 때 사용하면 유용합니다.
열거형을 정의하는 방법은 다음과 같습니다.
enum 열거형 이름 { 상수명1, 상수명2, ... }
다음과 같이 정의된 Card클래스가 있다고 가정해 보겠습니다.
class Card {
static final int CLOBER = 0;
static final int HEART = 1;
static final int DIAMOND = 2;
static final int SPADE = 3;
static final int TWO = 0;
static final int THREE = 1;
static final int FOUR = 2;
final int kind;
final int num;
}
이 코드는 열거형을 사용하여 다음과 같이 바꿀 수 있습니다.
class Card {
enum Kind { CLOVER, HEART, DIAMOND, SPADE }
enum Value { TWO, THREE, FOUR }
final Kind kind; // 타입은 int가 아닌 정의된 열거형 이름으로 사용
final Value value;
}
또한, 열거형이름.상수명 으로 열거형에 정의된 상수를 사용할 수 있습니다. 클래스의 static 변수를 참조하는 방식과 동일합니다.
예를 들어, 동서남북을 상수로 정의하는 열거형 Direction이 있다고 가정해보겠습니다.
enum Direction { EAST, SOUTH, WEST, NORTH }
그럼 Direction dir = Direction.EAST; 와 같이 사용할 수 있습니다.
열거형 상수끼리 비교할 때는 '=='를 사용할 수 있어 빠른 성능을 제공합니다. 그러나 <,> 연산자는 사용할 수 없으며 대신 compareTo()는 사용이 가능합니다.
다음과 같이 switch문의 조건식에도 열거형을 사용할 수 있습니다.
void move() {
switch(dir) {
case EAST:
x++;
break;
case WEST:
x--;
break;
case SOUTH:
y++;
break;
case NORTH:
y--;
break;
}
}
switch문에서 주의할 점은 case문에 열거형의 이름은 적지 않고, 상수의 이름만 적어야 한다는 것입니다!
Enum 클래스는 모든 열거형의 조상으로 다음과 같은 메서드가 정의되어 있습니다.
열거형에 멤버 추가하기
자바에서 열거형은 단순한 상수 집합이 아니라, 클래스처럼 멤버(필드와 메서드)를 가질 수 있습니다.
열거형에 멤버를 추가하는 방법은 다음과 같습니다:
- 열거형 상수 옆에 (원하는 값) 넣기
- 필드 추가
- 생성자 추가
- 필외부에서 접근할 수 있도록 getter 추가
예를 들어 위에서 정의한 열거형 Direction에서 각 방향마다 한글 이름을 저장하고 싶다고 가정했을 때, 다음과 같이 멤버를 추가할 수 있습니다.
enum Direction {
EAST("동쪽"), WEST("서쪽"), SOUTH("남쪽"), NORTH("북쪽"); // 뒤에 ; 을 붙여야 함
private final String koreanName; // 1. 필드 추가
// 2. 생성자 정의: 열거형은 생성자는 private
Direction(String koreanName) { this.koreanName = koreanName; }
// 3. 필드에 접근할 수 있는 getter 메서드
public String getKoreanName() { return koreanName; }
}
이때 주의할 점은 열거형에서 생성자를 정의하면 모든 enum 상수는 반드시 생성자에 맞게 인자를 넘겨야 하며, 그렇지 않으면 컴파일 오류가 발생합니다.
enum Direction {
EAST("동쪽"), WEST("서쪽"), SOUTH, NORTH; // 컴파일 오류
private final String koreanName;
Direction(String koreanName) { this.koreanName = koreanName; }
public String getKoreanName() { return koreanName; }
}
애너테이션(annotation)
애너테이션이란 프로그램의 소스코드 안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것으로 주석처럼 프로그래밍 언어에 영향을 미치지 않습니다.
애너테이션을 사용할 때는 '@'를 붙여서 사용합니다.
표준 애너테이션
표준 애너테이션은 자바에서 기본으로 제공하는 애너테이션으로, 코드의 의미나 동작에 영향을 주는 데 사용됩니다.
메타 애너테이션
애너테이션을 정의할 때 사용되는 애너테이션입니다. 즉, 애너테이션용 애너테이션이라고 생각하면 됩니다.
애너테이션 타입 정의하기
새로운 애너테이션을 정의하는 방법은 다음과 같습니다.
@interface 애너테이션 이름 {
타입 요소이름();
}
'@' 기호를 앞에 붙이는 것을 제외하면 인터페이스 정의하는 방법과 동일합니다.
요소란 애너테이션 내에 선언된 메서드를 말합니다. 요소는 반환값이 있고 매개변수는 없는 추상 메서드의 형태를 가지며, 상속을 통해 구현하지 않아도 됩니다. (애너테이션에서는 상속이라는 개념 자체가 적용되지 않습니다!)
'도서 정리 > 자바의 정석' 카테고리의 다른 글
[자바의 정석] CH13 쓰레드 (2) (0) | 2025.05.08 |
---|---|
[자바의 정석] CH13 쓰레드 (1) (1) | 2025.05.05 |
[자바의 정석] CH11 컬렉션 프레임워크(2) (1) | 2025.04.25 |
[자바의 정석] CH11 컬렉션 프레임워크(1) (2) | 2025.04.23 |
[자바의 정석] CH10 날씨와 시간 & 형식화 (1) | 2025.04.21 |