Java/Java의 정석

Chapter 12 지네릭스, 열거형, 에너테이션

계란💕 2022. 3. 2. 23:42

1. 지네릭스(Generics)

  1.1 지네릭스란?

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

  - 자료형을 제한해주는 기능이다.

  - 타입 안정성이 높아지고 형변환의 번거로움이 줄어들어 간결해진다.

  - 의도하지 않은 타입의 객체가 저장되는 것을 막는다.

                                                                                                 

  1.2 지네릭 클래스의 선언

  - 클래스메서드에 선언할 수 있다. 

  Ex) 지네릭 클래스 Box가 선언되어 있을 때

  - Box<T>: 지네릭 클래스, 'T의  Box' 또는 'T Box'라고 있는다.

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

  - Box: 원시 타입(raw type)

 

  Note) 지네릭스의 제한

  - 지네릭 타입의 배열을 생성할 수 없다.

  - static 멤버에 타입 변수 T를 사용할 수 없다.

 

  1.3 지네릭 클래스의 객체 생성과 사용

  Ex)

<hide/>
import java.util.ArrayList;

class Fruit					{public String toString()  {return "Fruit";}}
class Apple extends Fruit	{public String toString()  {return "Apple";}}
class Grape extends Fruit	{public String toString()  {return "Grape";}}
class Toy					{public String toString()  {return "Toy";}}

public class ArrayListEx1 {
	public static void main(String[] args) {
		Box<Fruit> fruitBox = new Box<Fruit>();
		Box<Apple> appleBox = new Box<Apple>();
		Box<Toy> toyBox = new Box<Toy>();
//		Box<Grape> grapeBox = new Box<Grape>();	// 에러 타입 불일치
		fruitBox.add(new Fruit());
		fruitBox.add(new Apple());	// OK. void add(Fruit item)	
		appleBox.add(new Apple());
		appleBox.add(new Apple());
//		appleBox.add(new Toy());	// 에러 사과만 넣을 수 있다.
		toyBox.add(new Toy());
//		toyBox.add(new Apple());	//에러 토이박스에는 사과를 담을 수 없다.
		System.out.println(fruitBox);
		System.out.println(appleBox);
		System.out.println(toyBox);
	}
}
class Box<T>{
	ArrayList<T> list = new ArrayList<T>();
	void add(T item){ 
    	list.add(item);
    }
	T get(int i){ 
    	return list.get(i); 
    }
	int size(){  
    	return list.size();
     }
	public String toString() { 
    	return list.toString();
    }
}

  Note) 실행결과

  - appleBox.add(new toy()); 에러 -> Box<Apple>에는 Apple만 담을 수 있다.

  - Grape, Apple은 Fruit의 자손이므로 상속 관계에 있다. 

    -> fruitBox에 Apple을 추가할 수 있다.

 

   1.4 제한된 지네릭 클래스

  - 지네릭 타입에 'extends'를 사용하면 특정 타입의 자손들만 대입할 수 있도록 제한한다.

  - 타입 매개 변수 T에 Object를 대입하면 모든 종류의 객체를 저장 가능하다.

  Ex)

<hide/>
import java.util.ArrayList;
class Fruit implements Eatable{
	public String toString() {	return "Fruit";}
}
class Apple extends Fruit {public String toString()  {return "Apple";}}
class Grape extends Fruit {public String toString()  {return "Grape";}}
class Toy					{public String toString()  {return "Toy";}}

interface Eatable {}

public class FruitBoxEx2 {
	public static void main(String[] args) {
		FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
		FruitBox<Apple> appleBox = new FruitBox<Apple>();
		FruitBox<Grape> grapeBox = new FruitBox<Grape>();	
//		FruitBox<Toy> toyBox = new FruitBox<Toy>(); 에러
		
		fruitBox.add(new Fruit());
		fruitBox.add(new Apple());	// OK. void add(Fruit item)	
		fruitBox.add(new Grape());
		appleBox.add(new Apple());
//		appleBox.add(new Grape()); 에러
		grapeBox.add(new Grape());		
		
		System.out.println( "fruitBox-"+  fruitBox);
		System.out.println( "appleBox-" + appleBox);
		System.out.println("grapeBox"+  grapeBox);
	}
}
class FruitBox<T extends Fruit & Eatable>	extends Box<T> {}

