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

LeeKyoungChang·2022년 3월 6일
0
post-thumbnail

Java의 정석 의 책을 읽고 정리한 내용입니다.

 

📚 1. 지네릭스(Generics)

📖 A. 지네릭스란?

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

  • 타입 안정성을 높인다는 것 : 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다.

🔔 지네릭스의 장점
(1) 타입 안정성을 제공한다.
(2) 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.

 

📖 B. 지네릭 클래스의 선언

class Box {
	Object item;
    
    void setItem(Object item) { this.item = item; }
    Object getItem() { return item; }
}

➡️

class Box<T> {// 지네릭 타입 T를 선언
	T item;
    
    void setItem(T item) { this.item = item; }
    T getItem() { return item; }
}
  • Box< T >에서 타입 변수라고 하며, Type의 첫 글자에서 따왔다.
  • 타입 변수가 여러 개인 경우에는 Map<K,V>와 같이 콤마,를 구분자로 나열하면 된다.
  • KKey(키)를 의미하고, VValue(값)을 의미한다.
f(x, y) = x + y 가 f(k, v) = k + v

와 같이 이들은 기호의 종류만 다를 뿐 임의의 참조형 타입을 의미한다는 것은 모두 같다.

 

지네릭 클래스가 된 Box클래스의 객체를 생성할 때는 다음과 같이 참조변수와 생성자에 타입 T 대신에 사용될 실제 타입을 지정해줘야 한다.

Box<String> b = new Bow<String>(); // 타입 T 대신, 실제 타입을 지정
b.setItem(new Object()); // 에러. String이외의 타입은 지정불가
b.setItem("ABC");	// OK. String이므로 가능
String item = b.getItem(); // 형변환이 필요없음

 

✔️ 지네릭스의 용어

class Box<T> {}

Box<T> : 지네릭 클래스. 'T의 Box' 또는 `T Box`라고 읽는다.
T : 타입 변수 또는 타입 매개변수.(T는 타입 문자)
Box : 원시타입(raw type)
Box<String> b = new Box<String>();
  • 지네릭 타입 호출 Box<String> : 타입 매개변수에 타입을 지정하는 것
  • 매개변수화된 타입(parameterized type), 대입된 타입 String : 지정된 타입 String와 같이

 

컴파일 후 Box<String>Box< Integer>는 이들의 원시 타입Box로 바뀐다. → 즉, 지네릭 타입이 제거된다.

 

✔️ 지네릭스의 제한

  • 지네릭스 클래스 Box의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절하다.
  • static멤버에 타입 변수 T를 사용할 수 없다. T는 인스턴스변수로 간주되기 때문이다.
class Box<T> {
	T[] itemArr; // Ok. T타입의 배열을 위한 참조변수
    	...
    T[] toArray() {
    	T[] tmpArr = new T[itemArr.length]; // 에러. 지네릭 배열 생성불가
        ...
        return tmpArr;
    }
    	...
}
  • 지네릭 타입의 배열을 생성하는 것도 허용되지 않는다.
  • 지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, new T[10]과 같이 배열을 생성하는 것은 안 된다는 뜻이다.

 

✏️ 지네릭 배열을 생성해야할 필요가 있을 때

  • new연산자대신 Reflection APInewInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object배열을 생성해서 복사한 다음에 T[]로 형변환하는 방법 등을 사용한다.

 

📖 C. 지네릭스 클래스의 객체 생성과 사용

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(); }
}

 

Box<T>의 객체를 생성

Box<Apple> appleBox = new Box<Apple>(); // OK
Box<Apple> appleBox = new Box<Grape>(); // 에러
  • 참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야 한다.
  • 일치하지 않으면 에러가 발생한다.

 

Box<Apple> appleBox = new FruitBox<Apple>(); // Ok. 다형성
  • FruitBoxBox의 자손이라고 가정할 때
  • 두 지네릭 클래스의 타입이 상속관계에 있고, 대입된 타입이 같은 것은 괜찮다.

 

Box<Apple> appleBox = new Box<Apple>();
appleBox.add(new Apple()); // Ok
appleBox.add(new Grape()); // 에러. Box<Apple>에는 Apple객체만 추가 가능
  • 생성된 Box< T>의 객체에 void add(T item)으로 객체를 추가할 때, 대입된 타입과 다른 타입의 객체는 추가할 수 없다.

 

