세상에 나쁜 코드는 없다

[Java] Generics, Annotation, Enum 본문

Computer Science/Java

[Java] Generics, Annotation, Enum

Beomseok Seo 2022. 5. 26. 21:56

Generics 지네릭스

Generics 란 ?

지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크(compile-time type check)를 해주는 기능이다.

객체의 타입을 컴파일 시에 체크하면 객체의 타입 안정성을 높일 수 있고 형변환의 번거로움을 줄일 수 있다.

  • 객체의 타입 안정성을 높인다
    • 의도치 않은 타입의 객체가 저장되는 것을 막는다.
    • 원래 의도와 다르게 잘못 형변환되는 경우를 막는다.
  • 형변환의 번거로움을 줄인다
    • 컬렉션 클래스의 경우 보통 한 종류의 객체만을 담는 경우가 많은데, 그럼에도 불구하고 꺼낼 때마다 타입체크를 해야하고 형변환을 하는 것은 불편하다.

// 지네릭스가 사용되지 않는다면..

  Class ArrayList {
        private Object arr;
        private int index = 0;

        public ArrayList(int size) {
            arr = new Object[size];
        }


        public int set(Object item) {
            arr[index] = item;
            return index++;
        }

        public Object get(int index) {
        return arr[index];
      }
  }

Class ArrayListMain {
  public static void main(String args[]) {
    ArrayList arrList = new ArrayList(10);
    arrList.set(1);
    int item = (int) arrList.get(0); 
    // 1반환, object arr 이므로 형변환이 필요함.

    ...

    arrList.set("String type Item"); 
    // 의도치 않은 타입의 변수가 들어가도 문제가 생기지 않음
  }  
}

지네릭 클래스의 선언

지네릭 클래스를 생성하는 방법은 클래스 네임 옆에 <지네릭변수> 를 넣고, 지네릭 타입이 들어가야 할 위치에 지네릭 변수를 넣어주면 된다

//ArrayList 클래스명 우측에 <T>를 더해주었고, Object가 들어가는 위치마다 T를 넣어주었다.
  Class ArrayList<T> {
        private T arr;
        private int index = 0;

        public ArrayList(int size) {
            arr = new T[size];
        }


        public int set(T item) {
            arr[index] = item;
            return index++;
        }

        public T get(int index) {
        return arr[index];
      }
  }

위 예시 코드에서 T 를 '타입 변수'라고 하며, T는 변수이므로 다른 것을 사용하여도 된다. 실제 컬렉션 클래스의 경우 를 사용하는데 이는 'element'의 첫 글자를 땄다고 한다.
Map<K,V>과 같이 지네릭 변수를 여러개 사용하는 경우도 있다. 무조건 T를 사용하는 것 보단, 상황에 맞게 의미있는 문자를 선택하는 것이 좋다. 아래 표는 암묵적으로 사용하는 지네릭 변수명이다.

변수 설명
E Element
K Key
N Number
T Type
V Value
* S, U, V 등 - 2번째,3번째, 4번째 타입  

위와 같이 지네릭 변수를 통해 객체를 생성하려면 아래와 같이 <> 사이에 실제 사용될 타입을 지정해 주면 된다.

    ArrayList<Integer> intList = new ArrayList<Integer>(10);
    intList.set(1); //원시타입 1을 넣으면 자동으로 Integer로 Wrapping되어 저장된다.
    int item = intList.get(0); // 1 (형변환이 필요없음)
    //intList.set("String type Value") 에러. Integer 타입만 가능

위와 같이 생성한다면 ArrayList 는 다음과 같이 정의된 것과 같다.

  Class ArrayList { // Generic 타입을 String으로 지정한다면 T 부분이 모두 String으로 바뀜
        private String arr;
        private int index = 0;

        public ArrayList(int size) {
            arr = new String[size];
        }

        public int set(String item) {
            arr[index] = item;
            return index++;
        }

        public String get(String index) {
        return arr[index];
      }
  }

