본문 바로가기

Java/자바 기초

12. 지네릭스, 열거형, 애너테이션

지네릭스(Generics)

다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능이다.

객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.

 

지정한 타입 외에 다른 타입의 객체가 저장되면 에러가 발생한다.

//TV객체만 저장할 수 있는 ArrayList를 생성
ArrayList<Tv> tvList = new ArrayList<Tv>();

tvList.add(new Tv()); //OK
tvList.add(new Audio()); //컴파일 에러. Tv 외에 다른 타입은 저장 불가

 

제네릭스의 장점

- 타입 안정성을 제공한다.

- 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.

 

지네릭 타입은 클래스와 메서드에 선언할 수 있다.

타입 변수는 ‘임의의 창조형 타입’을 의미한다.

class Box<T>  { // 지네릭 타입 T를 선언. T는 타입변수
  T item;

  void setItem(T item)  {
    this.item = item;
  }

  T getItem() {
    return item;
  }
}

 

지네릭 클래스가 된 Box 클래스의 객체를 생성할 때는 다음과 같이 참조변수와 생성자에 타입 T대신 사용될 실제 타입을 지정해야 한다.

- Box : 지네릭 클래스. ‘T의 Box’ 또는 ‘T Box’라고 읽는다.

- T: 타입 변수 또는 타입 매개변수.

- Box: 원시 타입

Box<String> b = new Box<String>();  // 타입 T대신 실제 타입 지정

b.setItem(new Object());  // 에러. String 외의 타입은 지정 불가
b.setItem("ABC"); // OK. String 타입이므로 가능

 

Box과 Box는 지네릭 클래스 Box에 서로 다른 타입을 대입하여 호출한 것일 뿐, 이 둘이 별개의 클래스를 의미하는 것은 아니다. 컴파일 후에 Box과 Box는 이들의 ‘원시 타입’인 Box로 바뀐다. 즉, 지네릭 타입이 제거된다. 아래와 같이 바뀐다.

//Box<String> 예시
class Box { //Box<String> -> Box
  String item; //T item -> String item

  void setItem(String item)  { //T item -> String item
    this.item = item;
  }

  String getItem() { // T getItem() -> String getItem()
    return item;
  }
}

지네릭 타입과 다형성

지네릭 클래스의 객체를 생성할 때, 참조변수에 지정해준 지네릭 타입과 생성자에 지정해준 지네릭 타입은 일치해야 한다. 서로 상속관계에 있어도 일치해야 한다.

ArrayList<Tv> list = new ArrayList<Tv>(); //OK. 일치
ArrayList<Product> list = new ArrayList<Tv>(); //에러. 불일치
    ...
class Product {}
class Tv extends Product {}
class Audio extends Product {}

 

지네릭 타입이 아닌 클래스의 타입 간에 다형성을 적용하는 것은 가능하다. 이 경우에도 지네릭 타입은 일치해야한다.

List<Tv> list = new ArrayList<Tv>(); //OK. 다형성. ArrayList가 List를 구현
List<Tv> list = new LinkedList<Tv>(); //OK. 다형성. LinkedList가 List를 구현

 

ArrayList에 Proudct의 자손 객체만 저장할 수 없을까? 그럴 때는 지네릭 타입이 Product인 ArrayList를 생성하고, 이 ArrayList에 Product의 자손인 Tv와 Audio의 객체를 저장하면 된다.

대신, ArrayList에 저장된 객체를 꺼낼 때, 형변환이 필요하다.

ArrayList<Product> list = new ArrayList<Product>();
list.add(new Product());
list.add(new Tv()); // OK.
list.add(new Audio()); //OK.

Product p = list.get(0); // Product객체는 형변환이 필요없다.
Tv t = (Tv)list.get(1); // Product의 자손객체들은 형변환을 필요로 한다.

제한된 지네릭 클래스

지네릭 타입에 ‘extends’를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.

class FruitBox<T extends Fruit> { // Fruit의 자손만 타입으로 지정가능
    ArrayList<T> list = new ArrayList<T>();
    ...
}

FruitBox<Apple> appleBox = new FruitBox<Apple>(); //OK
FruitBox<Toy> toyBox = new FruitBox<Toy>(); //에러. Toy는 Fruit의 자손이 아니다. 

 

만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면, 이때도 ‘extends’를 사용한다. ‘implements’를 사용하지 않는다는 점에 주의하자.

interface Eatable {}
class FruitBox<T extends Eatable> { ... }

 

클래스가 Fruit의 자손이면서 Eatable 인터페이스도 구현해야 한다면 아래와 같이 ‘&’기호로 연결한다.

class FruitBox<T extends Furit & Eatable> { ... }

지네릭스의 제약