Box<Fruit> fruitBox = new Box<Fruit>();
fruitBox.add(new Fruit()); // Ok
fruitBox.add(new Apple()); // Ok void add(Fruit item)
  • 타입 TFruit인 경우, void add(Fruit item)가 되므로 Fruit의 자손들은 이 메서드의 매개변수가 될 수 있다.
  • AppleFruit의 자손이라고 가정했다.

 

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 FruitBoxEx1 {
    public static void main(String[] args) {
        
        Box<Fruit> fruitBox = new Box<>();    // Box<Fruit> fruitBox = new Box<Fruit>();와 같음
        Box<Apple> appleBox = new Box<Apple>();
        Box<Toy> toyBox = new Box<Toy>();
//        Box<Grape> grapeBox = new Box<Apple>(); // 에러, 타입불일치
        
        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());    // 에러, Box<Apple>에는 Apple만 담을 수 있음
        
        toyBox.add(new Toy());
//        toyBox.add(new Apple());    // 에러, Box<Toy>에는 Apple을 담을 수 없음
        
        System.out.println(fruitBox);
        System.out.println(appleBox);
        System.out.println(toyBox);
    }
}

 
// 지네릭 클래스
class Box<T> {
    ArrayList<T> list = new ArrayList<>();    // 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(); }
}
[Fruit, Apple]
[Apple, Apple]
[Toy]

 

📖 D. 제한된 지네릭 클래스

class FruitBox<T extends Fruit> {	// Fruit의 자손만 타입으로 지정가능
	ArrayList<T> list = new ArrayList<T>();
    ...
} 
  • 지네릭 타입에 extends를 사용하면, 특정 타입의 자손만 대입할 수 있게 제한할 수 있다.

 

FruitBox<Apple> appleBox = nuew FruitBox<Apple>(); // Ok
FruitBox<Toy> toyBox = nuew FruitBox<Toy>(); // 에러. Toy는 Fruit의 자손이 아니다.
  • Fruit클래스의 자손들만 담을 수 있다는 제한이 더 추가된 것이다.

 

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
fruitBox.add(new Apple()); // Ok. Apple이 Fruit의 자손
fruitBox.add(new Grape()); // Ok. Grape가 Fruit의 자손
  • add()의 매개변수의 타입 TFruit와 그 자손 타입이 될 수 있으므로, 여러 과일을 담을 수 있다.

 

interface Eatable {}
class FruitBox<T extends Eatable> {...}
  • 다형성에서 조상타입의 참조변수로 자손타입의 객체를 가리킬 수 있는 것처럼, 매개변수화된 타입의 자손 타입도 가능한 것이다.
  • 타입 매개변수 TObject를 대입하면, 모든 종류의 객체를 저장할 수 있게 된다.
  • 만약, 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면, extend를 사용한다. (implements가 아닌)

 

class FruitBox<T extends Fruit & Eatable> {...}
  • 클래스 Fruit의 자손이면서 Eatable인터페이스도 구현해야한다면 &기호로 연결한다.
  • 이제 FruitBox에는 Fruit의 자손이면서 Eatable을 구현한 클래스만 타입 매개변수 T에 대입될 수 있다.

 

import java.util.ArrayList;

class Fruit implements Eatalbe {
    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 Eatalbe {}

public class FruitBoxEx2 {
    public static void main(String[] args) {
        FruitBox<Fruit> fruitBox = new FruitBox<>();
        FruitBox<Apple> appleBox = new FruitBox<>();
        FruitBox<Grape> grapeBox = new FruitBox<>();
//        FruitBox<Grape> grapeBox1 = new FruitBox<Apple>(); 타입 불일치
//        FruitBox<Toy> toyBox = new FruitBox<>(); 에러. Fruit의 자손 아님

        fruitBox.add(new Fruit());
        // 매개변수의 타입T도 Fruit와 그 자손 타입이 될 수 있음
        fruitBox.add(new Apple());
        fruitBox.add(new Grape());
        appleBox.add(new Apple());
//        appleBox.add(new Grape()); Grape는 Apple의 자손이 아님
        grapeBox.add(new Grape());

        System.out.println("fruitBox-" + fruitBox);
        System.out.println("appleBox-" + appleBox);
        System.out.println("grapeBox-" + grapeBox);
    }
}

// 클래스 Fruit의 자손이면서 Eatable인터페이스도 구현해야한다면 & 기호로 연결
// Fruit와 그 자손들만 대입가능 -> T extends Fruit
class FruitBox<T extends Fruit & Eatalbe> 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();
    }
}
fruitBox-[Fruit, Apple, Grape]
appleBox-[Apple]
grapeBox-[Grape]

 