JDK1.7부터는 추정이 가능한 경우 타입을 생략할 수 있게 되었다.

    ArrayList<Integer> intList = new ArrayList<Integer>(10);
    ArrayList<Integer> intList2 = new ArrayList<>(); // OK.

또한 지네릭 타입에 들어갈 변수의 타입의 종류를 제한하고 싶으면 extends 를 사용하면 된다.

class ExClass<T> extends SomeObject> {
    ...
} //SomeObject나 그 자손 클래스만 지정 가능

특정 인터페이스를 구현한 객체만 오게하고 싶을때 역시 기호를 붙여서 사용하면 된다.

class ExClass<T> extends Someable> {
    ...
}

특정클래스의 자손이면서 동시에 특정 인터페이스를 구현한 객체만 오게 하고 싶다면 '&'를 사용하여 구현할 수 있다.

class ExClass<T extends SomeObject&Someable> { ///SomeObject이나 그 자손 클래스이면서 동시에 Someable 을 implements 한 클래스만 타입으로 지정할 수 있다.
 ... 
}

지네릭스의 제한

지네릭스는 그 특성에 의하여 몇가지 제한 점이 있다.

  1. Static 멤버에 타입변수를 사용할 수 없다.
    모든 객체에 대해 동일하게 동작해야하는 static 멤버는 객체에 따라 달라질 수 있는 타입변수를 사용할 수 없다.
class List {  
static E item; // 에러  
static int compare(E e1, E e2); //에러  
}
  1. 지네릭 타입의 배열을 생성하는 것은 허용되지 않는다.

지네릭 배열을 생성할 수 없는 것은 new연산자 때문인데, 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다. 하지만 지네릭 클래스의 경우 컴파일 시점에서 T의 타입을 알 수 없기 때문에 생성이 불가능하다. 같은 원리로 instatnceof연산자도 T를 피연산자로 사용할 수 없다.

지네릭 배열을 생성할 필요가 있을 때는, new 연산자 대신 'Reflection API'의 newInstance() 와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object 배열을 생성해서 복사한 다음에 "T[]" 로 형변환 하는 방식을 사용해야 한다.

//실제 java.util.ArrayList의 코드 중 일부
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

    transient Object[] elementData; //데이터가 저장되는 곳. 제네릭 타입이 아닌 Object 배열로 생성되어있음

    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e; //Object 로 e를 저장
        size = s + 1;
    }

    public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }

    E elementData(int index) {
        return (E) elementData[index]; //elementData 에서 index의 요소를 형변환 하여 반환
    }

    public E get(int index) {
        Objects.checkIndex(index, size);
        return elementData(index);
    }

}

와 일 드 카 드

와일드 카드가 필요한 이유

제네릭 클래스에서 상위/하위타입 알아보기

String 타입은 상위 타입인 Object클래스에 넣을 수 있다.

String string = "string!";
Object object = string;
String string2 = (String)obj;

그럼 String타입 리스트를 Object타입 리스트 변수에 넣을 수 있을까?

List<String> stringList = new ArrayList<>();
List<Object> objectList = stringList;

위 코드는 컴파일 에러가 발생하는데, 이는 objectList 변수가 stringList의 의 상위타입이 아니어서 그렇다. 만약 위 코드가 가능하다면 아래와 같이 문제가 발생할 수 있다.

objectList.add(1); 
String s = stringList.get(0) // 1이 나와야함.
//stringList 에 Integer타입 변수가 들어갈 수 있는 상황

String은 Object의 하위타입이지만, List은 List

의 하위타입이 아니다.
타입 파라미터를 통해서는 상하위 관계가 생기지 않고 오직 raw-type 간에만 상하위 관계가 생긴다.
이러한 문제때문에 발생하는 문제를 살펴보자.

