본문 바로가기
개념

[JAVA의 정석] Chapter12

by cook_code 2024. 9. 8.
반응형

교재 목차

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

 

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


 

1. 지네릭스 (Generics)

1-1. 지네릭스란?

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

(c++의 템플릿 클래스와 유사한 개념)

지네릭스의 장점

  • 타입 안정성을 제공한다.
    • 의도하지 않은 타입의 객체가 저장되는 것을 막는다.
    • 저장된 객체를 꺼내올 때 다른 타입으로 잘못 형변환하여 발생할 수 있는 오류를 줄인다.
  • 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.

1-2. 지네릭스 선언

class Box<T>  { // 지네릭 타입 T를 선언. T는 타입변수
  T item;
  
  void setItem(T item)  {
    this.item = item;
  }
  
  T getItem() {
    return item;
  }
}

 

Box<T> : 지네릭 클래스, T의 box 또는 T box 라고 읽는다.

T : 타입 변수 또는 타입 매개변수 (T는 타입 문자)

Box : 원시 타입 (Row Type)

Box<String> b = new Box<String>();  // 타입 T대신 실제 타입 지정(대입된 타입, 매개변수화된 타입)
b.setItem(new Object());  // 에러. String 외의 타입은 지정 불가
b.setItem("ABC"); // OK. String 타입이므로 가능

 

지네릭스 제한

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

T는 인스턴스 변수로 간주되기 때문이다.

static 멤버는 인스턴스 변수를 참조할 수 없다.

지네릭스 제한에 대한 상세설명

지네릭 배열을 생성할 수 없는 것은 new 연산자 때문인데, 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다.

그런데 위 Box<T> 클래스를 컴파일하는 시점에서는 T가 어떤 타입이 될지 전혀 알 수 없다.

instanceof 연산자도 new 연산자와 같은 이유로 T를 피연산자로 사용할 수 없다.

 

1-3. 지네릭 클래스의 객체 생성과 사용

  • 참조변수와 생성자에 대입된 타입이 일치해야 한다.
  • 추정이 가능한 경우에는 타입 생략이 가능하다.
public class CoffeeMachine {

    public <T> Coffee makeCoffee(T capsule) {

        return new Coffee(capsule);
    }
}

CoffeeMachine coffeeMachine = new CoffeeMachine();
Colombian capsule = new Colombian();
coffeeMachine.<Colombian>makeCoffee(capsule);
coffeeMachine.makeCoffee(capsule);  // 타입 추정 가능하므로 생략 가능

 

1-4. 제한된 지네릭 클래스

지네릭 사용 시 타입 지정을 하지 않으면 아무 타입이나 모두 받을 수 있기 때문에 자바에서는 extends, super 키워드를 통해 이를 문법적으로 제한할 수 있도록 한다.

class BoxMaterial { }
class Paper { }
class Plastic { }

public class Box<M> {

    private M material;

    public static void main(String[] args) {

        Box<Water> box = new Box<>();   // 타입 M에 제한이 없으므로 어떤 타입이든 지정 가능
    }
}

class BoxMaterial { }
class Paper extends BoxMaterial { }
class Plastic extends BoxMaterial { }

public class Box<M extends BoxMaterial> {

    private M material;

    public static void main(String[] args) {

        Box<Water> waterBox = new Box<>();   // 불가. BoxMaterial과 자식 타입만 가능
        Box<Paper> paperBox = new Box<>();
        Box<Plastic> plasticBox = new Box<>();
    }
}

class BoxMaterial implements Hard { }
class Paper extends BoxMaterial { }
class Plastic extends BoxMaterial { }
public interface Hard { }

public class Box<M extends BoxMaterial & Hard> {

    private M material;

    public static void main(String[] args) {

        Box<Paper> paperBox = new Box<>();
        Box<Plastic> plasticBox = new Box<>();
    }
}

class BoxMaterial {

    public String printInfo() {
        return "";
    }
}

public class Paper extends BoxMaterial {

    @Override
    public String printInfo() {
        return "종이";
    }
}

public class Plastic extends BoxMaterial {

    @Override
    public String printInfo() {
        return "플라스틱";
    }
}

public class Box<M extends BoxMaterial> {

    private M material;

    public Box(M material) {
        this.material = material;
    }

    public void printInfo() {
        System.out.println(material.printInfo() + " 상자입니다.");
    }

    public static void main(String[] args) {

        Box<Paper> paperBox = new Box<>(new Paper());
        paperBox.printInfo();
        Box<Plastic> plasticBox = new Box<>(new Plastic());
        plasticBox.printInfo();
    }
}

 

1-5. 와일드 카드

와일드 카드를 왜 쓸까?