📖 E. 와일드 카드

class Juicer {
	static Juice makeJuice(FruitBox<Fruit> box) { //< <Fruit>으로 지정
    	String tmp = "";
        for(Fruit f : box.getList()) tmp += f + "";
        return new Juice(tmp);
	}
}
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();
	...
System.out.println(Juicer.makeJuice(fruitBox)); // OK. FruitBox<Fruit>
System.out.println(Juicer.makeJuice(appleBox)); // 에러. FruitBox<Apple>
static Juice makeJuice(FruitBox<Fruit> box) {
	String tmp = "";
    for(Fruit f : box.getList()) tmp += f + "";
    return new Juice(tmp);
}

static Juice makeJuice(FruitBox<Apple> box) {
	String tmp = "";
    for(Fruit f : box.getList()) tmp += f +"";
    return new Juice(tmp);
}
  • 위처럼 오버로딩을 하면, 컴파일 에러가 발생한다.
  • 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기 때문이다.
  • 지네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거해버린다.
  • 그래서 위 두 메서드는 오버로딩이 아니라 메서드 중복 정의이다.

➡️ 이럴 때, 와일드 카드를 사용한다!

✏️ 와일드 카드

  • ?로 표현
  • 와일드 카드는 어떠한 타입도 될 수 있다.

 

extendssuper로 상한(upper bound)과 하한(lower bound)을 제한할 수 있다.

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

 

💡 참고

  • 지네릭 클래스와 달리 와일드 카드에는 &를 사용할 수 없다. ➡️ 즉, <? extends T & E>와 같이 할 수 없다!
  • 매개변수의 타입을 FruitBox<? extends Object>로 하면, 모든 종류의 FruitBox가 매개변수로 가능하다.

 

static <T> void sort(List<T> list, Comparator<? super T> c)
  • static옆에 있는 <T>는 메서드에 선언된 지네릭 타입이다.
  • 이런 메서드를 지네릭 메서드라고 한다.
  • 첫 번째 매개변수는 정렬할 대상이고, 두 번째 매개변수는 정렬할 방법이 정의된 Comparator이다.
  • Comparator의 지네릭 타입에 하한 제한이 걸려있는 와일드 카드가 사용되었다.

 

Compparator<? super Apple> : Comparater<Apple>, Comparator<Fruit>, Comparator<Object>
Comparator<? super Grape> : Comparator<Grape>, Comparator<Fruit>, Comparator<Object>

매개변수의 타입이 Comparator<? super Apple>이라는 의미란?

  • Comparator의 타입 매개변수로 Apple과 그 조상이 가능하다는 뜻
  • 즉, Comparator<Apple>, Comparator<Fruit>, Comparator<Object> 중의 하나가 두 번째 매개변수로 올 수 있다는 뜻이다.

 

📖 F. 지네릭 메서드

지네릭 메서드 : 메서드의 선언부에 지네릭 타입이 선언된 선언된 메서드

  • Collections.sort() : 지네릭 메서드
  • 지네릭 타입의 선언 위치는 반환 타입 바로 앞이다.
static <T> void sort(List<T> list, Comparator<? super T>c)

 

💡 참고
지네릭 메서드는 지네릭 클래스가 아닌 클래스에도 정의될 수 있다!

 

class FruitBox<T> {
		...
	static <T> void sort(List<T> list, comparator<? super T> c) {
		...
	}
}
  • 지네릭 클래스 FruitBox에 선언된 타입 매개변수 T와 지네릭 메서드 sort()에 선언된 타입 매개변수 T는 타입 문자만 같을 뿐 서로 다른 것이다.
  • static멤버에는 타입 매개변수를 사용할 수 없지만, 메서드에 지네릭 타입을 선언하고 사용하는 것은 가능하다!

 