Collection 프레임워크의 데이터를 출력하는 기능을 담은 메서드를 아래와 같이 만들었다.


    static void printCollection(Collection<Object> c) {
        for(Object e : c) {
            System.out.println(e);
        }
    }

    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        printCollection(strList);//컴파일에러
    }

컬렉션의 지네릭 변수가 여러 타입이 올 수 있기 때문에 가장 상위 타입인 Object를 넣어야 한다고 생각할 수 있겠지만, 위 코드는 컴파일 에러가 뜬다. 이유는 아까 살펴봤던데로 List

는 각종 List들의 상위 타입이 아니기 때문이다.

?의 등장

이러한 문제를 해결하여 지네릭 타입과 상관없이 컬렉션을 받아서 출력하는 메소드를 만드려면 wildcard '?' 를 사용하면 된다. 위 메서드의

부분을 <?>로 바꾸어 사용할 수 있다.

    static void printCollection(Collection<?> c) { ... }

? 만 사용한다면 어떤 지네릭 타입이든간에 메서드의 파라미터로 가져와 사용할 수 있다.
만약 와일드카드로 들어오는 지네릭 타입을 제한하고 싶으면 지네릭타입의 제한 방법과 동일하게
extends, super을 사용하여 제한해 줄 수 있다.

    static void printCollection(Collection<? extends SomeClass> c) { ... }

     static void printCollection(Collection<? super SomeClass> c) { ... }

지네릭 타입의 제거

컴파일러는 컴파일 단계에서 지네릭 타입을 이용하여 소스파일을 체크하고 필요한 곳에 형변환을 넣어준 후 지네릭 타입을 제거한다. 이를 통해 컴파일된 파일에는 지네릭 타입에 대한 정보가 없게 된다.

  1. 지네릭 타입의 경계(bound)를 제거한다.
  • 지네릭 타입이 라면 T는 SomeObject로 치환된다. 인 경우 T는 Object로 치환된다. 그 후 클래스 옆의 선언은 제거된다.
class SomeClass<T extends SomeObject> {
    void someMethod(T t) { ... }
}

-->

class SomeClass {
    void someMethod(SomeObject t) { ... }
}
  1. 지네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가한다.
T get(int i) {
    return list.get(i); //list.get()은 Object를 반환
}

--->

SomeObject get(int i) {
    return (SomeObject) list.get(i);
}

와일드 카드의 경우에도 치환과 형변환이 동일하게 적용된다.

@Annotation (에너테이션)

에너테이션이란

마치 주석과 같이 프로그래밍 언어에 영향을 미치지 않으면서 다른 프로그램에게 유용한 정보를 제공하는 것.

소스코드 안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시키는 것.

  • 스프링부트를 사용할때 테스트코드중 특정 메서드를 테스트하길 원한다면 메서드 앞에 "@Test" 라는 에너테이션을 붙여야 한다. 이 @Test는 메서드의 코드를 전혀 건드리지 않고 (영향을 미치지 않으면서) JUnit 에게 이 메서드가 테스트가 되어져야할 메서드라는것을 알린다.
  • 스프링의 @Bean 에너테이션은 해당 클래스가 스프링 빈으로 관리해야하는 클래스라는 것을 스프링 설정 클래스에 알린다.
    • 기존엔 별도의 xml 파일로 설정해야하는 불편함이 있었지만, 설정하는 부분을 소스코드내로 끌고와 편하게 설정할 수 있다.

자바에서 제공하는 표준 에너테이션

자바에서 제공하는 표준 에너테이션은 주로 컴파일러를 위한 것으로 컴파일러에게 정보를 제공한다.