- 선행개념(공변/반공변/무공변)

  • 공변 : S 가 T 의 하위 타입이면,
    • S[] 는 T[] 의 하위 타입이다.
    • List<S> 는 List<T> 의 하위 타입이다.
  • 반공변 : S 가 T의 하위 타입이면,
    • T[] 는 S[] 의 하위 타입이다. (공변의 반대)
    • List<T> 는 List<S> 의 하위 타입이다. (공변의 반대)
  • 무공변 / 불공변 : S 와 T 는 서로 관계가 없다.
    • List<S> 와 List<T> 는 서로 다른 타입이다.

제네릭은 공변성이 없다.

대표적으로 컬렉션 프레임워크 Collection과 그의 하위인 ArrayList는 서로 조상-자손 상속 관계에 있기 때문에 캐스팅이 가능하다.

그러나 제네릭에는 이러한 상하관계가 없다. 제네릭은 전달받은 매개타입 그 타입으로만 서로 캐스팅이 가능하다.

제네릭의 공변성, 이 부분이 왜 문제가 될까?

public static void print(List<Object> arr) {
    for (Object e : arr) {
        System.out.println(e);
    }
}

public static void main(String[] args) {
    List<Integer> integers = Arrays.asList(1, 2, 3);
    print(integers); // ! Error
}

=> 리스트 제네릭의 경우, print 메서드의 매개변수로 아규먼트가 넘어갈 때, 
Integer 타입이 자연스럽게 업캐스팅되지 않아 에러가 난다.
1. 외부로부터 값을 받는 매개변수의 제네릭 타입 파라미터를 Interger 로 고정된 타입으로 작성하거나 
2. Integer의 상위 타입까지 받으려면 매서드를 오버로딩해야 하는 번거로움이 있다.

이러한 방식은 객체지향적이지 못하므로, 이 문제를 해결하기 위해 와일드 카드 기능이 나오게 되었다.

 

 

☕ 자바 제네릭의 공변성 & 와일드카드 완벽 이해

자바의 공변성 / 반공변성 제네릭의 와일드카드를 배우기 앞서 선수 지식으로 알고 넘어가야할 개념이 있다. 조금 난이도 있는 프로그래밍 부분을 학습 하다보면 한번쯤은 들어볼수 있는 공변

inpa.tistory.com

와일드카드 종류

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

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

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

와일드카드
네이밍
설명
<?>
Unbounded wildcards
비한정적 와일드 카드
제한 없음 (모든 타입이 가능)
<? extends U>
Upper Bounded Wildcards
상한 경계 와일드카드
상위 클래스 제한 (U와 그 자손들만 가능)
상한이 U라 상한 경계라고 한다.
<? super U>
Lower Bounded Wildcards
하한 경계 와일드카드
하위 클래스 제한 (U와 그 조상들만 가능)
하한이 U라 하한 경계라고 한다.

와일드카드 제약

상속 관계 예시

// 타입 계층 관계
class Food {
}

class Fruit extends Food {
}

class Vegetable extends Food {
}

class Apple extends Fruit {
}

class Banana extends Fruit {
}

class Carrot extends Vegetable {
}
출처: https://inpa.tistory.com/entry/JAVA-☕-제네릭-와일드-카드-extends-super-T-완벽-이해 [Inpa Dev 👨‍💻:티스토리]

 

상한 경계 <? extends U>

  • 타입 매개변수의 범위는 U 클래스이거나, U를 상속한 하위 클래스 (U와 U의 자손 타입만 가능)
  • 최대 U 이하
Box<? extends Fruit> box1 = new Box<Fruit>();
Box<? extends Fruit> box2 = new Box<Apple>();
Box<? extends Fruit> box3 = new Box<Banana>();

 

하한 경계 <? super U>

  • 타입 매개변수의 범위는 U 클래스이거나, U를 상속한 상위 클래스 (U와 U의 조상 타입만 가능)
  • 최소 U 이상
Box<? super Fruit> box1 = new Box<Fruit>();
Box<? super Fruit> box2 = new Box<Food>();
Box<? super Fruit> box3 = new Box<Object>();

 

비 경계 <?>

  • 타입 매개변수의 범위에 제한이 없다. (모두 가능)
  • <? extends Object>의 줄임 표현
Box<?> box1 = new Box<Vegetable>();
Box<?> box2 = new Box<Fruit>();
Box<?> box3 = new Box<Food>();
Box<?> box3 = new Box<Carrot>();

 

1-6. 지네릭 메서드

메서드 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라고 한다.

(= 배열의 타입을 지정하듯, 리스트 자료형 같은 컬렉션 클래스나 메소드에서 사용할 내부 데이터 타입을 파라미터 주듯이 외부에서 지정하는 타입을 변수화 한 기능)

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

ArrayList<String> list = new ArrayList<>();

 

제네릭 클래스 생성

class FruitBox<T> {
    List<T> fruits = new ArrayList<>();