💡 참고
내부 클래스에 선언된 타입 문자가 외부 클래스의 타입 문자와 같아도 구별될 수 있다.

 

이전 makeJuice() 메서드를

static Juice makeJuice(Fruit<? extends Fruit> box) {
	String tmp = "";
	for(Fruit f : box.getList()) tmp += f + "";
	return new Juice(tmp);
}

➡️

지네릭 메서드로 변경

static <T extends Fruit> Juice makeJuice(Fruit<T> box) {
	String tmp = "";
	for(Fruit f : box.getList()) tmp += f + "";
	return new Juice(tmp);
}

이제 지네릭 메서드를 호출할 때 이와 같다.

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();
	...
System.out.println(Juice.<Fruit>makeJuice(fruitBox));
System.out.println(Juice.<Apple>makeJuice(appleBox));
  • 그러나 대부분의 경우 컴파일러가 타입을 추정할 수 있기 때문에 생략해도 된다.
  • 위 코드에서도 fruitBoxappleBox의 선언부를 통해 대입된 타입을 컴파일러가 추정할 수 있다.
System.out.println(Juice.<Fruit>makeJuice(fruitBox)); // 대입된 타입을 생략할 수 있다.
System.out.println(Juice.<Apple>makeJuice(appleBox));

 

⚠️ 주의할 점

System.out.println(<Fruit>makeJuice(fruitBox));	// 에러. 클래스 이름 생략불가
System.out.println(this.<Fruit>makeJuice(fruitBox));	// Ok
System.out.println(Juicer.<Fruit>makeJuice(fruitBox));	// Ok
  • 지네릭 메서드를 호출할 때, 대입된 타입을 생략할 수 없는 경우에는 참조변수나 클래스 이름을 생략할 수 없다.
  • 같은 클래스 내에 있는 멤버들끼리 참조변수나 클래스이름,
    this클래스 이름을 생략하고 메서드 이름만으로 호출이 가능하지만, 대입된 타입이 있을 때는 반드시 써줘야 한다!
  • 이것은 단지 기술적인 이유에 의한 규칙이므로 그냥 지키기만 하면 된다!

 

public static void printAll(ArrayList<? extends Product> list,
			ArrayList<? extends Product> list2) {
	for(Unit u : list) {
		System.outprintln(u);
	}
}

↔️

public static <T extends Product> void printAll(ArrayList<T> list,
						ArrayList<T> list2) {
	for(Unit u : list) {
		System.outprintln(u);
	}
}
  • 지네릭 메서드는 매개변수의 타입이 복잡할 때도 유용하다.
  • 위와 같이 타입을 별도로 선언함으로써 코드를 간략히 할 수 있다.

 

✔️ 복잡하게 선언된 지네릭 메서드 예시

public static<T extends Comparable<? super T>> void sort(List<T> list)
  • 매개변수로 지정한 List<T>를 정렬한다는 것은 알겠지만, 메서드에 선언된 지네릭 타입이 조금 복잡하다!
  • 이럴 때, 와일드 카드를 걷어내자!

 

public static<T extends Comparable<T>> void sort(List<T> list)
  • 현재 와일드 카드를 걷어낸 결과를 보면, List<T>의 요소가 Comparable인터페이스를 구현한 것이어야 한다.

 

public static <T extends Comparable<? super T>> void sort(List<T> list)

(1) List<T>

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

(2) <T extends Comparable<? super T>>

  • TComparable을 구현한 클래스이어야 하며 (<T extends Comparable>). T또는 그 조상의 타입을 비교하는 Comparable이어야 한다는 것(Comparable<? super T>)을 의미한다.
  • 만일 TStudent이고, Person의 자손이라면, <? super T>Student, Person, Object가 모두 가능하다.

 

📖 G. 지네릭 타입의 형변환

Box<Object> objBox = null;
Box<String> strBox = null;

