-
Java 지네릭스(Generics)Java 2024. 2. 26. 11:01728x90반응형
지네릭스(Generics)
다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능이다.
객체의 타입을 컴파일 시에 체크하기 때문에, 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.
타입 안정성
의도하지 않은 타입의 객체가 저장되는 것을 막고,
저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여줌
>> 예를 들어, 다양한 종류의 객체를 담을 수 있긴 하지만, 보통 한 종류의 객체를 담는 경우가 더 많다.
그런데도 꺼낼 때마다 타입체크를 하고 형변환을 하는 것은 아무래도 불편하다.
또한, 원하지 않는 종류의 객체가 포함되는 것을 막을 방법이 없다는 것도 문제다.
이 것을 지네릭스가 해결해준다
1. 선언
클래스와 메서드에 선언할 수 있다.
예시class Box{ Object item; void setItem(Object item){this.item = item;} Object getItem(){ return item; } }
>> 클래스 옆에 <T>를 붙이고, 'Object'를 모두 'T'로 바꾼다class Box<T>{ T item; void setItem(T item){this.item = item;} T getItem(){ return item; } }
T를 타입변수라고 하며, T가 아닌 다른 것을 사용해도 된다. 글자 자체엔 의미가 없음, 그냥 x, y 이런거 해도 됨
근데 상황에 따라 Key는 K, Value는 V처럼 사용해주는 게 보기 좋다
Q. 그냥 T 안쓰고 처음부터 Object 대신 String 써버리면 되는 거 아닌가요?
A. 그러면 String일 때, int일 때 등등 모든 상황에 대해 오버로딩 해줘야함.
평생 String만 쓸 거 같으면 그래도 되는데 보통 안그럴거니까
2. 객체 생성 간단하게
참조변수와 생성자에 타입 T 대신 사용될 실제 타입을 적어야 함
Box<String>b = new Box<String>(); b.setItem(new Object()); // 에러, String 말고는 지정 불가 b.setItem("ABC"); String item = b.getItem(); // 형변환이 필요 없음, Box클래스 객체인데 String으로 형변환 안함
3. 용어
Box<T> : 지네릭클래스. T Box라고 읽는다.
T : 타입변수 or 타입 매개변수
Box : 원시 타입(raw type)
Box<String> : 지네릭 타입 호출
String : 매개변수화된 타입, 대입된 타입
4. 제한
지네릭스는 인스턴스별로 다르게 동작하려고 만든 기능이기 때문에,
모든 객체에 동일하게 동작해야 하는 static멤버에 타입변수 T를 사용할 수 없다.
T는 인스턴스 변수로 간주되는데, static멤버는 인스턴스변수를 참조할 수 없다.
지네릭 타입의 배열을 생성하는 것도 허용되지 않는다. 지네릭 배열 타입의 참조변수를 선언하는 것은 가능하다.class Box<T>{ T[] itemArr; // 됨 T[] toArray(){ T[] tmpArr = new T[itemArr.length]; // 이건 안됨 } }
new 연산자 때문에 생성이 안됨, 이 연산자는 컴파일 시점에 T가 뭔지 정확히 알아야 함.
근데 Box<T> 클래스를 컴파일하는 시점에선 T의 타입을 전혀 알 방법이 없다.
같은 이유로 instanceof 연산자도 T를 피연산자로 사용할 수 없다.ArrayList<T> list = new ArrayList<T>();
이건 가능
배열은 공변, 제네릭은 불공변
배열의 경우 Child가 Parent의 하위 타입일때 Child[]는 Parent[]의 하위 타입이 된다.
이런 경우를 공변하다고 한다.
반면 제네릭 타입의 경우 Sub가 Super의 하위 타입이더라도, ArrayList<Child>는 ArrayList<Parent>의 하위 타입이 아니다.
이런 경우를 불공변하다고 한다.이런 상황도 있다.
그냥 객체 생성 형태로 선언하면서 사용하는 건 배열 뿐아니라 다른 것도 안된다는 걸 보여주는 케이스의 질문임
Q. main 메서드에서 Box<T>를 호출할 때 Box<String> 이렇게 지정해서 부를텐데 T가 뭔지 알고 있는 상태 아닌가? 근데 왜 안되지?
A.public class Main { public static void main(String[] args) { ChildA a = new ChildA(); ChildB b = new ChildB(); } } class Box<T> { public void someMethod() { T t = new T(); // 여기서 T에 ChildB가 들어가면 ChildB엔 기본 생성자가 없다. // 그러면 문법적 오류가 생겨서 이 케이스를 허용 안해줌 } } class ChildA { ChildA() { super(); } } class ChildB { public String str; ChildB(String s) { super(); this.str = s; } }
5. 객체 생성과 사용
// ArrayList를 이용하면 여러 객체를 저장할 수 있다 classBox<T>{ ArrayList<T> list = new ArrayList<T>(); public void ~~~~ }
Box<T>의 객체를 생성할 때는 다음과 같이 한다.참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야 함, 상속관계에 있더라도
Box<Fruit> appleBox = new Box<Grape>(); // 에러
단, 두 지네릭클래스의 타입이 상속관계이고, 대입된 타입이 같은 것은 괜찮다Box<Apple> appleBox = new FruitBox<Apple>(); // 인정, 다형성
jdk1.7부터 추정이 가능한 경우 타입 생략 가능Box<Apple> appleBox = new Box<>(); // 허용
6. 제한된 지네릭 클래스
지네릭 타입에 extends를 사용하면 특정 타입의 자손들만 대입할 수 있다
class FruitBox<T extends Fruit>{}
주의
인터페이스를 구현할 때 implements가 아니라 똑같이 extends를 사용한다
클래스 Fruit의 자손이면서 Eatable 인터페이스를 구현해야 할 때 &로 연결한다class FruitBox<T extends Fruit & Eatable>{~~~}
이렇게 안하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만,
여전히 모든 종류의 타입을 지정할 수 있다는 것에는 변함이 없다.FruitBox<Toy> fruitBox = new FruitBox<Toy>(); fruitBox.add(new Toy()); // 과일 상자에 장난감을 담을 수 있다.
7. 와일드 카드
class Juicer{ static Juice makeJuice (FruitBox<Fruit> box){ ~~~ } }
Juicer클래스는 지네릭클래스가 아니고,지네릭클래스라 해도 static메서드는 타입매개변수 T를 매개변수에 사용할 수 없으므로,
아예 지네릭스를 적용하지 않던가, 위와 같이 타입 매개변수 대신 특정 타입을 지정해줘야 한다FruitBox<Apple> appleBox = new FruitBox<Apple>(); sysout(Juicer.makeJuice(appleBox)); // 에러
FruitBox<Apple> 타입 객체는 makeJuice의 매개변수가 될 수 없으므로,여러 타입의 매개변수를 갖는 makeJuice를 만들 수밖에 없다
static Juice makeJuice (FruitBox<Fruit> box){ ~~~ } static Juice makeJuice (FruitBox<Apple> box){ ~~~ }
그런데 위처럼 오버로딩하면, 컴파일 에러가 발생한다. 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않는다.
지네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거해버린다.
그래서 두 메서드는 오버로딩이 아니라 '메서드 중복정의'이다.
이 때문에 생각해낸 것이 와일드 카드이다.
기호로 ?을 사용하고, 어떤 타입도 될 수 있다.
?만으로는 Object타입과 다를게 없으므로, extends와 super로 상한과 하한을 제한한다
<? extends T> : 와일드카드의 상한 제한. T와 그 자손들만 가능
<? super T> : 와일드카드의 하한 제한. T와 그 조상만 가능
<?> : 제한 없음. <? extends Object>와 같음
참고 지네릭클래스와 달리 와일드 카드는 &를 사용할 수 없다. <? extends T & E> 같은 건 불가능
와일드 카드를 사용해 makeJuice의 매개변수 타입을 바꾼다static Juice makeJuice(FruitBox<? extends Fruit> box){ ~~~ }
이제 메서드의 매개변수로 FruitBox<Fruit>뿐 아니라, FruitBox<Apple>이랑 FruitBox<Grape>도 가능해졌다
매개변수 타입을 FruitBox<? extends Object>로 하면 모든 종류의 FruitBox가 매개변수로 가능해진다.
대신, 전과 달리 box의 요소가 Fruit의 자손이라는 보장이 없으므로,
아래의 for문에서 box에 저장된 요소를 Fruit타입의 참조변수로 못받는다.static Juice makeJuice(FruitBox<? extends Object> box){ String tmp = ""; for(Fruit f : box.getList()) tmp += f + " "; // 에러. Fruit이 아닐 수도 있음 return new Juice(tmp); }
그러나, 실제로 테스트 해보면 문제 없이 컴파일되는데, 지네릭 클래스 FruitBox를 제한했기 때문이다class FruitBox<T extends Fruit> extends Box <T>{ ~~~ }
컴파일러는 위 문장으로부터 모든 FruitBox의 요소들이 Fruit의 자손이라는 것을 알고 있으므로 문제 삼지 않는 것이다.
8. 지네릭 메서드
메서드의 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라 한다.
지네릭 타입의 선언 위치는 반환 타입 바로 앞static <T> void sort(List<T> list, Comparator<? super T> c)
참고 지네릭 메서드는 지네릭 클래스가 아닌 클래스에도 정의될 수 있다.
+ 앞에서 말했듯 매개변수의 타입이 T인건 안된다.
애초에 그걸 지네릭 메서드라 부르지도 않음. (T t1, T t2) 이런 것들class FruitBox<T>{ static <T> void sort(List<T> list, Comparator<? super T> c) }
위 코드에서 지네릭 클래스 FruitBox에 선언된 타입 매개변수 T와 지네릭 메서드 sort에 선언된 타입 매개변수 T는 문자만 같을 뿐, 서로 다른 것이다.
메서드에 선언된 지네릭 타입은 지역변수와 비슷한데,
이 타입 매개변수는 메서드 내에서만 지역적으로 사용될 것이므로 메서드가 static이건 아니건 상관없다.
같은 이유로 내부 클래스에 선언된 타입 문자가 외부 클래스의 타입 문자와 같아도 구별될 수 있다.
앞서 나왔던 makeJuice를 지네릭 메서드로 바꾸면 다음과 같다.static Juice makeJuice(FruitBox<? extends Fruit> box){ String tmp = ""; for(Fruit f : box.getList()) tmp += f + " "; return new Juice(tmp); } >> static <T extends Fruit> Juice makeJuice(FruitBox<T> box){ String tmp = ""; for(Fruit f : box.getList()) tmp += f + " "; // 에러. Fruit이 아닐 수도 있음 return new Juice(tmp); }
메서드를 호출할 때는 아래와 같이 타입 변수에 타입을 대입한다.FruitBox<Fruit> fr = new FruitBox<Fruit>(); sysout(Juicer.<Fruit>makeJuice(fr));
근데 대부분의 경우 컴파일러가 참조 변수의 선언부를 통해 타입을 추정할 수 있기 때문에 생략 가능하다sysout(Juicer.makeJuice(fr)); // 타입 생략
주의
같은 클래스 내에 있는 멤버끼리는 참조변수나 클래스 이름, this나 Juicer같은 것들 생략하고 메서드 이름만으로 호출이 되지만, 대입된 타입을 생략할 수 없는 경우에는 참조변수나 클래스 이름을 생략할 수 없다.sysout(<Fruit>makeJuice(fr)); // 에러, 클래스 이름 생략 불가 sysout(this.<Fruit>makeJuice(fr)); sysout(Juicer.<Fruit>makeJuice(fr));
사용처
매개변수의 타입이 복잡할 때, 코드 간략화public static void printAll(ArrayList<? extends Product> list, ArrayList<? extends Product> list2){ ~~ } >> public static <T extends Product> void printAll(ArrayList<T> list, ArrayList<T> list2){ ~~ }
9. 지네릭 타입의 형변환 정리
지네릭 타입과 원시 타입 간의 형변환
Box b = null; Box<Object> obB = null; b = (Box)obB; // 됨, 경고 발생 obB = (Box<Object>)b // 됨, 경고 발생
지네릭과 넌지네릭 간의 형변환은 항상 가능하다
대입된 타입이 다른 지네릭 타입 간 형변환Box<Object> obB = null; Box<String> stB = null; obB = (Box<Object>)stB; // 에러 stB = (Box<String>)obB; // 에러
대입된 타입이 Object더라도 불가능
와일드 카드를 이용한 형변환이 가능하다.Box<String> >> Box<? extends Object> Box<? extends Object> wBox = new Box<String>();
10. 지네릭 타입 제거
컴파일러는 지네릭 타입을 이용해 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다.
그리고 지네릭 타입을 제거한다.
즉, 컴파일된 파일(.class)에는 지네릭 타입에 대한 정보가 없다.
지네릭이 도입되기 이전의 소스코드와의 호환성을 위해,아직도 원시 타입을 사용해서 코드를 작성하는 것을 허용한다.
그러나 앞으로 가능하면 원시 타입을 사용하지 않도록 하자.
1. 지네릭 타입의 경계(bound) 제거
지네릭 타입이 <T extends Fruit>라면 T는 Fruit로 치환된다. <T>일 땐 Object로 치환된다.
그리고 클래스 옆의 선언은 제거된다.class Box<T extends Fruit>{ void add(T t){ ~~~ } } >> class Box{ void add(Fruit t){ ~~~ } }
2. 지네릭 타입을 제거한 후에 타입이 일치하지 않으면 형변환을 추가T get(int i){ return list.get(i); } >> Fruit get(int i){ return (Fruit)list.get(i); // 원래 list.get(i)의 타입은 Object }
728x90반응형'Java' 카테고리의 다른 글
Java 애너테이션 (0) 2024.02.26 Java 열거형(Enum) (0) 2024.02.26 Java 자료구조, 컬렉션 프레임웍 - 4, (Hash, Set, Map) (2) 2024.02.26 Java 자료구조, 컬렉션 프레임웍 - 3, (Iterator, ListIterator, Enumeration) (0) 2024.02.26 Java 자료구조, 컬렉션 프레임웍 - 2, 스택과 큐 (0) 2024.02.26