class Box<T>{
	ArrayList<T> list = new ArrayList<T>();
	void add(T item)	{ list.add(item); }
	T get (int i) 		{ return list.get(i); }
	int size() 			{  return list.size(); }
	public String toString() { return list.toString();}
}

  Note) 실행결과

  - implements가 아닌 'extends'를 이용해서 Eatable인터페이스를 구현한다. 

 

  1.5 와일드카드

<? extends T> : 와일드 카드의 상한 제한. T와 그 자손들만 가능
<? super T> : 와일드 카드의 하한 제한. T와 그 조상들만 가능
<?> : 제한 없음. 모든 타입이 가능하다.  < ? extends Object >와 동일하다.

   

  Ex)

<hide/>
import java.util.*;
class Fruit {
	String name;
	int weight;
	Fruit(String name, int weight){
		this.name = name;
		this.weight = weight;
	}
	public String toString() {return name + "(" + weight + ")" ; }
}
class Apple extends Fruit{	
	Apple(String name, int weight){
		super(name, weight);
	}
}
class Grape extends Fruit{	
	Grape(String name, int weight){
		super(name, weight);
	}
}
class AppleComp implements Comparator<Apple>{
	public int compare(Apple t1, Apple t2) {
		return t2.weight - t1.weight;	
	}
}
class GrapeComp implements Comparator<Grape>{
	public int compare(Grape t1, Grape t2) {
		return t2.weight - t1.weight;	
	}
}
class FruitComp implements Comparator<Fruit>{
	public int compare(Fruit t1,Fruit t2) {
		return t1.weight - t2.weight;	
	}
}
public class FruitBoxEx2 {
	public static void main(String[] args) {
		FruitBox<Apple> appleBox = new FruitBox<Apple>();
		FruitBox<Grape> grapeBox = new FruitBox<Grape>();

		appleBox.add(new Apple("GreenApple", 300));
		appleBox.add(new Apple("GreenApple", 100));
		appleBox.add(new Apple("GreenApple", 200));
		
		grapeBox.add(new Grape("GreenGrape", 400));
		grapeBox.add(new Grape("GreenGrape", 300));
		grapeBox.add(new Grape("GreenGrape", 200));
		
		Collections.sort(appleBox.getList(), new AppleComp());
		Collections.sort(grapeBox.getList(), new GrapeComp());
		System.out.println(appleBox);
		System.out.println(grapeBox);
		System.out.println();
		Collections.sort(appleBox.getList(), new FruitComp());
		Collections.sort(grapeBox.getList(), new FruitComp());
		System.out.println(appleBox);
		System.out.println(grapeBox);		
	}
}
class FruitBox<T extends Fruit> extends Box<T>{}

class Box<T>{
	ArrayList<T> list = new ArrayList<T>();
	void add(T item) {
		list.add(item);
	}
	T get(int i) {
		return list.get(i);
	}
	ArrayList<T> getList() { return list;}
	int size() {
		return list.size();
	}
	public String toString(){
		return list.toString();
		
	}
}

  Note) 실행결과

  - Collections.sort()를 이용해서 appleBox와 grapeBox에 담긴 과일을 무게 별로 정렬한다.

  - Comparator<? super apple>: Comparator의 타입 매개변수로 Apple과 그 조상이 가능하다는 뜻.

 

  1.6 지네릭 메서드

  - 지네릭 타입이 선언된 메서드(타입 변수는 메서드 내에서만 유효)

    -> 반환 타입 바로 앞에 지네릭 타입을 선언한다.

  - 클래스의 타입 매개변수 <T>와 메서드의 타입 매개변수 <T>는 별개

  - 메서드를 호출할 때마다 타입을 대입해야 (대부분 생략 가능)

  - 메서드를 호출할 때, 타입을 생략하지 않을 때는 클래스 이름 생략 불가

  1) 타입 T를 요소로 하는 List를 매개변수로 허용한다.

  2) 'T'는 Comparable을 구현한 클래스여야 한다.( <T extends Comparable> )

    -> 'T'또는 그 조상의 타입을 비교하는 Comparable이어야한다는 것( Comparable<? super T>)를 의미

    -> T가 Student이고 Person의 자손이라면 <? super T>는 Student, Person, Object가 모두 가능하다.

 

  1.7 지네릭 타입의 형변환

  - 지네릭 타입과 원시 타입(raw type) 간의 형변환은 바람직하지 않다. (경고 발생)

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

  - 와일드 카드가 사용된 지네릭 타입끼리도 형변환 가능한 경우가 있다. (경고 발생)

 

  1.8 지네릭 타입의 제거

  - 컴파일러는 지네릭 타입을 제거하고 필요한 곳에 형변환을 넣는다.

  - 제거 과정

  1) 지네릭 타입의 경계(Bound)를 제거

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

    -> 와일드 카드가 포함된 경우, 적절한 타입으로 형변환 추가한다. 

 