모든 객체에 동일하게 동작해야하는 static멤버에 타입 변수 T를 사용할 수 없다.

T는 인스턴스 변수로 간주되기 때문이다. static멤버는 인스턴스 변수를 참조할 수 없다.

또한, static멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야하기 때문이다.

class Box<T>{
    satic T item; //에러
    static int compare(T t1, T t2) { ... } // 에러
    ...
}

 

지네릭 타입의 배열을 생성하는 것도 허용되지 않는다.

지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, ‘new T[10]’과 같이 배열을 생성하는 것은 안된다는 뜻이다. new 연산자 때문인데, 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야한다.

class Box<T>{
    T[] itemArr; //OK. T타입의 배열을 위한 참조변수
    ...

    T[] toArray(){
        T[] tmpArr = new T[itemArr.length]; //에러. 지네릭 배열 생성 불가
        ...
        return tmpArr;
    }

        ...
}

 

instanceOf 연산자도 new 연산자처럼 컴파일 시점에 타입 T를 정확히 알아야 하므로 피연산자로 사용할 수 없다.

와일드 카드

‘와일드 카드’를 사용해서 지네릭 타입에 다형성을 적용할 수 있다.

와일드 카드는 기호 ‘?’를 사용하는데 다음과 같이 ‘extends’와 ‘super’로 상한(upper bound)과 하한(lower bound)을 제한할 수 있다.

- <? extends T> : 와일드 카드의 상한 제한. T와 그 자손들만 가능

- <? super T> : 와일드 카드의 하한 제한. T와 그 조상들만 가능

- <?> : 제한 없음. 모든 타입이 가능. <? extends Object>와 동일

 

와일드 카드를 이용하면 다음과 같이 하나의 참조변수로 다른 지네릭 타입이 지정된 객체를 다룰 수 있다. (Tv와 Audio가 Product의 자손이라고 가정)

ArrayList<Product> list = new ArrayList<Tv>(); //에러. 지네릭 타입 불일치

// 지네릭 타입이 '? extends Proudct'이면, Product와 Product의 모든 자손이 OK
ArrayList<? extends Product> list = new ArrayList<Tv>(); //OK
ArrayList<? extends Product> list = new ArrayList<Audio>(); //OK

 

와일드 카드를 아래와 같이 메서드의 매개변수에 적용하면, 지네릭 타입이 다른 여러 객체를 매개변수로 지정할 수 있다. (Apple이 Fruit의 자손이라고 가정)

static Juice makeJuice(FruitBox<? extends Furit> box){
    String tmp = "";
    for(Fruit f : box.getList()) tmp += f + " ";
    return new Juice(tmp);
}

Juicer.makeJuice(new FruitBox<Fruit>()); //OK
Juicer.makeJuice(new FruitBox<Apple>()); //OK

지네릭 메서드

메소드의 선언부에 지네릭 타입이 선언된 메소드가 지네릭 메소드다.

지네릭 메서드에서 지네릭 타입의 선언 위치는 반환 타입 바로 앞이다.

지네릭 메소드는 지네릭 클래스가 아닌 클래스에도 정의할 수 있다.

class FruitBox<T> {
    ...
    //지네릭 타입인 <T>를 반환 타입 void 바로 앞에 선언.
  static <T> void sort(List<T> list, Comparator<? super T> c) {
    ...
  }
}

 

지네릭 클래스에 정의된 타입 매개변수(FruitBox의 T)와 지네릭 메소드에 정의된 타입 매개변수(static void sort의 T)는 전혀 별개의 것이다.

 

static 멤버에는 타입 매개변수를 사용할 수 없지만, 이처럼 메소드에 지네릭 타입을 선언하고 사용하는 것은 가능하다.

- 메소드에 선언된 지네릭 타입은 지역 변수를 선언한 것과 같다고 생각하면 이해하기 쉽다.

- 타입 매개변수는 메소드 내에서만 지역적으로 사용될 것이므로 메소드가 static이건 아니건 상관없다.

- 같은 이유로, 내부 클래스에 선언된 타입 문자가 외부 클래스의 타입 문자와 같아도 구별될 수 있다.

 

지네릭 메서드를 호출할 때는 아래와 같이 타입 변수에 타입을 대입해야 한다.

그러나 대부분의 경우 컴파일러가 대입된 타입을 추정할 수 있기 때문에 생략해도 된다.

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
...
Juicer.<Fruit>makeJuice(fruitBox); 
Juicer.makeJuice(fruitBox); //<Fruit> 생략 가능

 

지네릭 메서드를 호출할 때, 대입된 타입을 생략할 수 없는 경우에는 참조변수나 클래스 이름을 생략할 수 없다.