에너테이션 설명
@Override 컴파일러에게 오버라이딩하는 메서드라는 것을 알린다.
@Deprecated 앞으로 사용하지 않을 것을 권장하는 대상에 붙인다.
@SuppressWarnings 컴파일러의 특정 경고메시지가 나타나지 않게 해준다.
@SafeVarargs 지네릭스 타입의 가변인자에 사용한다.
@FunctionalInterface 함수형 인터페이스라는 것을 알린다.
@Native 네이티브메서드에서 참조되는 상수 앞에 붙인다.
--- 이하는 메타 에너테이션
@Target 에너테이션이 적용가능한 대상을 지정하는데 사용한다.
@Documented 에너테이션 정보가 javadoc 으로 작성된 문서에 포함되게 한다.
@Inherited 에너테이션이 자손 클래스에 상속되게 한다.
@Retention 에너테이션이 유지되는 범위를 지정하는데 사용한다.
@Repeatable 에너테이션을 반복해서 적용할 수 있게 한다.

@Override

@Override 에너테이션은 이 메서드가 조상의 메서드를 오버라이딩하는 것이라는걸 컴파일러에게 알리는 역할을 한다. 컴파일러는 @Override가 적용된 메서드의 시그니쳐와 동일한 메서드가 있는지 확인하고, 조상 클래스에 해당 메서드가 없는 경우에는 컴파일 에러를 낸다.

  • @Override는 컴파일러에게 알리는 역할만 하지 실질적으로 오버라이딩에 관여하지 않는다. 이 에너테이션이 없어도 오버라이딩은 정상적으로 동작한다.

@Deprecated

사용을 권고하지 않는 메서드 앞에 붙여 사용한다. 더 나은 기능을 하는 메서드가 생겼지만 기존 코드와 호환성 문제 때문에 기존의 코드를 없앨 수 없을때, @Deprecated 를 붙여 메서드 사용자에게 알린다. @Deprecated 를 붙인 메서드가 있다면 컴파일시 아래와 같은 메시지가 나타난다.

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

또한 ide에서는 deprecated 된 메서드에 취소선을 그어주는 등에 기능도 갖고있다.

메타에너테이션 중 @Target

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

package java.lang;

import java.lang.annotation.*;

/**
 * Indicates that a method declaration is intended to override a
 * method declaration in a supertype. If a method is annotated with
 * this annotation type compilers are required to generate an error
 * message unless at least one of the following conditions hold:
 *
 * <ul><li>
 * The method does override or implement a method declared in a
 * supertype.
 * </li><li>
 * The method has a signature that is override-equivalent to that of
 * any public method declared in {@linkplain Object}.
 * </li></ul>
 *
 * @author  Peter von der Ah&eacute;
 * @author  Joshua Bloch
 * @jls 8.4.8 Inheritance, Overriding, and Hiding
 * @jls 9.4.1 Inheritance and Overriding
 * @jls 9.6.4.4 @Override
 * @since 1.5
 */

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

METHOD 와 함께 올 수 있는 값은 FIELD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE 등이 있으며, 여러개를 사용할 때는 배열처럼 {} 안에 넣어줘야 한다.

@Retention

@Retention 은 에너테이션이 유지되는 기간을 지정하는데 사용한다.

위에서 볼 수 있는 RetentionPolicy.SOURCE 는 에너테이션이 소스파일에만 존재하고 클래스 파일에는 존재하지 않는 경우 넣어주는 유지 정책인데, @Override의 경우 컴파일 타임에 에러를 내는 기능을 하기 때문에 컴파일 전의 소스코드에만 에너테이션이 유지되고 컴파일 후의 .class파일에서는 에너테이션이 존재하지 않게 하기 위해 넣어준 것이다.

RetentionPolicy.SOURCE 외에 RetentionPolicy.CLASS ( 클래스 파일에 존재. 실행시에 사용불가. 기본값), RetentionPolicy.RUNTIME (클래스 파일에 존재, 실행시 사용가능) 등을 상황에 맞게 사용할 수 있다.

에너테이션 타입 정의법

우리가 사용하는, 사용하게 될 에너테이션은 아래와 같은 방식으로 구현되어 있다.


@Interface SomeAnnotation {
    //에너테이션 요소: 기본형,String,enum,에너테이션,Class가 올수 있으며
    //매개변수 선언이 불가하고, 예외를 선언할 수 없다.
    //type typeName();
    int int1();
    String value();
}

