- 지네릭스 [Generics]
- 열거형[ enum ]
- 애너테이션 [ annotation ]
지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크(compile-time type check)를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.
타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다.
--> 다룰 객체의 타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다.
지네릭 타입은 클래스와 메서드에 선언할 수 있는데, 먼저 클래스에 선언하는 지네릭타입에 대해 알아본다.
class Box{
Object item;
void setItem(Object item) { this.item = item; }
Object getItem(){ return item; }
}
위의 클래스를 지네릭 클래스로 변경하면 클래스 옆에 <T>
를 붙이면 된다. 그리고 Object
를 모두 T
로 바꾼다.
Class Box<T>{
T item;
void setItem(T item){ this.item = item; }
T getItem(){ return item;}
}
이 <T>
에서 T를 타입 변수(type variable) 라고 하며, Type의 첫 글자에서 따온 것이다. (T가 아닌 다른 것을 사용해도 된다. )
타입변수가 여러개인 경우 <T,E>
와 같이 콤마 ,
를 구분자로 나열하면 된다. 상황에 맞게 의미있는 문자를 선택해서 사용하는 것이 좋다. 이들의 기호의 종류만 다를뿐 임의의 참조형 타입
을 의미한다는 것은 모두 같다.
지네릭 클래스의 객체를 생성할 때에는 참조변수와 생성자에 타입 T대신 사용될 실제 타입을 지정해주어야 한다.
Box<String> b = new Box<String>();
b.setItem(new Object()); //에러. String 타입 외 지정 불가
b.setItem("ABC"); // OK. String 타입이므로 가능
String item = b.getItem(); // 형변환 필요 없음
지네릭 도입 이전의 코드와 호환을 위해, 지네릭 클래스임에도 예전의 방식으로 객체를 생성하는 것이 허용되지만 안전하지 않다는 경고가 발생
class Box<T>{}
에서 Box<T>
는 지네릭클래스. T의 Box
또는 T Box
라고 읽는다. T
는 타입 변수 또는 타입 매개변수. Box
는 원시타입 이라 한다.
타입 매개변수에 타입을 지정하는 것을 지네릭 타입 호출 이라고 하고, 지정된 타입을 매개변수화된 타입 이라고 한다. (대입된 타입)
지네릭 클래스의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절하다. 그러나 모든 객체에 대해 동일하게 동작해야 하는 static멤버에 타입 변수 T를 사용할 수 없다. T는 인스턴스 변수로 간주되기 때문이다. (static멤버는 인스턴스 변수를 참조할 수 없다. )
static멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 하기 때문이다. 그리고, 지네릭 타입의 배열을 생성하는 것도 허용되지 않는다. 지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, new T[10]
과 같이 배열을 생성하는 것은 안된다는 뜻이다.
class Box<T>{
T[] itemArr; // OK . T타입의 배열을 위한 참조변수
T[] toArray(){
T[] tmpArr = new T[itemArr.length]; //에러. 지네릭 배열 생성 불가
...
return tmpArr;
}
}
지네릭 배열을 생성할 수 없는 것은 new연산자 때문인데, 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다. 그런데 지레릭 클래스를 컴파일하는 시점에서는 T가 어떤 타입인지 전혀 알수 없다. (instanceOf도 T를 피연산자로 사용할 수 없다. )
꼭 지네릭 배열을 생성해야할 필요가 있을 때는 ,new연산자 대신 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object배열을 생성하여 복사한 다음에 T[]
로 형변환하는 방법등을 사용해야 한다.
지네릭 클래스의 객체를 생성할 떄에 참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야 한다. 일치하지 않으면 에러가 발생한다.
Box<Apple> appleBox = new Box<Grape>(); //에러
두 타입이 상속관계에 있어도 마찬가지이다. Apple이 Fruit의 자손이라고 가정하자.
Box<Fruit> appleBox = new Box<Apple>(); //에러. 대입된 타입이 다르다.
단,두 지네릭 클래스의 타입이 상속관계에 있고, 대입된 타입이 같은 것은 괜찮다. FruitBox가 Box의 자손이라 가정하자.
Box<Apple> appleBox = new FruitBox<Apple>(); //OK
JDK1.7부터는 추정이 가능한 경우 타입의 생략이 가능하다.
Box<Apple> appleBox = new Box<>(); OK. Apple 생략
해당 클래스의 객체에 add<T item>
으로 객체를 추가할 떄, 대입된 타입과 다른 타입의 객체는 추가할 수 없으나, 상속관계인경우 자손들은 매개변수가 될수 있다.
Box<Fruit> fruitBox = new Box<>();
fruitBox.add(new Fruit()); //Ok.
fruitBox.add(new Apple()); //Ok. Apple은 Fruit의 자손
타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만, 그래도 여전히 모든 종류의 타입을 지정할 수 있다는 것에는 변함이 없다. 그렇다면, 타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법은 없을까 ?
지네릭 타입에 extends
를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.
class FruitBox<T extends Fruit>{ // Fruit의 자손만 타입으로 지정가능
ArrayList<T> list = new ArrayList<T>();
...
}
여전히 한 종류의 타입만 담을 수 있지만, Fruit클래스의 자손들만 담을 수 있다는 제한이 더 추가된 것이다. (매개변수의 타입 T도 조상의 자손 타입이 될 수 있다. )
만약 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면, 이때도 extends
를 사용한다. ( implements
를 사용하지 않는다. )
interface Eatable{}
class FruitBox<T extends Eatable>{ ..}
클래스 Fruit의 자손이면서 Eatable인터페이스도 구현해야 한다면 아래와 같이 &
기호로 연결한다.
class FruitBox<T extends Fruit & Eatable>{...}
이제 FruitBox에는 Fruit의 자손이면서 Eatable을 구현한 클래스만 타입 매개변수로 대입될수 있다.
매개변수에 과일박스를 대입하면 주스를 만들어서 반환하는 Juicer라는 클래스가 있고, 이 클래스에는 과일을 주스로 만들어서 반환하는 makeJuice()라는 static메서드가 다음과 같이 정의되어 있다고 가정하자.
class Juicer{
static Juice makeJuice(FruitBox<Fruit> box){
String tmp = '';
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
}
Juicer클래스는 지네릭 클래스가 아니고 static메서드를 사용하므로 아예 지네릭스를 적용하지 않던가, 위와 같이 타입 매개변수 대신, 특정 타입을 지정해주어야 한다.
이렇게 지레릭 타입을 고정해 놓으면, 다른 매개변수화 타입을 가진 (예 - FruitBox<Apple>
) 객체는 makeJuice()의 매개변수가 될 수 없으므로, 여러 가지 타입의 매개변수를 갖는 메서드를 만들 수 밖에 없게 된다.
그러나 , 위와 같은 방법으로 오버로딩을 하면 컴파일 에러가 발생한다. 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기 때문이다. 지네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거해버리므로 오버로딩이 아니라 메서드 중복 정의
이다. 이럴 때 사용하기 위해 고안된 것이 바로 와일드 카드 이다. 와일드 카드는 기호로 ?
로 표현하는데, 와일드 카드는 어떠한 타입도 될 수 있다.
?
만으로는 Object타입과 다를 게 없으므로, 다음과 같이 extends
와 super
로 상한과 하한을 제한할 수 있다.
< ? extends T > : 와일드 카드의 상한 제한. T와 그 자손들만 가능
< ? super T > : 와일드 카드의 하한 제한. T와 그 조상들만 가능
< ? > : 제한 없음. 모든 타입이 가능. < ? extends Object > 동일
static Juice makeJuice(FruitBox<? extends Fruit> box){
String tmp = "";
for(Fruit f: box.getList()) tmp += f + " ";
return new Juice(tmp);
}
메서드의 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라 한다. 지네릭 타입의 선언 위치는 반환타입 바로 앞이다.
static <T> void sort(List<T> list,Comparator<? super T> c)
지네릭 클래스에 정의된 타입 매개변수와 지네릭 메서드에 정의된 타입 매개변수는 전혀 별개의 것이다. 같은 타입 문자 T를 사용해도 같은 것이 아니라는 것에 주의해야 한다.
static멤버에는 타입 매개변수를 사용할 수 없지만 이처럼 메서드에 지네릭타입을 선언하고 사용하는 것은 가능하다. 메서드에 선언된 지네릭 타입은 지역 변수를 선언한 것과 같다고 생각하면 이해하기 쉬운데, 이 타입 매개변수는 메서드 내에서만 지역적으로 사용될 것이므로 메서드가 static이건 아니건 상관이 없다.
static <T extends Fruit> Juice makeJuice(FruitBox<T> box){
String tmp = "";
for(Fruit f: box.getList()) tmp += f + " ";
return new Juice(tmp);
}
이 메서드를 호출 할 때에는 아래와 같이 타입 변수에 타입을 대입해야 한다.
Juicer.<Fruit>makeJuice(fruitBox);
Juicer.<Apple>makeJuice(appleBox);
그러나 대부분의 경우 컴파일러가 추정할 수 있기 때문에 생략해도 된다.
원시타입과 지네릭 타입과의 형변환은 항상 가능하다. ( 경고 ) 그러면, 대입된 타입이 다른 지네릭 타입 간에는 형변환이 불가능하다.
Optional< Object > 를 Optional< String > 으로 직접 형변환은 불가능하지만, 와일드 카드가 포함된 지네릭 타입으로 형변환하면 가능하다.
컴파일러는 지네릭 타입을 이요해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다. 그리고 지네릭 타입을 제거한다.
열거형은 서로 관련된 상수를 편리하게 선언하기 위한 것으로 여러 상수를 정의할 때 사용하면 유용하다. 자바의 열거형은 C언어의 열거형보다 더 향상된 것으로 열거형이 갖는 값뿐만 아니라 타입도 관리하기 때문에 보다 논리적인 오류를 줄일 수 있다.
class Card{
enum Kind { CLOVER,HEART,DIAMOND,SPADE }
enum Value { TWO,THREE,FOUR }
findal Kind kind;
finald Valud value;
}
자바의 열거형은 타입에 안전한 열거형
이라서 실제 값이 같아도 타입이 다르면 컴파일 에러가 발생한다. 이처럼 값뿐만 아니라 타입까지 체크하기 때문에 타입에 안전하다고 하는 것이다.
열거형의 정의는 괄호{} 안에 상수의 이름을 나열하기만 하면 된다.
enum 열거형이름 { 상수명1,상수명2 }
이 열거형에 정의된 상수를 사용하는 방법은 열겨헝이름.상수명
이다. 클래스의 static변수를 찹조하는 것과 동일하다.
enum Direction { EAST,WEST,SOUTH,NORTH }
class Unit{
int x,y;
Direction dir;
void init(){
dir = Direction.EAST;
}
}
열거형 상수간의 비교에는 ==
를 사용할 수 있다. equals()가 아닌 ==
로 비교가 가능하다는 것은 그만큼 빠른 성능을 제공한다는 얘기다. 그러나 <
,>
와 같은 비교연산자는 사용할 수 없고 compareTo()는 사용가능하다. (compareTo() - 같으면 0 , 왼쪽 크면 양수, 오른쪽 크면 음수 )
switch문의 조건식에도 열거형의 사용이 가능하다.
switch(dir){
case EAST: x++; break;
case WEST: x--; break;
case SOUTH: y++; break;
case NORTH: y--; break;
}
열거형 Direction에 정의된 모든 상수를 출력하려면 다음과 같이 한다.
Direction[] dArr = Direction.values();
for(Direction d : dArr)
System.out.println("%s = %d%n ", d.name(),d.ordinal());
values() 는 열거형의 모든 상수를 배열에 담아 반환한다. ordinal() 은 모든 열거형의 조상인 java.lang.Enum클래스에 정의된 것으로, 열거형 상수가 정의된 순서를 정수로 반환한다.
메서드 | 설명 |
---|---|
Class<E> getDeclaringClass() | 열거형의 Class객체를 반환 |
String name | 열거형 상수의 이름을 문자열로 반환 |
int ordinal() | 열거형 상수가 정의된 순서를 반환 |
T valueOf(Class<T> enumType,String name) | 지정된 열거형에서 name과 일치하는 열거형 상수를 반환 |
이 외에 values() 처럼 컴파일러가 자동적으로 추가해주는 메서드가 하나 더 있다.
static E valuess()
static E valueOf(String name)
이 메서드는 열거형 상수의 이름으로 문자열 상수에 대한 참조를 얻을 수 있게 해준다.
열거형 상수의 값이 불연속적인 경우에는 열거형 상수의 이름 옆에 원하는 값을 괄호()와 함께 적어주면 된다.
enum Direction { EAST(1),SOUTH(5),WEST(-1),NORTH(10) }
지정된 값을 저장할 수 있는 인스턴스 변수와 생성자를 새로 추가해주어야 한다. 주의할 점은, 먼저 열거형 상수를 모두 정의한 다음에 다른 멤버들을 추가해야 한다. (열거형 상수의 마지막에 ;
도 잊지 말아야 한다.)
enum Direction{
EAST(1),SOUTH(5),WEST(-1),NORTH(10);
private final int value; //정수를 지정할 필드
Direction(int value) {this.value = value;} //생성자
public int getValue(){return value; }
}
열거형의 인스턴스 변수는 반드시 final이어야 한다는 제약은 없지만,value는 열거형 상수의 값을 저장하기 위한 것이므로 final을 붙였다.
Direction d = new Direction(1); //에러. 열거형의 생성자는 외부에서 호출 불가 private
필요하다면 하나의 열거형 상수에 여러 값을 지정할 수도 있다. 다만 그에 맞게 인스턴스 변수와 생성자등을 새로 추가해야 한다.
enum Transportation {
bus(100) {
int fare(int distance) { return distance * BASIC_FARE; }
},
TRAIN(150){
int fare(int distance) { return distance * BASIC_FARE; }
},
SHIP(100){
int fare(int distance) { return distance * BASIC_FARE; }
},
AIRPLANE(300){
int fare(int distance) { return distance * BASIC_FARE; }
};
abstract int fare(int distance); //거리에 따른 요금을 계싼하는 추상 메서드
protected final int BASIC_FARE; // protected로 해야 각 상수에서 접근이 가능
Transporation(int basicFare){
BASIC_FARE = basicFare;
}
public int getBasicFare() { return BASIC_FARE; }
}
거리에 따라 요금을 계산하는 방식이 각 운송 수단 마다 다를 것이 기 때문에 열거형에 추상메서드 fare(int distance)
를 선언하면 각 열거형 상수가 이 추상메서드를 반드시 구현해야 한다.
열거형 Direction의 정의가 밑과 같다.
enum Direction{ EAST,SOUTH,WEST,NORTH }
이때 위 열거형의 상수 하나하나가 Direction의 객체인 것이다. 위의 문장을 클래스로 정의하면 아래와 같다.
class Direction {
static final Direction EAST = new Direction("EAST");
static final Direction SOUTH = new Direction("SOUTH");
static final Direction WEST = new Direction("WEST");
static final Direction NORTH = new Direction("NORTH");
private String name;
private Direction(String name){
this.name = name;
}
}
Direction클래스의 static상수의 값은 객체의 주소이고, 이 값은 바뀌지 않는 값이므로 ==
으로 비교가 가능한 것이다.