<Fruit>makeJuice(fruitBox); //에러. 클래스 이름 생략 불가
this.<Fruit>makeJuice(fruitBox); //OK
Juicer.<Fruit>makeJuice(fruitBox); //OK

지네릭 타입의 형변환

지네릭 타입과 넌지네릭(non-generic) 타입 간의 형변환은 항상 가능하다. 다만 경고가 발생할 뿐이다.

Box box = null;
Box<Object> objBox = null;

box = (Box)objBox; //OK. 지네릭 타입 -> 원시 타입(넌지네릭 타입). 경고 발생
objBox = (Box<Object>); //OK. 원시 타입(넌지네릭 타입) -> 지네릭 타입. 경고 발생

 

대입된 타입이 다른 지네릭 타입 간에는 형변환이 불가능하다.

Box<Object> objBox = null;
Box<String> strBox = null;

strBox = (Box<String>)objBox; //에러. Box<Object> -> Box<String>
objBox= (Box<Object>)strBox ; //에러. Box<String> -> Box<Object>

 

와일드 카드를 사용한 지네릭 타입은 형변환이 가능하다.

Box<? extends Object> wBox = new Box<String>();

//형변환이 가능하기 떄문에 makeJuice메서드의 매개변수에 다형성이 적용될 수 있었다.
static Juice makeJuice(FruitBox<? extends Furit> box){
    String tmp = "";
    for(Fruit f : box.getList()) tmp += f + " ";
    return new Juice(tmp);
}

Juicer.makeJuice(new FruitBox<Fruit>());
//OK. FruitBox<? extends Furit> box = new FruitBox<Fruit>() 처럼 
//makeJuice 매개변수에 대입한 것과 같다.

지네릭 타입의 제거

컴파일러는 지네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다. 그리고 지네릭 타입을 제거한다. 즉, 컴파일된 파일(*.class)에는 지네릭 타입에 대한 정보가 없는 것이다. 이렇게 하는 주된 이유는 지네릭이 도입되기 이전(JDK1.5 이전)의 소스 코드와의 호환성을 유지하기 위해서이다.

열거형(enum)

열거형은 여러 상수를 선언해야 할 때, 편리하게 선언할 수 있는 방법이다.

 

enum을 사용하지 않을 때

class Card{

    static final int CLOVER = 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;
}

 

enum을 사용할 때

enum을 이용하면 따로 값을 지정해주지 않아도 자동적으로 0부터 시작하는 정수값이 할당된다.

class Card{ //     0  ,   1  ,    2   ,   3
    enum Kind { CLOVER, HEART, DIAMOND, SPADE } //열거형 Kind 정의
    enum Value { TWO, THREE, FOUR }                //열거형 Value 정의

    final Kind kind;   //타입이 int가 아닌 Kind임에 유의하자.
    final Vlaue value;

}

 

enum을 사용하지 않을 때 Card.CLOVER와 Card.TWO의 값이 0이라 조건식이 true가 되는데, 사실 카드의 무늬와 숫자는 비교 대상이 아니므로 이 조건식은 의미상 false가 되는 것이 맞다.

if(Card.CLOVER==Card.TWO) //true이지만 false이어야 의미상 맞음.

 

enum을 사용해서 상수를 정의한 경우, 값을 비교하기 전에 타입을 먼저 비교하므로 값이 같더라도 타입이 다르면 컴파일 에러가 발생한다.

if(Card.Kind.CLOVER==Card.Value.TWO) //컴파일 에러. 타입이 달라서 비교가 불가

열거형의 정의와 사용

열거형 정의하는 방법

enum 열거형이름 { 상수명1, 상수명2, ...}

//예시
enum Direction { EAST, SOUTH, WEST, NORTH }

 

정의된 상수 사용하는 방법은 ‘열거형이름.상수명’이다.

클래스의 static변수를 참조하는 것과 동일하다.

enum Direction { EAST, SOUTH, WEST, NORTH }

class Unit {
    int x, y; // 유닛의 위치
    Direction dir; // 열거형 인스턴스 변수를 선언

    void init(){
        dir = Direction.EAST; // 유닛의 방향을 EAST로 초기화
    }
}

 

열거형 상수 간의 비교에는 ‘==’를 사용할 수 있다.

‘<’, ‘>’와 같은 비교연산자는 사용할 수 없고 compareTo()는 사용 가능하다.

compareTo()는 두 비교 대상이 같으면 0, 왼쪽이 크면 양수, 오른쪽이 크면 음수를 반환한다.

if(dir==Direction.EAST){
    x++;
}else if (dir > Direction.WEST) { //에러. 열거형 상수에 비교연산자 사용 불가
    ...
}else if (dir.compareTo(Direction.WEST) > 0) { // compareTo()는 가능
    ...
}

