-
자바 - Generics언어/JAVA 2023. 3. 16. 12:09
1. Generics란?
-데이터타입의 일반화를 의미 (generalize)
-다양한 타입의 객체를 다루는 메서드나, 컬렉션 클래스에 타입체크 기능
-컴파일 시 이러한 타입 체크를 통해 타입의 안정성을 제공하고, 타입체크와 형변환 생략으로 코드가 간결해진다.
간단하게 객체의 타입을 미리 명시하므로써 형변환의 번거로움을 줄이고, 잘못된 타입의 기입을 컴파일 과정에서 잡아내어 오류를 줄여준다.
*generics가 도입되기 이전인 JDK 1.5이전에는 여러타입을 매개변수로 받거나, 반환할 때 Object를 사용했다.
->이 경우에 Object타입을 다시 형변환해야하는 번거로움이 있고, 이 과정에서 오류 발생 가능성이 있다. generics는 이를 보완해준다.
* List<T>처럼 인스턴스 필드에 타입 변수 객체 가져야한다고 생각하지말자
-> 클래스에 선언된 타입변수는 인스턴스 메서드 반환 값, 매개변수 등 필요한 곳에 사용된다.
2. Generics클래스 만들기
class Box<T>{ T item; void setItem(T item){ this.item = item; } T getItem() {return item;} }
위와 같이 지네릭 클래스 box를 정의 할 수 있다.
2.1 Generics용어
-지네릭 클래스: Box<T>를 의미
-타입변수: T - 타입변수 혹은 타입 매개변수
- 이 T에는 아무런 이름이나 지정해도 괜찮다.
- 임의의 참조형 타입을 의미한다.
- 여러개의 타입변수는 쉼표로 구분하여 명시할 수 있다.
- 위와같은 타입변수는 클래스 뿐 아니라 메소드 매개변수나 반환값으로 사용할 수 있다.
-원시타입: BOX
Box<T>클래스의 타입변수를 실제의 특정 타입으로 지정하는 것을 지네릭 타입 호출이라고 부른다. 이때 지정된 타입을
매개변수화된 타입이라고 한다.
Box<Integer> b = new Box<Integer>();
public static void main(String[] args){ Box<String> box1 = new Box<>(); Box<Integer> box2 = new Box<Integer>(); box1.setItem("String box"); box2.setItem(1); System.out.println(box1.getItem()); System.out.println(box2.getItem()); }
서로 다른 지네릭 타입 호출로 Box<T>를 호출해 보았다. 결과는 각각 String box와 1이다.
두개의 인스턴스는 서로다른 클래스가 아니다.
이러한 결과를 보면, generics을 이용은 마치 클래스를 메서드 오버로딩과 같이 사용하는 것과 같다.
2.2 지네릭스의 제한
-static멤버는 타입변수를 사용할 수 없다. (와일드 카드 아닌, 지네릭 클래스의 타입변수의미)
타입변수 T는 인스턴스 변수로 간주된다. -> static멤버는 인스턴스 멤버를 참조할 수 없다
static멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 한다.
->임의의 타입을 의미하는 T는 사용될 수 없다.
static T item; //에러 static int compare(T t1, T t2){....}; //에러
-지네릭 타입의 배열을 생성하는 것도 허용되지 않는다.
T[] iArr; // T타입(임의타입)형 배열을 위한 참조변수는 ok T[] toArr(){ T[] tempArr = new T[6]; //지네릭 배열은 생성불가 return tempArr; }
이는 new연산자 때문이다. new연산자는 컴파일 시점에 타입 T가 무엇인지 정확하게 알아야한다.
하지만, Box<T>클래스를 컴파일하는 시점에는 T가 어떤 타입이 될지 전혀 알 수 없다. 따라서 사용불가
같은 이유로 instanceof연산자도 T를 피연산자로 사용할 수 없다.
*지네릭 배열을 생성할 필요가 있다면, ReflectionAPI 혹은 Object배열을 생성해서 T[]로 형변환하는 방법을 생각해보자.
T[] tempArr = (T[])new Object[6]; //사용가능
2.3 지네릭 클래스 인스턴스의 생성과 사용
-지네릭 객체를 생성할 때 참조변수와 생성자에 대입된 타입은 일치해야한다. 이는 대입된 타입끼리 상속관계에 있어도 마찬가지.
Box<Object> box3 = new Box<String>(); //다형성 해줄 것 같지만 아니다.
-지네릭 클래스 사이의 다형성은 허용된다. (단, 대입된 타입은 일치해야함)
Box<String> box3 = new FBox<String>(); // FBox는 Box의 자손
-타입변수형 매개변수의 다형성도 허용된다.
//상속관계가 있는 두 클래스 class Fruit{} class Apple extends Fruit {} //Box에 add메서드 타입변수를 매개변수로 선언했다. void add(T item){...} Box<Fruit> ffbox = new Box<>(); // Fruit형으로 선언하면 -->void add(Fruit item)이다. ffbox.add(new Apple()); //다형성이 허용된다.
-즉 제네릭 타입은 상하관계가 없다.
2.4 제한된 지네릭 클래스 만들기 ( extends 의 사용 )
타입 변수를 사용하여 지네릭 클래스를 정의하면, 임의의 타입으로 클래스를 정의하도록 하여
-실제 인스턴스를 생성하여 사용할 때에는 한가지 타입으로만 지정하도록 제한되는 것은 맞지만,
-처음 대입시에는 모든 타입을 지정할 수 있다는 점이 남는다.
-그렇다면 처음 타입변수 T에 대입할 때 부터 타입을 제한할 수 있는 방법을 알아보자
class FBox<T extends Fruit & Eatable> extends Box<T>{...Box와 모두 동일} interface Eatable {//빈 인터페이스이다}
타입변수에 extends를 사용하면 특정 타입의 자손만 대입 할 수 있게 제한된다.
인터페이스를 구현해야한다는 제약을 둘 때도 extends를 사용한다.
만약 특정클래스의 자손이면서, 인터페이스도 구현해야한다면 &로 이어준다.
3. 완성된 Generic 클래스의 활용
3.1 와일드 카드
위에서 다루었듯이 제네릭 타입은 상하관계가 없다. 이러한 문제로 아래와 같은 문제점이 있다.
static void print(Box<Fruit> box){}
fruit형 box의 인스턴스를 가져와서 적절히 print해주는 메서드가 있다고 가정해보자.
위 메서드에는 두 가지사항이 걸린다.
- static에는 타입변수 T를 사용할 수 없으므로 제네릭을 사용하지 말던가 위와 같이 특정 타입이 대입된 제네릭 클래스를 사용해야함
- 위와 같이 특정한 제네릭 클래스를 대입하면, Fruit형 box인스턴스 외에는 print에 대입될 수 없고, 제네릭 타입이 다른 것 만으로는 오버로딩이 불가 --> Box<apple> box로 print만들기 불가
그렇다고 FruitPrint , ApplePrint등등 제네릭 타입에 따라 print메서드를 따로따로 만드는 것도 웃기다.
위와같은 문제를 해결하기 위해 와일드카드<?>가 고안되었다.
와일드카드 ?은 어떠한 지네릭 타입도 허용한다는 의미이지만, (그렇다고 any type은 아니고 unknown type에 가깝다.)
- <?> 제한 없음. 모든 타입 가능
-<? super T> T와 그 조상들만 가능 와일드 카드 하한.
-<? extends T> T와 그 자손들만 가능 와일드 카드 상한.
static void print(FBox<? extends Object> fbox){ Fruit ff = fbox.getItem(); // 에러 Fruit이 아닐 수 있다. } static void print(FBox<? extends Fruit> fbox){ Fruit ff = fbox.getItem(); // Fruit과 그 조상만 대입되므로 이건 괜찮다. }
위와 같이 변경하면, 모든 제네릭 타입의 FBox<>가 대입 가능하지만, 어떤 타입으로 선언된 FBox<T extends Fruit & Eatable>일지 모르므로 위와같이 Fruit 참조변수에 함부로 값을 담을 수 없다.
*와일드 카드는 지네릭클래스를 만들 때 사용하는 것이 아니라, 이미 만들어진 지네릭 클래스를 활용할 때 사용하는 것임을 명심하자.
3.2 Generic 메서드
메서드 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라 한다. 지네릭 타입의 선언 위치는 반환 타입 바로 앞이다.
static <T> void sort(List<T> list, Comparator<? super T> c); //지네릭 메서드 예시
***지네릭 클래스에 정의된 타입 매개변수와 지네릭 메서드에 정의된 타입 매개변수는 전~혀 다른 것이다.
***앞서 계속 나온 것 처럼 static 멤버에는 타입 매개변수는 사용할 수 없지만, 메서드에 지네릭 타입을 선언하고 사용하는 것은 가능하다.
-위와 같은 메서드에 선언된 지네릭 타입은 지역 변수를 선언한 것과 같다.
-이 타입 매개변수는 메서드 내에서만 지역적으로 사용될 것이므로 메서드가 static이냐 아니냐는 중요하지 않다.
static <T extends Fruit> void print(Box<T> box){ System.out.println(box.toString()); } //////////////////////위는 아래의 수정////////////// static void print(FBox<? extends Object> fbox) ////////////////////////////실제 사용///////////// //static 메서드이므로 클래스명.<지네릭 메서드 타입변수>메서드명(매개변수) ???클래스명???.<Fruit>print(ffbox)
메서드 호출 시에 지네릭 타입을 적는 것은 생략될 수 있다. ->선언된 지네릭 인스턴스의 지네릭타입을 보고 알아서 유추
*한가지 주의점은 지네릭 메서드를 호출할 때, 메서드에 대입된 타입을 생략할 수 없다면, 참조변수나 클래스 이름을 생략할 수 없다.
->즉 원래 클래스 내에 있는 멤버끼리는 참조변수명이나 클래스명을 생략하고 메서드 이름만으로 호출이 가능하지만,
대입된 타입<Fruit>등이 있을 때는 클래스명이나 참조변수명 생략할 수 없다.
와일드 카드와 마찬가지로 이미 완성된 지네릭 클래스를 활용할 때 와일드 카드와 함께 사용할 수 있는 하나의 방식일 뿐이다.
어렵게 생각할 것 없다.
4. Generic 타입의 제거
컴파일러는 지네릭 타입을 이용해 소스파일(.java)를 체크하고 필요한 곳에 형변환을 넣어준 뒤 지네릭 타입을 제거한다.
즉 컴파일이 완료된 바이트코드(.class)에는 지네릭 타입에 대한 정보가 없다.
*위와 같은 이유는 지네릭 도입 이전에 소스 코드와의 호환성을 유지하기 위해서 이다.
간단하게 지네릭 타입의 제거과정을 보면 다음과 같다.
1. 지네릭 타입의 경계를 제거한다.
만약 <T extends Fruit>이라면 T를 Fruit으로 치환한다. <T>인 경우에는 Object로 치환한다. 그리고 클래스 옆의 선언은 제거
2. 지네릭 타입을 제거한 후 타입이 일치하지 않으면, 형변환을 추가한다.
T get(int i){ return list.get(i); } Fruit get (int i){ return (Fruit)list.get(i) }
->컴파일 후 T가 Fruit이라면, List의 get()은 Object를 변환하므로 형변환 필요
와일드 카드가 포함되어 있어도 적절히 형변환을 추가한다.
static void print(FBox<? extends Fruit> fbox){ Fruit ff = fbox.getItem(); } static void print(FBox fbox){ Fruit ff = (Fruit)fbox.getItem(); . }
----------------------------------------------------------------------------------------------------------
*제한된 지네릭 <T extends Class>와 와일드 카드<? extends Class>
공통점: 타입변수의 범위를 제한
차이점:
제한된 지네릭: 타입변수(파라미터)를 제한하는 것
- 지네릭 클래스,메서드 선언 시
- 매개변수, 리턴타입으로 타입변수 사용시
- 지네릭 클래스 없이 <T extends ~> 등으로 사용가능
와일드카드: 특정 지네릭클래스의 타입변수를 제한하는 것
메서드의 리턴값, 매개변수로 지네릭 클래스가 사용될 때
- 지네릭 클래스<? extends ~> 등의 형식으로만 사용가능
-------------------------------------------------------------------------------------------------------------------
* <T extends Comparable<? super T>>
- T 는 Comparable 구현 클래스이며, 구현 Comparable 클래스는 T 또는 그 조상 타입 비교하는 Comparable이어야함
- 만일 T 가 Student이고 Person의 자손이라면
- T는 Comparable<Student>, Comparable<Person> , Comparable<Object> 셋 중 하나 가능
-------------------------------------------------------------------------------------------------------------------
참고자료
-자바의 정석
-https://sujl95.tistory.com/73
'언어 > JAVA' 카테고리의 다른 글
자바 - Arrays 클래스와 Comparator (0) 2023.03.23 자바 - Thread(4) 동기화 (0) 2023.03.21 자바 - thread(3) 실행제어 메서드 예제 (0) 2023.03.21 자바 -thread(2) [5~8] (0) 2023.03.20 자바 - thread(1) 기본특성 (1~4) (0) 2023.03.20