2. 열거형(enums)

  2.1 열거형이란?

  Def) 열거형: 관련된 상수들을 같이 묶어 놓은 것. Java는 타입에 안전한 열거형을 제공한다.

  

  2.2 열거형의 정의와 사용

  - 열거형 정의: enum 열거형이름 { 상수명1, 상수명2, ... }

  - 열거형 타입의 변수를 선언하고 사용하는 방법

  - 열거형 상수의 비교에 '==' 와 compareTo() 사용 가능하다. (부등호는 사용불가)

    -> compareTo() : 같으면 0 / 왼쪽이 크면 (+) / 오른쪽이 크면 (-) 반환

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

    -> values(), valueOf()는 컴파일러가 자동으로 추가

    -> values():열거형의 모든 상수를 배열에 담아 반환한다. 

  Ex)

<hide/>
enum Direction { EAST, SOUTH, WEST, NORTH }

public class EnumEx1 {
	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:	// Direction.EAST 라고 쓸 수 있다.
				System.out.println("The direction is SOUTH."); break;
			case WEST:	// Direction.EAST 라고 쓸 수 있다.
				System.out.println("The direction is WEST."); break;
			case NORTH:	// Direction.EAST 라고 쓸 수 있다.
				System.out.println("The direction is NORTH."); break;
			default: 
				System.out.println("Invalid direction.");
		}
		Direction[] dArr = Direction.values();
		for(Direction d: dArr) {
			System.out.printf("%s = %d%n" , d.name(), d.ordinal());	
		}
	}
}

  Note) 실행결과

 

  2.3 열거형에 멤버 추가하기

  - ordinal()이 열거형 상수가 정의된 순서를 반환하지만 사용하지 않는 게 좋다.

  - 불연속적인 열거형 상수의 경우, 원하는 값을 괄호()안에 적는다.

  - 정수를 저장한 필드(인스턴스 변수)를 추가해야한다.

  - 열거형의 생성자는 묵시적으로 private이므로 외부에서 객체 생성 불가.

  - 열거형의 생성자는 외부에서 호출 불가능

 

 

  Ex)

<hide/>
enum Direction {
	EAST(1, ">"), SOUTH(2, "V"), WEST(3, "<"), NORTH(4, "^");
	
	private static final Direction[] DIR_ARR = Direction.values();
	private final int value;	// 정수를 저장할 필드(인스턴스 변수)를 추가한다.
	private final String symbol;
	
	Direction(int value, String symbol){	// 생성자를 추가한다. private 생략됨
		this.value = value;
		this.symbol = symbol;
	}
	public int getValue() 	{return value;}
	public String getSymbol()	{return symbol;}
	public static Direction of(int dir) {
		if(dir < 1 || dir > 4) {
			throw new IllegalArgumentException("Invalid value :" + dir);
		}
		return DIR_ARR[dir - 1];
	}
	public Direction 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();
	}
}
public class EnumEx2 {
	public static void main(String[] args) {
		for(Direction d: Direction.values()) 
			System.out.printf("%s = %d %n", d.name(), d.getValue());

		Direction d1 = Direction.EAST;
		Direction d2 = Direction.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(Direction.EAST.rotate(1));
		System.out.println(Direction.EAST.rotate(2));
		System.out.println(Direction.EAST.rotate(-1));
		System.out.println(Direction.EAST.rotate(-2));	
	}
}

  Note) 실행결과

  - 하나의 열거형 상수여러 값을 지정할 수 있다. 

  - rotate(): 방향을 회전시키는 메서드, num의 값만큼 90도씩 시계 방향으로 회전한다.

   -> num이 음수일 때는 시계 반대 방향으로 회전한다.

 

  2.4 열거형의 이해

  - 열거형 상수 하나 하나가 Direction객체이다.

  - Direction클래스의 static상수 EAST, SOUTH, WEST, NORTH 값은 객체의 주소이다.

    -> 이 값은 바뀌지 않으므로 == 로 비교가 가능하다.

    -> 객체가 생성될 때마다 번호를 붙여서 인스턴스변수 ordinal에 저장한다. 

 

  Ex) 컴파일 에러 발생하는데 오류는 