인터페이스앞에 @을 넣어서 선언하며, 반환값이 있고 매개변수가 없는 추상메서드의 형태를 가진 에너테이션 요소들을 가진다.

에너테이션을 적용할 때 이 요소들의 값을 지정해줘야 한다.

@SomeAnnotation(int1 = 8, value="someStringValue")
class SomeClass {
    ...
}

에너테이션 요소들에 기본값을 default 를 사용하여 부여할 수 있다.

@Interface SomeAnnotation {
    int int1() default 0;
    String value() default "hello";
}

위 형식에 맞추어 작성된 어노테이션을 부여한 코드들은,

아래와 같은 컨테이너 메서드를 통하여 어노테이션에 관한 로직들을 설정할 수 있다.
실제로 사용할 일은 거의 없고, 이런 구조에 의하여 동작한다는 것만 알아놓자.

import java.lang.reflect.Field;

public class MyContextContainer {

    public MyContextContainer(){}

    /**
     * 객체를 반환하기 전 어노테이션을 적용합니다.
     * @param instance
     * @param <T>
     * @return
     * @throws IllegalAccessException
     */
     //T는 주로 클래스가 될 것임
    private <T> T invokeAnnonations(T instance) throws IllegalAccessException {

        //클래스에 선언된 필드를 배열에 담는다. 클래스에는 어노테이션에 대한 정보도 포함되어 있음
        Field [] fields = instance.getClass().getDeclaredFields();
        for( Field field : fields ){
            //필드마다 SomeAnnotation 어노테이션의 정보를 가져온다.
            StringInjector annotation = field.getAnnotation(SomeAnnotation.class);

            //
            if( annotation != null ){
                field.setAccessible(true);
                //annotation.value() 는 에너테이션에서 value로 넣은 String 값이 반환됨
                field.set(instance, annotation.value());
            }
        }
        return instance;
    }
    ...


}

열거형 (Enums)

열거형은 JDK1.5에서 새로 추가된 기능으로 서로 관련된 상수를 편리하게 선언하기 위한 것이다.

class Card { //enums 를 사용하지 않은 기존의 코드
    static final int CLOVER = 0;
    static final int HEART = 1;
    static final int DIAMOND = 2;
    static final int SPADE = 3;

    static final int ONE = 0;
    static final int TWO = 1;
    static final int THREE = 2;
    static final int FOUR = 3;

    final int kind;
    final int num;
}
class Card {

    enum Kind { CLOVER, HEART, DIAMOND, SPADE }
    enum VALUE { ONE, TWO, THREE, FOUR }

    final Kind kind; //타입 명에 enum이 들어간다.
    final Value value;
}

Java Enums 사용의 장점

  1. 타입에 안전하다. (typesafe enum)
  • Java 의 Enum 은 값과 타입을 동시에 체크한다!
  • 기존의 코드에서는 CLOVER 변수와 ONE 변수를 비교하여 참이라는 값을 가져올 수 있었다. 프로그램적으론 둘다 0이라는 값을 갖고 있기 때문인데, 해당 변수들의 갖고 있는 의미상 FALSE를 반환하는 것이 타당할 것이다.
  • Java의 Enum은 타입이 다르면 컴파일 에러를 발생시켜 런타임에서 잘못된 비교식이 실행되지 않도록 한다.
  • c++ enum의 경우 타입은 체크하지 않는다.
if(Card.Kind.CLOVER == Card.Value.TWO) // 컴파일에러. 타입이 다름
  1. 상수의 값이 바뀌면, 해당 상수를 참조하는 모든 소스를 다시 컴파일해야하지만,
    열거형 상수를 사용하면, 기존의 소스를 다시 컴파일하지 않아도 된다.

Java Enums 특징

자동으로 값을 부여

enum Kind { CLOVER , HEART, DIAMOND, SPADE }