열거형의 조상 - java.lang.Enum

모든 열거형의 조상은 java.lang.Enum이고, 다음 메서드들이 정의되어 있다.

메서드 설명
Class<E> getDeclaringClass() 열거형의 Class객체를 반환한다.
String name() 열거형 상수의 이름을 문자열로 반환한다.
int ordinal() 열거형 상수가 정의된 순서를 반환한다. (0부터 시작)
T valueOf(Class<T> enumType, String name) 지정된 열거형에서 name과 일치하는 열거형 상수를 반환한다.

 

컴파일러가 모든 열거형에 자동적으로 values()와 valueof() 메서드를 추가해준다.

- static E[] values() : 열거형에 정의된 모든 상수를 출력하는데 사용된다.

Direction[] dArr = Direction.values();

for(Direction d : dArr) // for(Direction d : Direction.values())
	System.out.printf("%s=%d%n", d.name(), d.ordinal());

 

- static E valueOf(String name) : 열거형 상수 이름으로 문자열 상수에 대한 참조를 얻을 수 있게 해준다.

Direction d = Direction.valueOf("WEST");

System.out.println(d); // WEST
System.out.println(Direction.WEST==Direction.valueOf("WEST")); //true

열거형 예제

enum Direction { EAST, SOUTH, WEST, NORTH }

class Ex12_5 {
    public static void main(String[] args) {
        Direction d1 = Direction.EAST;
        Direction d2 = Direction.valueOf("WEST");
        Direction d3 = Enum.valueOf(Direction.class, "EAST");

        System.out.println("d1="+d1); 
        System.out.println("d2="+d2);
        System.out.println("d3="+d3);

        System.out.println("d1==d2 ? "+ (d1==d2));
        System.out.println("d1==d3 ? "+ (d1==d3));
        System.out.println("d1.equals(d3) ? "+ d1.equals(d3));
//        System.out.println("d2 > d3 ? "+ (d1 > d3)); // 에러
        System.out.println("d1.compareTo(d3) ? "+ (d1.compareTo(d3)));
        System.out.println("d1.compareTo(d2) ? "+ (d1.compareTo(d2)));

        switch(d1) {
            case EAST: // Direction.EAST라고 쓸 수 없다.
                System.out.println("The direction is EAST."); break;
            case SOUTH:
                System.out.println("The direction is SOUTH."); break;
            case WEST:
                System.out.println("The direction is WEST."); break;
            case NORTH:
                System.out.println("The direction is NORTH."); break;
            default:
                System.out.println("Invalid direction."); break;
        }

        Direction[] dArr = Direction.values();

        for(Direction d : dArr)  // for(Direction d : Direction.values()) 
            System.out.printf("%s=%d%n", d.name(), d.ordinal()); 
    }
}

/* 출력 결과

d1=EAST
d2=WEST
d3=EAST
d1==d2 ? false
d1==d3 ? true
d1.equals(d3) ? true
d1.compareTo(d3) ? 0
d1.compareTo(d2) ? -2
The direction is EAST.
EAST=0
SOUTH=1
WEST=2
NORTH=3

*/

열거형에 멤버 추가하기

Enum에서 자동으로 정해준 숫자를 열거형 상수의 값으로 사용하지 않는 것이 좋다. 순서가 변경될 수 있기 때문이다.

 

열거형 상수의 값이 불규칙적인 경우에는 다음과 같이 열거형 상수의 이름 옆에 원하는 값을 괄호()와 함께 적어주면 된다. 그리고 지정된 값을 저장할 수 있는 인스턴스 변수와 생성자를 새로 추가해 주어야 한다. 이때 주의할 점은, 먼저 열거형 상수를 모두 정의한 다음에 다른 멤버들을 추가해야 한다. 그리고 열거형 상수의 마지막에 ‘;’도 잊지 말아야 한다.

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; } //외부에서 value값을 얻을 수 있도록 추가
}

 

열거형의 객체를 생성자가 추가되었지만, 열거형의 객체는 생성할 수 없다. 제어자가 묵시적으로 private이기 때문이다.

Direction d = new Direction(1); // 에러. 열거형의 생성자는 외부에서 호출불가

enum Direction{
    ...
    Direction(int value) {  // private Direction(int value)와 동일
        ...
}

열거형에 멤버 추가하기 예제

enum Direction2 { 
    EAST(1, ">"), SOUTH(2,"V"), WEST(3, "<"), NORTH(4,"^");

    private static final Direction2[] DIR_ARR = Direction2.values();
    private final int value;
    private final String symbol;

    Direction2(int value, String symbol) { // 접근 제어자 private이 생략됨
        this.value  = value;
        this.symbol = symbol;
    }