<hide/>
abstract class MyEnum< T extends MyEnum<T>> implements Comparable {
	static int id = 0;
	int ordinal;
	String name = "";
	
	public int ordinal() { return ordinal;}

	MyEnum(String name){
		this.name = name;
		ordinal = id++;
	}
	public int compareTo(T t) {
		return ordinal - t.ordinal();
	}
}
abstract class MyTransportation extends MyEnum{
	static final MyTransportation BUS = new MyTransportation("BUS", 100) {
		int fare(int distance) {return distance * BASIC_FARE; }
	};
	static final MyTransportation TRAIN = new MyTransportation("TRAIN", 150) {
		int fare(int distance) {return distance * BASIC_FARE; }
	};
	static final MyTransportation SHIP = new MyTransportation("SHIP", 100) {
		int fare(int distance) {return distance * BASIC_FARE; }
	};
	static final MyTransportation AIRPLANE = new MyTransportation("AIRPLANE", 300) {
		int fare(int distance) {return distance * BASIC_FARE; }
	};	
	
	abstract int fare(int distance);	//추상메서드
	
	protected final int BASIC_FARE;
	
	private MyTransportation(String name, int basicFare) {
		super(name);
		BASIC_FARE = basicFare;
	}
	public String name() {return name;}
	public String toString() {return name;}
}
public class EnumEc4 {
	public static void main(String[] args) {
	MyTransportation t1 = MyTransportation.BUS;
	MyTransportation t2 = MyTransportation.BUS;
	MyTransportation t3 = MyTransportation.TRAIN;
	MyTransportation t4 = MyTransportation.SHIP;
	MyTransportation t5 = MyTransportation.AIRPLANE;
	
	System.out.printf("t1 = %s, %d%n", t1.name(), t1.ordinal());
	System.out.printf("t2 = %s, %d%n", t2.name(), t2.ordinal());
	System.out.printf("t3 = %s, %d%n", t3.name(), t3.ordinal());
	System.out.printf("t4 = %s, %d%n", t4.name(), t4.ordinal());
	System.out.printf("t5 = %s, %d%n", t5.name(), t5.ordinal());
	System.out.println("t1==t2 ? "+ (t1 == t2));
	System.out.println("t1.compareTo(t3) = " + t1.compareTo(t3));	
	}
}

  Note) 실행결과

  - 열거형에 추상 메서드를 추가하면 각 열거형 상수가 추상 메서드를 구현해야한다.

  

 

