[Java] Generics, Enums, Annotation

허경두·2025년 4월 10일

Java

목록 보기
10/12

Generics

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

장점

  • 컴파일 시점에 타입 안정성을 보장한다
  • 타입체크와 형변환을 생략할 수 있어 코드가 간결해 진다

Generic 클래스의 선언과 사용

class Box<T> {
    private T item;
    public void set(T item) { this.item = item; }
    public T get() { return item; }
}

class FruitBox<T extends Fruit & Eatable> { // 타입을 Fruit의 자손과 Eatable의 구현체로 제한할 수도 있다
	// 멤버
}

class Main {
	public static void main(String[] args) {
    	Box<Apple> appleBox = new Box<Apple>();
        Box<Grape> grapeBox = new Box<>(); // 변수 타입과 생성자 타입은 일치해야 하므로 생성자에서 생략할 수 있다
    }
}
  • Java는 다중 상속이 안되기 때문에 제한된 Generics에서 <T extends Class1 & Class2> 는 허용되지 않는다. <T extends Class & Interface1 & Interface2> 는 가능하다

Generics의 제한

  1. static 멤버에는 타입 변수 T를 사용할 수 없다
    static은 인스턴스에 관계없이 동일하게 동작해야 하므로 타입에 따라 달라지면 안된다
  2. new 연산자와 사용할 수 없다
    T[] arr = new T[100]; // X
    new는 컴파일 시점에 타입이 무엇인지 정확히 알아야 하는데 타입 변수 T는 런타임에 결정되므로 사용할 수 없다
    • 대안 1 : Reflections API 의 newInstance()와 같이 동적으로 객체를 생성하는 메서드를 사용한다
    • 대안 2 : Object 타입의 객체를 생성하여 복사한 후 T로 형변환한다

와일드 카드

와일드 카드(Wild Card) : Generic 타입을 다룰 때 정확한 타입 지정 없이 사용하고 싶을 때 유연하게 타입을 받을 수 있게 하는 문법

변수나 반환값의 타입에도 사용할 수 있지만 추천되지 않고 주로 매개변수의 타입에 사용한다
왜?
1. 타입이 명확하지 않기 때문에 변수를 읽기만 가능하고 쓰기가 거의 불가능하다
2. 리턴 받은 쪽에서 타입을 모르기 때문에 바로 사용할 수가 없다 → 실용성과 유지보수성이 떨어진다

Generic 타입만 바꿔서 오버로딩하는 것은 불가능(컴파일 에러)하기 때문에 다음과 같이 사용한다
→ Generic 타입은 컴파일 후에는 없어지기 때문에 동일한 메서드가 2개 존재하게 된다

void printList(List<?> list) { // 요소가 어떤 타입이든 출력 가능한 메서드
    for (Object obj : list) {
        System.out.println(obj);
    }
}

제한된 와일드 카드

구문의미읽기 (get)쓰기 (add)사용 목적예시
<? extends T>T와 그 하위 타입만 허용✅ 가능❌ 불가능데이터를 읽기만 할 때List<? extends Number>
<? super T>T와 그 상위 타입만 허용⚠️ 제한적✅ 가능데이터를 쓰기만 할 때List<? super Integer>
<?>모든 타입 허용✅ 가능❌ 불가능타입 모를 때 읽기 전용용List<?>
  • extends
    • 읽기 : 하위 타입만 가능하므로 T 타입으로 받으면 요소가 하위 타입이어도 할당이 가능하다
    • 쓰기 : 예를 들어 Fruit > Apple > GreenApple 일때 타입이 GreeApple 이면 Apple이나 Fruit 타입은 쓰지 못한다
  • super
    • 읽기 : 예를 들어 T가 Integer 일 경우, NumberObject 타입은 할당하지 못할 수도 있다
    • 쓰기 : ? 가 어떤 타입이든 간에 T 보다는 항상 상위 타입이기 때문에 T 타입의 변수는 항상 쓰기를 진행할 수 있다

Generic Method

Generic Method : 선언부에 Generic 타입이 선언된 메서드

클래스 전체를 Generic으로 만들지 않고 특정 메서드만 유연하게 만들고 싶을 때 사용한다 (Generic 클래스에서도 사용할 수 있다)

public <T> void print(T item) {
    System.out.println(item);
}

와일드 카드와 Generic 메서드의 차이

와일드 카드는 매개변수의 타입을 모르기 때문에 읽기나 쓰기가 제한될 수 있지만
Generic 메서드는 메서드 호출 시 타입을 대입해야 하기 때문에 타입에 맞게 읽기나 쓰기를 할 수 있다

Generic 타입의 형변환

Generic → Raw : 가능. 경고 발생
Raw → Generic : 가능. 경고 발생
Generic 자손 → Generic 조상 : 에러
Generic 조상 → Generic 자손 : 에러
Generic 자손 or 자기 자신 → 와일드 카드 : 가능
와일드 카드 → Generic 자손 or 자기 자신 : 가능. 경고 발생
와일드 카드 → 와일드 카드 : 가능. 경고 발생