    public int getValue()     { return value;  }
    public String getSymbol() { return symbol; }

    public static Direction2 of(int dir) {
        if (dir < 1 || dir > 4) 
            throw new IllegalArgumentException("Invalid value :" + dir);

        return DIR_ARR[dir - 1];
    }    

    // 방향을 회전시키는 메서드. num의 값만큼 90도씩 시계방향으로 회전한다.
    public Direction2 rotate(int num) {
        num = num % 4;

        if(num < 0) num +=4; // num이 음수일 때는 시계반대 방향으로 회전 

        return DIR_ARR[(value-1+num) % 4];
    }

    public String toString() {
        return name()+getSymbol();
    }
} // enum Direction2

class Ex12_6 {
    public static void main(String[] args) {
        for(Direction2 d : Direction2.values()) 
            System.out.printf("%s=%d%n", d.name(), d.getValue()); 
        System.out.println();

        Direction2 d1 = Direction2.EAST;
        Direction2 d2 = Direction2.of(1);

        System.out.printf("d1=%s, %d%n", d1.name(), d1.getValue());
        System.out.printf("d2=%s, %d%n", d2.name(), d2.getValue());
        System.out.println();

        System.out.println(Direction2.EAST.rotate(1));
        System.out.println(Direction2.EAST.rotate(2));
        System.out.println(Direction2.EAST.rotate(-1));
        System.out.println(Direction2.EAST.rotate(-2));
    }
}

/*
출력 결과

EAST=1
SOUTH=2
WEST=3
NORTH=4

d1=EAST, 1
d2=EAST, 1

SOUTHV
WEST<
NORTH^
WEST<

*/

애너테이션이란?

애너테이션이란 프로그램의 소스코드 안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것이다.

 

애네테이션은 주석(comment)처럼 프로그래밍 언어에 영향을 미치지 않으면서도 다른 프로그램에게 유용한 정보를 제공할 수 있다는 장점이 있다. 예를 들어, 자신이 작성한 소스코드 중에서 특정 메서드만 테스트하기를 원한다면, 다음과 같이 ‘@Test’라는 애너테이션을 메서드 앞에 붙인다. ‘@Test’는 ‘이 메서드를 테스트해야 한다’는 것을 테스트 프로그램에게 알리는 역할을 할 뿐, 메서드가 포함된 프로그램 자체에는 아무런 영향을 미치지 않는다. 주석처럼 존재하지 않는 것이나 다름없다.

@Test // 이 메서드가 테스트 대상임을 테스트 프로그램에게 알린다.
public void method(){
	...
}

해당 프로그램에 미리 정의된 종류와 형식으로 애너테이션을 작성해야만 의미가 있다.

 

JDK에서 제공하는 표준 애너테이션은 주로 컴파일러를 위한 것으로 컴파일러에게 유용한 정보를 제공한다. 새로운 애너테이션을 정의할 때 사용하는 메타 애너테이션도 제공한다.

표준 애너테이션

표준 애너테이션은 주로 컴파일러를 위한 것으로 컴파일러에게 유용한 정보를 제공한다.

애너테이션 설명
@Override 컴파일러에게 메서드를 오버라이딩하는 것이라고 알린다.
@Deprecated 앞으로 사용하지 않을 것을 권장하는 대상에 붙인다.
@SuppressWarnings 컴파일러의 특정 경고메시지가 나타나지 않게 해준다.
@SafeVarargs 지네릭스 타입의 가변인자에 사용한다. (JDK1.7)
@FunctionalInterface 함수형 인터페이스라는 것을 알린다. (JDK 1.8)
@Native native메서드에서 참조되는 상수 앞에 붙인다. (JDK 1.8)

@Override

조상의 메서드를 오버라이딩하는 것이라는 걸 컴파일러에게 알려주는 역할을 한다.

 

오버라이딩 할 때, 조상 메서드의 이름을 잘못 써도 컴파일러는 이것이 잘못된 것인지 알지 못한다. 새로운 이름의 메서드가 추가되는 것으로 인식할 뿐이다. 오류가 발생하지 않고 조상의 메서드가 호출되므로 어디서 잘못되었는 지 알아내기 어렵다.

 

@Override를 붙이면, 컴파일러가 같은 이름의 메서드가 조상에 있는 지 확인하고 없으면, 에러메시지를 출력한다. 알아내기 어려운 실수를 미연에 방지해주니 반드시 붙이도록 하자.

class Parent {
    void parentMethod() { }
}

class Child extends Parent {
    @Override
    void parentmethod() { } // 조상 메서드의 이름을 잘못 적었음. 
}

/*
출력 결과
Ex12_7.java:6: error: method
does not override or implement a method from a supertype
@Overide

1 error

*/

@Deprecated

앞으로 사용하지 않을 것을 권장하는 대상에 붙인다.

 

하위호환성을 고려하여 기존의 것들을 함부로 삭제하지 않고 @Deprecated를 사용하는데, 이를 사용할 경우 컴파일 시 경고 메시지가 출력된다.

class NewClass {
    @Deprecated
    int oldField;