3. 애너테이션(annotation)

 

  3.1 애너테이션이란?

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

  • 주석처럼 프로그래밍 언어에 영향을 미치지 않으며 유용한 정보를 제공

 

  3.2 표준 애너테이션

  - Java에서 제공하는 애너테이션

  - @Override: 오버라이딩을 바르게 했는지 컴파일러가 체크하도록 한다.

    -> 오버라이딩할 때 메서드 이름을 잘못 적는 실수를 하는 경우가 많다.

    -> 메서드 선언부 앞에 붙인다.

  - @Deprecated: 앞으로 사용하지 않을 것을 권장하는 필드나 메서드에 붙인다.

    ex) Date클래스의 getDate()

   - @FunctionalInterface: 함수형 인터페이스에 붙이면 컴파일러가 올바르게 작성했는지 체크

    -> 함수형 인터페이스에는 하나의 추상메서드만 가져야 한다는 제약이 있다.

   - @SuppressWarnings: 컴파일러의 경고 메시지가 나타나지 않게 억제한다.

    -> 괄호() 안에 억제하고자한는 경고의 종류를 문자열로 지정

    -> 둘 이상의 경고를 동시에 억제하려면 괄호 안에 쭉 쓴다.

    -> 'Xlint'옵션으로 컴파일하면 경고메시지를 확인할 수 있다.

  - @SafeVarargs: 메서드에 선언된 가변인자 타입이 non-refiable 타입일 경우, 해당 메서드를 선언하는 부분과 호출하는 부분에서 "unchecked" 경고가 발생한다. 해당 코드에 문제가 없다면 경고를 억제시키기 위해 사용한다.

    -> @SafeVarargs로 "unchecked"는 억제할 수 있지만 "varargs"경고는 억제할 수 없기 때문에 

    -> @SuppressWarnings("varargs")를 같이 붙인다.

 

 

  3.3 메타 애너테이션

  - 메타 애너테이션은 '애너테이션을 위한 애너테이션'

  - 애너테이션의 적용 대상이나 유지 지간을 지정하는데 사용한다. 

  - @Target: 애너테이션을 정의할 때, 적용 대상 지정에 사용

    -> TYPE: 타입을 선언할 때, 애너테이션을 붙일 수 있다.

    -> TYPE_USE: 해당 타입의 변수를 선언할 때 붙일 수 있다.

  - @Retention: 애너테이션이 유지되는 기간을 지정하는데 사용

    -> 컴파일러에 의해 사용되는 애너테이션의 유지 정책은 SOURCE이다.

    -> 실행 시에 사용가능한 애너테이션의 정책은 RUNTIME이다.

   - @Documented: javadoc으로 작성한 문서에 포함시키려면 붙인다.

  - @Inherited: 에너태이션을 자손 클래스에 상속하고자 할 때 붙인다.

    -> 조상 클래스에 붙이면 자손 클래스도 붙인 것처럼 인식된다. 

  - @Repeatable: 반복해서 붙일 수 있는 애너테이션을 정의할 때 사용

    -> @Todo를 하나로 묶을 컨테이너 애너테이션도 정의해야한다.

  - @Native: 네이티브 메서드에 의해 참조되는 상수 필드에 붙이는 애너테이션이다.

  

 

  3.4 애너테이션 타입 정의하기

  - 애너테이션의 메서드는 추상 메서드이며 애너테이션을 적용할 때 지정한다. (순서 X)

  - 에너테이션의 요소

    -> 반환값이 있고 매개변수를 없는 추상메서드의 형태를 가진다.

    -> 적용시 값은 지정하지 않으면, 사용될 수 있는 기본값 지정 가능(Null 제외)

    -> 요소가 하나이고 이름이 value일 때, 요소의 이름 생략가능하다.

    ->  요소의 타입이 배열인 경우, 괄호{}를 사용한다. (값이 하나일 때는 생략 가능, 값이 없을 때는 빈 괄호)

  

  - 모든 애너테이션의 조상

    -> Annotation은 모든 애너테이션의 조상이지만 상속은 불가하다.

    -> 사실 Annotation은 인터페이스이다. 

 

  - java.lang.annotation.Annotation

    - 마커 애너테이션: 요소가 하나도 정의되지 않은 애너테이션

    - 애너테이션 요소의 규칙

    1) 요소의 타입은 기본형, String, enum, 애너테이션, class만 허용된다.

    2) 괄호 안에 매개 변수를 선언할 수 없다.

    3) 예외를 선언할 수 없다.

    4) 요소를 타입 매개 변수로 정의할 수 없다.

 

 

  Ex) AnnoEx5

<hide/>

@Deprecated
@SuppressWarnings("1111")
@TestInfo(testedBy = "aaa", testDate =  @DateTime(yymmdd= "160101", hhmmss="235959"))
public class AnnotationEx5 {
    public static void main(String[] args) {
        Class<AnnotationEx5> cls = AnnotationEx5.class;
        TestInfo anno = (TestInfo)cls.getAnnotation(TestInfo.class);
        System.out.println("anno.testedBy() = " + anno.testedBy());
        System.out.println("anno.testedDate().yymmdd() = " + anno.testDate().yymmdd());
        System.out.println("anno.testedDate().hhmmss() = " + anno.testDate().hhmmss());
        for(String str : anno.testTools())
            System.out.println("testTools =" + str);
        System.out.println();
        Annotation[] annoArr = cls.getAnnotations();
        for(Annotation a: annoArr)
            System.out.println(a);
    }
}

@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 }

  Note) 실행결과

  • getAnnotation(): 매개 변수로  정보를 얻는다.
  • getAnnotations(): 모든 애너테이션을 배열로 받아온다