objBox = (Box<Object>)strBox; // 에러 발생 Box<String> -> Box<Object>
strBox = (Box<Object>)objBox; // 에러 발생 Box<Object> -> Box<String>
  • 대입된 타입이 다른 지네릭 타입 간에는 형변환은 불가능하다.

 

Box<? extends Object> wBox = new Box<String>)();
  • BoxBox<? extends Object>로 형변환은 가능하다!

 

static Juice makeJuice(FruitBox<? extends Fruit> box) {...}

FruitBox<? extends Fruit> box = new FruitBox<Fruit>(); // OK
FruitBox<? extends Fruit> box = new FruitBox<Apple>(); // OK
FruitBox<? extends Fruit> box = new FruitBox<Grape>(); // OK
  • makeJuice메서드의 매개변수에 다향성 적용

 

Optional<?> EMPTY = new Optional<>(); // 에러. 미확인 타입의 객체는 생성불가
Optional<?> EMPTY = new Optional<Object>(); // OK
Optional<?> EMPTY = new Optional<>(); // OK. 위의 문장과 동일
  • <?><? extends Object>를 줄여 쓴 것이며, <>안에 생략된 타입은 Object이다.
  • class Box<T extends Fruit>의 경우 Box<?> b = new Box<>;Box<?> b = new Box<Fruit>;이다.

 

✔️ 정리

Optional<Object>Optional<T>	// 형변환 불가능
Optional<Object>Optional<?>Optional<T> // 형변환 가능. 경고 발생
  • Optional<Object>Optional<String>으로 직접 형변환하는 것은 불가능하지만, 와일드 카드가 포함된 지네릭 타입으로 형변환하면 가능하다.
  • 대신 확인되지 않은 타입으로의 형변환이라는 경고가 발생한다.

 

Box<? extends Object> objBox = null;
Box<? extends String> strBox = null;

strBox = (Box<? extends String>)objBox; // OK. 미확정 타입으로 형변환 경고
objBox = (Box<? extends Object>)strBox; // OK. 미확정 타입으로 형변환 경고
  • 와일드 카드가 사용된 제네릭 타입끼리도 형변환이 가능하다.
  • 형변환이 가능하긴 하지만, 와일드 카드는 타입이 확정된 타입이 아니므로 컴파일러는 미확정 타입으로 형변환하는 것이라고 경고한다.

 

📖 H. 제네릭 타입의 제거

  • 컴파일러는 제네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다. 그리고 제네릭 타입을 제거한다.
  • 즉, 컴파일된 파일(*.class)에는 제네릭 타입에 대한 정보가 없는 것이다.

 

✏️ 기본적인 지네릭 타입의 제거과정
(1) 제네릭 타입의 경계(bound)를 제거한다.

  • 제네릭 타입이 <T extends Fruit>라면 TFruit로 치환된다. <T>인 경우는 TObject로 치환된다. 그리고 클래스 옆의 선언은 제거된다.
class Box<T extends Fruit>{
	void app(T t){
		// ...
	}
}

➡️

// 제네릭 타입의 경계 제거
class Box{
	void app(Fruit t){
		// ...
	}
}

 

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

  • Listget()Object타입을 반환하므로 형변환이 필요하다.
T get(int i){
	return list.get(i);
}

➡️

// 제네릭 타입 제거 후 타입이 일치하지 않으면, 형변환을 추가한다.
Fruit get(int i){
	return (Fruit)list.get(i);
}

 

와일드 카드가 포함되어 있는 경우

static Juice makeJuice(FruitBox<? extends Fruit> box){
	String tmp ="";
	for(Fruit f : box.getList()) tmp += f +" ";
	return new Juice(tmp);
}


➡️

static Juice makeJuice(FruitBox box){
	String tmp ="";
	Iterator it = box.getList().iterator();
	while(it.hasNext()){
		tmp += (Fruit)it.next() + " ";
	}
	return new Juice(tmp);
}
  • 적절한 타입으로의 형변환이 추가된다.

 

💡 참고

  • 지네릭스 공부방법 : 다른 장을 공부하면서 부족하다고 느끼는 부분을 다시 복습하는 방식으로 학습하면서 이해의 폭을 넓혀가야 한다!

 

profile
"야, (오류 만났어?) 너두 (해결) 할 수 있어"

0개의 댓글

관련 채용 정보