컴파일러의 Generic 제거

  1. 경계(bound) 제거

    class Box<T extends Fruit> {
    	void add(T t) {...}
     }
    // →
    class Box {
    	void add(Fruit t) {...}
    }
  2. 타입이 다르면 형변환 추가

    List list; // List.get()은 Object 타입을 반환한다
    
    T get(int i) {
    	return list.get(i);
     }
     // →
     Fruit get(int i) {
     	return (Fruit)list.get(i);
     }

Enums

열거형(enums) : 서로 관련된 상수들을 하나의 타입으로 묶어서 정의

따로 클래스를 생성해도 되고 클래스 내부에서 정의해도 된다
클래스 내부에서 정의할 경우 static인 내부 클래스이다

  • 사용하는 상황

    상황enum을 쓰는 게 좋을까?이유
    상수들이 명확히 정해져 있고, 의미 있는 이름이 있으며✅ Yes타입 안정성, 가독성, 유지보수 용이
    해당 상수들이 특정 클래스에서만 쓰이는 단순한 경우❌ Maybe Not구조가 단순해서 enum 없이도 충분
    나중에 조건 분기데이터 추가 가능성이 있음✅ Definitelyenum 구조가 더 유연하고 확장에 적합

제한

  1. 열거형 상수에 == 비교 연산자는 사용 가능하지만,
    >, < 는 사용 할 수 없다 → compareTo() 로 비교할 수 있다
  2. 열거형의 생성자는 외부에서 호출할 수 없다
Direction d = new Direction(1); // 불가

열거형의 정의와 사용

public enum Day { // 정의
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

Day d = Day.MONDAY; // 사용

java.lang.Enum

모든 열거형의 조상은 java.lang.Enum 이다

메서드반환 타입설명
name()String열거 상수의 이름을 반환 (선언된 그대로)
ordinal()int열거 상수가 정의된 순서(0부터 시작)를 반환
compareTo(E o)int열거 상수 간 비교 (ordinal 값으로 비교)
equals(Object other)boolean두 열거 상수의 동일성 비교 (==와 동일)
hashCode()int열거 상수의 해시 코드 반환
toString()String보통 name()과 동일 (필요시 오버라이드 가능)
getDeclaringClass()Class<E>열거 상수가 정의된 enum 클래스 반환
valueOf(Class<T> enumType, String name)T문자열 이름으로 열거 상수 반환 (정적 메서드)

열거형에 멤버 추가하기

Enum 클래스에 정의된 ordinal() 메서드로 정의된 순서를 받아 사용할 수 있지만, 내부적인 용도로만 사용되기 위한 값이므로 다른 value가 필요하다면 멤버를 추가해서 사용하는 것이 좋다

public enum Fruit {
    APPLE("red", 95),
    BANANA("yellow", 105),
    ORANGE("orange", 80);

    private final String color; // final이 필수는 아니지만 상수를 저장하기 위한 타입이므로 final을 붙인다
    private final int calories;

    Fruit(String color, int calories) { 
        this.color = color;
        this.calories = calories;
    }

    public String getColor() {
        return color;
    }

    public int getCalories() {
        return calories;
    }
}

열거형의 추상 메서드

각 상수마다 다른 기능이 필요할 때 추상 메서드를 선언하고 구현할 수 있다

추상 메서드를 선언하고 상수 뒤 블럭에서 구현한다

내부에서 각각 enum 타입을 상속한 익명 내부 클래스 안에 구현부가 있다

public enum Operation {
    PLUS {
        @Override
        public int apply(int x, int y) {
            return x + y;
        }
    },
    MINUS {
        @Override
        public int apply(int x, int y) {
            return x - y;
        }
    },
    MULTIPLY {
        @Override
        public int apply(int x, int y) {
            return x * y;
        }
    },
    DIVIDE {
        @Override
        public int apply(int x, int y) {
            return y != 0 ? x / y : 0;
        }
    };

    // 추상 메서드 선언
    public abstract int apply(int x, int y);
}

Enum 클래스 내부와 컴파일 후의 열거형 코드

  • Enum 클래스
package java.lang;

import java.io.Serializable;
import java.lang.Comparable;

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {

    private final String name;     // 상수의 이름
    private final int ordinal;     // 상수가 선언된 순서 (0부터 시작)

    /**
     * enum의 생성자
     * - enum 선언 내부에서만 호출 가능
     */
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

    public final String name() {
        return name;
    }

    public final int ordinal() {
        return ordinal;
    }
    
    // ...
}
  • 컴파일 후 코드
    • 아래와 같이 열거형은 상수 배열이 아니라 각각의 멤버 변수로 초기화된다
    • 열거형의 생성자는 private 이기 때문에 프로그램 전체에서 하나의 인스턴스만 생성해서 공유한다 (singleton 객체)
public final class Fruit extends Enum<Fruit> {
    public static final Fruit APPLE = new Fruit("APPLE", 0, "red", 95);
    public static final Fruit BANANA = new Fruit("BANANA", 1, "yellow", 105);