    @Deprecated
    int getOldField(){
        return oldField;
    }
}

/* 
@Deprecated가 붙은 대상을 사용하는 코드를 작성하면,
컴파일 할 때 아래와 같은 메시지가 나타난다.

Note: AnnotationEx2.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details. 

*/

@FunctionalInteface

‘함수형 인터페이스(functional interface)’를 선언할 때, 이 애너테이션을 붙이면 컴파일러가 ‘함수형 인터페이스’를 올바르게 선언했는지 확인하고, 잘못된 경우 에러를 발생시킨다. 실수를 방지할 수 있으므로 ‘함수형 인터페이스’를 선언할 때는 이 어노테이션을 반드시 붙이도록 하자.

 

함수형 인터페이스는 추상 메서드가 하나뿐이어야 한다는 제약이 있다.

@FunctionalInterface
public interface Runnable{
    public abstract void run(); // 추상 메서드
}

@SuppressWarnings

컴파일러가 보여주는 경고메시지가 나타나지 않게 억제해준다.

 

@SuppressWarnings로 억제할 수 있는 경고 메시지의 종류는 여러 가지가 있는데, 주로 사용하는 것은 ‘deprecation’, ‘unchecked’, ‘rawtypes’, ‘varargs’ 정도이다.

- deprecation: @Deprecated가 붙은 대상을 사용해서 발생하는 경고를 억제한다.

- unchecked: 지네릭스 타입을 지정하지 않았을 때 발생하는 경고를 억제한다.

- rawtypes: 지네릭스를 사용하지 않아서 발생하는 경고를 억제한다.

- varargs: 가변인자의 타입이 지네릭 타입일 때 발생하는 경고를 억제한다.

@SuppressWarning("unchecked")     // 지네릭스와 관련된 경고를 억제
ArrayList list = new ArrayList(); // 지네릭 타입을 지정하지 않았음
list.add(obj);                    // 여기서 경고가 발생하지만 억제됨

 

둘 이상의 경고를 동시에 억제하려면, 아래와 같이, 배열에서처럼 괄호{}를 추가로 사용해야 한다.

@SuppressWarnings({"deprecation", "unchecked", "varargs"})

메타 애너테이션

‘에너테이션을 위한 애너테이션’, 즉 애너테이션을 정의할 때 애너테이션의 적용대상이나 유지기간을 지정하는데 사용한다.

애너테이션 설명
@Target 애너테이션이 적용가능한 대상을 지정하는데 사용한다.
@Documented 애너테이션 정보가 javadoc으로 작성된 문서에 포함되게 한다.
@Inherited 애너테이션이 자손 클래스에 상속되도록 한다.
@Retention 애너테이션이 유지되는 범위를 지정하는데 사용한다.
@Repeatable 애너테이션을 반복해서 적용할 수 있게 한다. (JDK1.8)

@Target

애너테이션이 적용가능한 대상을 지정하는데 사용된다.

 

‘@Target’으로 지정할 수 있는 애너테이션 적용 대상의 종류는 아래와 같다.

  • ANNOTATIN_TYPE : 애노테이션
  • CONSTRUCTOR : 생성자
  • FIELD : 필드(멤버 변수, enum 상수), 기본형에 사용
  • LOCAL_VARIABLE : 지역변수
  • METHOD : 메서드
  • PACKAGE : 패키지
  • PARAMETER : 매개변수
  • TYPE : 타입(클래스, 인터페이스(애노테이션 포함), enum)을 선언할 때
  • TYPE_PARAMETER : 지네릭의 타입 매개변수(JDK 1.8)
  • TYPE_USE : 타입이 사용되는 모든 곳(JDK 1.8), 타입의 변수를 선언할 때

 

예시

import static java.lang.annotation.ElementType.*; 

@Target({FIELD, TYPE, METHOD, TYPE_USE}) //적용대상이 FIELD, TYPE, METHOD, TYPE_USE
public @interface MyAnnotation { }       //MyAnnotation을 정의

@MyAnnotation //적용대상이 TYPE인 경우
class Hello {

    @MyAnnotation //적용대상이 FIELD인 경우
    int num; 

    @MyAnnotation //적용대상이 TYPE_USE인 경우
    MyClass mc;