내부적으로 좌측부터 0부터의 숫자를 갖게 된다.
(CLOVER : 0 , HEART : 1, DIAMOND : 2, SPADE : 3)

열거형 상수간 비교법

  • '=='를 사용할 수 있다.
  • '<','>'은 사용할 수 없다.
  • compareTo는 사용가능하다
  • equals도 사용가능하다

    Kind k1 = Kind.CLOVER;
    Kind k2 = Kind.HEART;
    Kind k3 = Kind.CLOVER;

    System.out.println(k1==k2); //false
    System.out.println(k1.compareTo(k2)); // 오른쪽이 크므로 음수
    System.out.println(k1.equals(k3)); // true
    //System.out.println(k2 < k3) 컴파일 에러

이러한 비교법이 나오는 이유는 자바에서 enum의 상수들을 객체로 관리하기 때문이다.

열거형의 조상 클래스, java.lang.Enum

Enum 클래스에 있는 메서드는 다음과 같다.

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

또한 Enum 클래스의 메소드는 아니지만, 특이하게 컴파일러가 자동적으로 추가해주는 메서드도 있다.

메서드 설명
static E [] values() 열거형의 모든 상수를 배열에 담아 반환한다.
static valueOf(String name) 열거형 상수를 name으로 넣으면 그에 대한 참조값을 얻을 수 있다.
enum Kind { CLOVER , HEART, DIAMOND, SPADE }

class EnumApp {
    public static void main(String[] args) {
        Kind k1 = Kind.CLOVER;
        Kind k2 = Kind.valueOf("HEART"); // 컴파일러가 추가한 메서드
        Kind k3 = Enum.valueOf(Kind.class, "CLOVER"); //Enum의 메서드

        Kind [] kArr = Kind.values(); // 컴파일러가 추가한, 열거형의 모든 상수 배열에 담아 반환

        for(Kind k : kArr) {
            System.out.println(k.name()+"="+k.ordinal()); // 열거형 상수의 이름과 정의된 순서
        }
        /* for문 결과
        CLOVER=0
        HEART=1
        DIAMOND=2
        SPADE=3
        */

    }
}

열거형에 멤버 추가하기

열거형 내부적으로 0부터 차례로 올라가는 상수 값을 갖지만, 필요에 따라 이 상수값을 지정해줘야 할 경우 아래와 같은 방식으로 할 수 있다.


enum Kind {
    CLOVER(1), HEART(5) , DIAMOND(-1), SPADE(10); // 끝에 ';'를 추가해야한다.

    private final int value; // 정수를 저장할 공간을 추가
    Kind(int value) { this.value = value; } //생성자

    public int getValue() { return value; }
}

위에서도 설명했지만 Java의 Enum의 상수들은 하나 하나가 객체이기 때문에 생성자와 변수의 값을 저장할 변수를 작성하는것을 통하여 상수 값을 지정해줄 수 있다. 생성자를 통해 생성한 상수는 외부에서 값을 가져올 수 있게 하기 위하여 getter 역시 만들어 주어야 한다.

아래와 같은 방법으로 하나의 열거형 상수에 여러가지 값을 지정할 수도 있다.

enum Direction {
    EAST(1,">"), SOUTH(2,"V"), WEST(3,"<"), NORTH(4,"^");
    private final int value;
    private final String symbol;

    Direction(int value, String symbol) {
        this.value = value;
        this.symbol = symbol;
    }

    //생성된 상수의 형식에 따라 get메서드를 작성해 주어야 한다.
    public int getValue( return value; )
    public String getSymbol( return symbol;)
}

생성자와 메서드등을 enum 클래스 내부에서 만들수 있듯이, 다른 로직이 들어간 메서드 역시 구현할 수 있다.

'Computer Science > Java' 카테고리의 다른 글

Generic Programming  (2) 2024.04.10
상속이 안티패턴이라고?  (0) 2024.03.17
Java - 예외처리 정리  (2) 2022.05.16