    private final String color;
    private final int calories;

    private Fruit(String name, int ordinal, String color, int calories) {
        super(name, ordinal); // Enum 클래스가 제공
        this.color = color;
        this.calories = calories;
    }

    // ...
}

열거형을 이용한 Singleton 패턴 구현

  • 전통적인 싱글톤 패턴의 문제점

    • 리플렉션을 이용해 private 생성자를 호출할 수 있다
      * 리플렉션(Reflection) : 런타임에 클래스 정보에 접근하고 조작할 수 있게 해주는 도구

      import java.lang.reflect.Constructor;
      
      public class Main {
          public static void main(String[] args) throws Exception {
              Constructor<MyClass> constructor = MyClass.class.getDeclaredConstructor();
              constructor.setAccessible(true); // private 무시
              MyClass obj = constructor.newInstance(); // ✅ 생성됨!
          }
      }
      
    • 직렬화/역직렬화를 잘못 처리하면 새로운 인스턴스가 생길 수 있다
      * 직렬화/역직렬화(Serialization/Deserialization) : 객체를 바이트(byte) 형태로 변환해서 파일, 네트워크 등으로 저장하거나 전송할 수 있도록 만드는 과정. 역직렬화는 반대의 과정

public class MySingleton {
    private static final MySingleton instance = new MySingleton();

    private MySingleton() {}

    public static MySingleton getInstance() {
        return instance;
    }
  }
  • 열거형을 이용한 구현
    • JVM이 ENUM을 특별 처리하기 때문에 직렬화에 안전하다 (readResolve 불필요)
    • ENUM은 리플렉션으로 깨기 어렵다
    • 코드가 간결하다
    • 쓰레드 세이프 : 동기화 처리를 직접 하지 않아도 된다
public enum MySingleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("싱글턴 동작 중!");
    }
}

// main
MySingleton singleton = MySingleton.INSTANCE;
singleton.doSomething();

Annotation

annotation : 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것 (메타 데이터를 추가하는 것)

컴파일러, 프레임워크, 런타임 환경 등이 이 정보를 보고 자동으로 어떤 동작을 수행한다

표준 Annotation

어노테이션패키지적용 대상설명
@Overridejava.lang메서드부모 클래스 메서드 재정의 시 사용
@Deprecatedjava.lang클래스, 메서드 등더 이상 사용하지 말라는 경고 표시
@SuppressWarningsjava.lang클래스, 메서드 등특정 컴파일 경고 무시
@SafeVarargsjava.lang메서드, 생성자제네릭 가변인자 사용 시 안전함 명시
@FunctionalInterfacejava.lang인터페이스함수형 인터페이스임을 명시
@Retentionjava.lang.annotation어노테이션 정의용어노테이션 유지 시점 설정 (SOURCE, CLASS, RUNTIME)
@Targetjava.lang.annotation어노테이션 정의용어노테이션 적용 가능한 위치 지정
@Documentedjava.lang.annotation어노테이션 정의용Javadoc에 어노테이션 포함 여부 설정
@Inheritedjava.lang.annotation어노테이션 정의용하위 클래스에 자동 상속 여부 설정
@Repeatablejava.lang.annotation어노테이션 정의용같은 어노테이션을 여러 번 적용 가능하게 함

Annotation 정의하기

@을 제외하면 인터페이스 정의와 동일하다

각 속성은 매개변수는 없고 반환값이 있는 추상 메서드의 형태(getter와 같은)를 가지며 상속을 통해 구현하지 않아도 된다

  • 규칙
    1. 상수는 정의할 수 있지만 디폴트 메서드는 정의할 수 없다
    2. 기본값이 없는 속성들은 반드시 지정해줘야 한다
    3. 상속이 허용되지 않는다 (모든 Annotation의 조상은 java.lang.annotation.Annotation 이다)
    4. 속성의 타입은 기본형, String, enum, annotation, Class
    5. 매개변수 X, 예외 X, 타입 매개변수(Generic) X
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

// 어노테이션 정의
@Retention(RetentionPolicy.RUNTIME) // 런타임에도 유지됨
@Target(ElementType.METHOD)         // 메서드에만 적용 가능
public @interface MyAnnotation {
    String value();   // 속성 (필수)
    int count() default 1; // 속성 (기본값 있음)
}

// 어노테이션 사용
@MyAnnotation(value = "Hello")
    public void sayHello() {
        System.out.println("Hello method");
    }

Marker Annotation

속성이 하나도 정의되지 않은 annotation

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

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

0개의 댓글