    @MyAnnotation //적용대상이 METHOD인 경우 
    public int getNum() { return num; }
}

@Retention

애너테이션이 유지되는 기간을 지정하는 데 사용된다.

 

애너테이션의 유지 정책의 종류는 다음과 같다.

  • SOURCE : 소스 파일에만 존재, 클래스 파일에는 존재하지 않는다.
  • CLASS(디폴트 값) : 클래스 파일에 존재, 실행 시에 사용 불가능하다.
  • RUNTIME : 클래스 파일에 존재, 실행 시에 사용 가능하다.

@Documented, @Inherited

@Documented

애너테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록 한다.

 

@Inherited

애너테이션이 자손 클래스에 상속되도록 한다.

‘@Inherited’가 붙은 애너테이션을 조상 클래스에 붙이면, 자손 클래스도 이 애너테이션이 붙은 것과 같이 인식된다.

@Inherited  //@SuperAnno가 자손까지 영향 미치게
@interface SuperAnno{ } 

@SuperAnno
class Parent { } 

class Child extends Parent { } //Child에 @SuperAnno 애너테이션이 붙은 것으로 인식

@Repeatable

하나의 대상에 같은 애너테이션을 여러 번 붙일 수 있도록 해준다.

@Repeatable(ToDos.class) // ToDo애너테이션을 여러 번 반복해서 쓸 수 있게 한다.
@interface ToDo {
    String value();
}

@ToDo("delete test codes.")
@ToDO("override inherited methods")
class MyClass{
    ...
}

 

일반 애너테이션과 달리 하나의 대상에 같은 이름의 애너테이션이 여러 개가 사용되기 때문에 이를 묶어서 보관할 컨테이너 애너테이션을 별도로 사용해야 한다.

@Interface ToDos{ // 여러 개의 ToDo애너테이션을 담을 컨테이너 애너테이션 ToDos
    ToDo[] value(); // ToDo애너테이션 배열타입의 요소를 선언. 이름이 반드시 value이어야 함
}

@Repeatable(ToDos.class) // 괄호 안에 컨테이너 애너테이션을 지정해 줘야한다.
@interface ToDo {
    String value();
}

애너테이션 타입 정의하기

새로운 애너테이션을 정의하는 방법은 아래와 같다.

‘@’기호를 붙이는 것을 제외하면 인터페이스를 정의하는 것과 동일하다.

@interface 애너테이션이름 {
    타입 요소이름(); //애너테이션의 요소를 선언한다.
    ...
}

엄밀히 말해서 ‘@Override’는 애너테이션이고, ‘Override’는 애너테이션의 타입이다.

애너테이션의 요소

애너테이션 내에 선언된 메서드를 ‘애너테이션의 요소(element)’라고 한다.

애너테이션의 요소는 반환값이 있고, 매개변수는 없는 추상 메서드의 형태를 가진다.

@interface Testinfo{
    int       count();
    String    testedBy();
    String[]  testTools();
    TestType  testType(); // enum TestType { FIRST, FINAL }
    DateTime  testDate(); // 자신이 아닌 다른 애너테이션(@DateTime)을 포함할 수 있다.
}

@interface DateTime{
    String yymmdd();
    String hhmmss();
}

 

추상 메서드이지만 상속을 통해 구현하지 않아도 된다.요소의 이름도 같이 적어주므로 순서는 상관 없다.

다만, 애너테이션을 적용할 때 이 요소들의 값을 빠짐없이 지정해주어야 한다.

@TestInfo(
    count=3,
  testedBy="Kim",
    testType=TestType.FIRST,
    testDate=@DateTime(yymmdd="160101", hhmmss="235959")
)
public class NewClass { ... }

 

애너테이션의 각 요소는 기본값을 가질 수 있으며, 기본값이 있는 요소는 애너테이션을 적용할 때 값을 지정하지 않으면 기본값이 사용된다.

- 기본값으로 null을 제외한 모든 리터럴이 가능하다.

- 애너테이션도 인터페이스처럼 애너테이션의 요소로 상수를 정의할 수 있지만, 디폴트 메서드는 정의할 수 없다.

@interface TestInfo{
    int count() default 1;  //기본값을 1로 지정
}

@Testinfo // @TestInfo(count=1)과 동일
public class NewClass { .. }

 

요소의 타입이 배열인 경우, 괄호{}를 사용해서 여러 개의 값을 지정할 수 있다.

@inteface TestInfo{
    String[] testTools();
}

@Test(testTools={"JUnit", "AutoTester"}) // 값이 여러 개인 경우
@Test(testTools="JUnit")  //값이 하나일 때는 괄호{}생략가능
@Test(testTools={}) //값이 없을 때는 괄호{}가 반드시 필요

 

기본값을 저장할 때도 괄호{}를 사용할 수 있다.

@interface TestInfo{
    String[] info() default {"aaa", "bbb"}; // 기본값이 여러 개인 경우. 괄호{} 사용
    String[] info2() default "ccc"; //기본값이 하나인 경우, 괄호 생략가능
}

@Testinfo // @TestInfo(info={"aaa", "bbb"}, info2="ccc")와 동일
@TestInfo(info2={})  // @TestInfo(info={"aaa", "bbb"}, info2={})와 동일
class NewClass { ... }

 

애너테이션 요소가 오직 하나뿐이고 이름이 value인 경우, 애너테이션을 적용할 때 요소의 이름을 생략하고 값만 적어도 된다. 요소의 타입이 배열일 때도 요소의 이름이 value이면, 요소의 이름을 생략할 수 있다.

@interface TestInfo{
    String value();
}

@interface TestInfo2{
    Sring[] value();
}

@TestInfo("passed")  // @TestInfo(value="passed")와 동일
@Testinfo2({"passed", "failed"}) //@TestInfo(value={"passed", "failed"})와 동일
class NewClass { ... }

마커 애너테이션

요소가 하나도 정의되지 않은 애너테이션을 마커 애너테이션이라고 한다.

@Target(ElementType.METHOD)
@Retention(RetiontionPolicy.SOURCE)
public @interface Override {} // 마커 애너테이션. 정의된 요소가 하나도 없다. 

애너테이션 요소의 규칙

애너테이션의 요소를 선언할 때 반드시 지켜야 하는 규칙은 다음과 같다.

