세상에 나쁜 코드는 없다
[Java] Generics, Annotation, Enum 본문
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 한 클래스만 타입으로 지정할 수 있다.
...
}
지네릭스의 제한
지네릭스는 그 특성에 의하여 몇가지 제한 점이 있다.
- Static 멤버에 타입변수를 사용할 수 없다.
모든 객체에 대해 동일하게 동작해야하는 static 멤버는 객체에 따라 달라질 수 있는 타입변수를 사용할 수 없다.
class List {
static E item; // 에러
static int compare(E e1, E e2); //에러
}
- 지네릭 타입의 배열을 생성하는 것은 허용되지 않는다.
지네릭 배열을 생성할 수 없는 것은 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
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
?의 등장
이러한 문제를 해결하여 지네릭 타입과 상관없이 컬렉션을 받아서 출력하는 메소드를 만드려면 wildcard '?' 를 사용하면 된다. 위 메서드의
static void printCollection(Collection<?> c) { ... }
? 만 사용한다면 어떤 지네릭 타입이든간에 메서드의 파라미터로 가져와 사용할 수 있다.
만약 와일드카드로 들어오는 지네릭 타입을 제한하고 싶으면 지네릭타입의 제한 방법과 동일하게
extends, super을 사용하여 제한해 줄 수 있다.
static void printCollection(Collection<? extends SomeClass> c) { ... }
static void printCollection(Collection<? super SomeClass> c) { ... }
지네릭 타입의 제거
컴파일러는 컴파일 단계에서 지네릭 타입을 이용하여 소스파일을 체크하고 필요한 곳에 형변환을 넣어준 후 지네릭 타입을 제거한다. 이를 통해 컴파일된 파일에는 지네릭 타입에 대한 정보가 없게 된다.
- 지네릭 타입의 경계(bound)를 제거한다.
- 지네릭 타입이 라면 T는 SomeObject로 치환된다. 인 경우 T는 Object로 치환된다. 그 후 클래스 옆의 선언은 제거된다.
class SomeClass<T extends SomeObject> {
void someMethod(T t) { ... }
}
-->
class SomeClass {
void someMethod(SomeObject t) { ... }
}
- 지네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가한다.
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é
* @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 사용의 장점
- 타입에 안전하다. (typesafe enum)
- Java 의 Enum 은 값과 타입을 동시에 체크한다!
- 기존의 코드에서는 CLOVER 변수와 ONE 변수를 비교하여 참이라는 값을 가져올 수 있었다. 프로그램적으론 둘다 0이라는 값을 갖고 있기 때문인데, 해당 변수들의 갖고 있는 의미상 FALSE를 반환하는 것이 타당할 것이다.
- Java의 Enum은 타입이 다르면 컴파일 에러를 발생시켜 런타임에서 잘못된 비교식이 실행되지 않도록 한다.
- c++ enum의 경우 타입은 체크하지 않는다.
if(Card.Kind.CLOVER == Card.Value.TWO) // 컴파일에러. 타입이 다름
- 상수의 값이 바뀌면, 해당 상수를 참조하는 모든 소스를 다시 컴파일해야하지만,
열거형 상수를 사용하면, 기존의 소스를 다시 컴파일하지 않아도 된다.
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 |