    public void add(T fruit) {
        fruits.add(fruit);
    }
}

 

제네릭 클래스 인스턴스화

// 제네릭 타입 매개변수에 정수 타입을 할당
FruitBox<Integer> intBox = new FruitBox<>(); 

// 제네릭 타입 매개변수에 실수 타입을 할당
FruitBox<Double> intBox = new FruitBox<>(); 

// 제네릭 타입 매개변수에 문자열 타입을 할당
FruitBox<String> intBox = new FruitBox<>(); 

// 클래스도 넣어줄 수 있다. (Apple 클래스가 있다고 가정)
FruitBox<Apple> intBox = new FruitBox<Apple>();

 

1-7. 지네릭 타입의 형변환

지네릭 타입과 원시 타입 간의 형변환이 가능할까?

=> 결론: 바람직하지 않지만 가능은 하다.

와일드 카드가 선언된 제네릭 타입으로는 형변환이 가능하다.

대입된 타입이 다른 지네릭 타입 간에 형변환이 가능할까?

=> 결론: 불가능하다.

1-8. 지네릭 타입의 제거

제거 순서

⑴ 지네릭 타입을 이용해 소스파일 체크

⑵ 필요한 곳에 형변환을 넣어준다.

⑶ 지네릭 타입의 경계(bound)를 제거한다.

⑷ 지네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가한다.

 

2. 열거형 (Enums)

2-1. 열거형이란?

열거형은 서로 관련된 상수를 편리하게 선언하기 위한 것으로 여러 상수를 정의할 때 사용하면 편리하다. 값 뿐만 아니라 타입도 관리하기 때문에 보다 논리적인 오류를 줄일 수 있다.

상수의 값이 바뀌면 해당 상수를 참조하는 모든 소스를 다시 컴파일 해야하지만 열거형으로 선언된 상수를 사용하면 기존의 소스를 다시 컴파일하지 않아도 된다.

열거형 상수에 비교연산자는 사용이 불가하지만 compareTo()를 사용해 값의 비교가 가능하다.

2-2. 열거형의 정의와 사용

열거형 메서드

메서드
설명
Class<E> getDeclaringClass()
열거형의 Class 객체를 리턴한다.
String name()
열거형 상수의 이름을 문자열로 리턴
int ordinal() (웬만하면 사용 X)
열거형 상수가 정의된 순서를 리턴 (0부터 시작)
T valueOf(Class<T> enumType, String name)
T valueOf(String name)
[지정된 열거형에서] name 과 일치하는 열거형 상수를 리턴
T[] values()
열거형 상수들을 배열 형태로 리턴

정의

enum Direction {  EAST, WEST, SOUTH, NORTH  }

 

구현

enum Direction {

    static final Direction EAST = new Direction("EAST");
    static final Direction WEST = new Direction("WEST");
    static final Direction SOUTH = new Direction("SOUTH");
    static final Direction NORTH = new Direction("NORTH");
  
    private String name;
  
    private Direction(String name)  { this.name = name; }
}

 

열거형에 멤버 추가하기

enum Direction {  EAST(1), WEST(5), SOUTH(-1), NORTH(10)  }

 

열거형에 추상메서드 추가하기

열거형에 추상 메서드를 선언하면 각 열거형 상수가 이 추상 메서드를 반드시 구현해야 한다.

상수는 protected로 접근 제한자를 사용해야 각 상수에서 접근이 가능하다.

public enum Transportation {
	BUS(100) { int fare(int distance) { return distance * BASIC_FARE; } },
	TRAIN(150) { int fare(int distance) { return distance * BASIC_FARE; } },
	SHIP(200) { int fare(int distance) { return distance * BASIC_FARE; } },
	AIRPLANE(300) { int fare(int distance) { return distance * BASIC_FARE; } };

	protected final int BASIC_FARE; // 반드시 protected

	Transportation(int basicFare) {
		BASIC_FARE = basicFare;
	}

	public int getBasicFare() { return BASIC_FARE; };
    
	// 반드시 abstract로 설정 
	// 거리에 따른 요금 계산
	abstract int fare(int distance);
}

 

 

3. 애너테이션 (Annotation)

3-1. 애너테이션이란?

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

3-2. 표준 애너테이션

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

3-3. 메타 애너테이션

애너테이션을 위한 애너테이션, 애너테이션의 적용대상이나 유지기간 등을 지정하는데 쓰인다.

애너테이션 규칙

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

'개념' 카테고리의 다른 글

[JAVA의 정석] Chapter14  (2) 2024.09.08
[JAVA의 정석] Chapter13  (1) 2024.09.08
[JAVA의 정석] Chapter11  (1) 2024.09.08
[JAVA의 정석] Chapter10  (0) 2024.09.08
[JAVA의 정석] Chapter08 - 09  (0) 2024.09.08