  • 요소의 타입은 기본형, String, enum, 애너테이션, Class만 허용된다.
  • ( ) 안에 매개변수를 선언할 수 없다.
  • 예외를 선언할 수 없다.
  • 요소를 타입 매개변수로 정의할 수 없다.

 

잘못된 예

@interface AnnoTest{
    int id = 100;                        // OK. 상수 선언. static final int id = 10;
    String major(int i, int j);          // 에러. 매개변수를 선언할 수 없음
    String minor() throws Excpetion;     // 에러. 예외를 선언할 수 없음
    ArrayList<T> list();                 // 에러. 요소의 타입에 타입 매개변수 사용불가
}

애너테이션의 활용 예제

import java.lang.annotation.*;

@Deprecated
@SuppressWarnings("1111") // 유효하지 않은 애너테이션은 무시된다.
@TestInfo(testedBy="aaa", testDate=@DateTime(yymmdd="160101",hhmmss="235959"))
class Ex12_8 {
    public static void main(String args[]) {
        // Ex12_8의 Class객체를 얻는다.
        Class<Ex12_8> cls = Ex12_8.class;

        TestInfo anno = cls.getAnnotation(TestInfo.class);
        System.out.println("anno.testedBy()="+anno.testedBy());
        System.out.println("anno.testDate().yymmdd()=" +anno.testDate().yymmdd());
        System.out.println("anno.testDate().hhmmss()=" +anno.testDate().hhmmss());

/* 출력 결과
    anno.testedBy()=aaa
    anno.testDate().yymmdd()=160101
    anno.testDate().hhmmss()=235959
*/

        for(String str : anno.testTools())
            System.out.println("testTools="+str);

/* 출력 결과
    testTools=JUnit
*/

        System.out.println();

        // Ex12_8에 적용된 모든 애너테이션을 가져온다.
        Annotation[] annoArr = cls.getAnnotations();

        for(Annotation a : annoArr)
            System.out.println(a);

/* 출력 결과
    @java.lang.Deprecated()
    @TestInfo(count=1, testType=FIRST, testTools=[JUnit], testedBy=aaa,
                            testDate=@DateTime(yymmdd=160101, hhmmss=235959)
*/

    } // main의 끝
}

@Retention(RetentionPolicy.RUNTIME)  // 실행 시에 사용가능하도록 지정 
@interface TestInfo {
    int       count()          default 1;
    String    testedBy();
    String[]  testTools()     default "JUnit";
    TestType  testType()    default TestType.FIRST;
    DateTime  testDate();
}

@Retention(RetentionPolicy.RUNTIME)  // 실행 시에 사용가능하도록 지정
@interface DateTime {
    String yymmdd();
    String hhmmss();
}

enum TestType { FIRST, FINAL }

'Java > 자바 기초' 카테고리의 다른 글

11. 컬렉션 프레임웍  (0) 2022.03.14
10. 날짜와 시간 & 형식화  (0) 2022.03.14
9. java.lang 패키지와 유용한 클래스  (0) 2022.03.14
8. 예외처리  (0) 2022.03.14
7. 객체지향 프로그래밍 II  (0) 2022.03.11