제네릭스란?
제네릭스란 다양한 타입의 객체들을 다루는 메소드나 컬렉션 클래스 컴파일시 타입 체크를 해주는 기능이다. 객체 타입을 '컴파일 시' 체크하므로 안정성을 높이고 형변환의 번거로움이 줄어든다.
타입 안정성을 높인다는것은 의도하지 않은 타입의 객체가 저장되는것을 막는다는 소리이다. 정리해보면 제네릭스의 장점은 아래와 같다.
- 타입 안정성을 제공한다
- 타입 체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.
제네릭을 적용한 제네릭 클래스 선언을 해보자
package generic;
class Box<T>{
T item;
void setItem(T item){
this.item = item;
}
T getItem(){
return this.item;
}
}
Box<T>에서 'T'를 타입변수라고 하며 T가 아닌 다른것을 사용해도 된다 . 기호의 종류는 다르되 상황에 맞게 알맞은 의미가 담긴 기호를 사용하는것이 좋다. 제네릭스의 용어를 조금 살펴보면
- Box<T> : 제네릭 클래스, T의 Box라고 읽는다
- T : 타입변수
- Box : 원시타입
제네릭스의 제한
모든 객체에 대해 동일하게 동작해야하는 static멤버(=static변수)에 타입변수 T를 사용할 수 없다. 타입변수는 무조건 인스턴스 변수로 취급하기 때문이다. 또한 T타입 배열을 생성하는것도 허용되지 않는다. 제네릭 배열 타입의 참조변수를 선언하는것은 가능하지만, new T[10]과 같은 형태는 생성할 수 없다.
package generic;
class Box<T extends Object>{
// static T item; // static멤버는 T타입을 사용할 수 없다.
T[] itemArr; // 참조변수는 만들 수 있다.
// T[] testArr = new T[10]; // 에러
}
이 이유는 new 연산자 때문이다. new연산자는 컴파일 시점에서 T가 어떤 타입인지 정확히 알아야 하지만, Box 클래스를 컴파일 하는 시점에서는 타입을 알 수 없다.
package generic;
import java.util.Arrays;
class Box<T extends Object>{
// static T item; // static멤버는 T타입을 사용할 수 없다.
T[] itemArr; // 참조변수는 만들 수 있다.
// T[] testArr = new T[10]; // 에러
int index;
// 경고가 발생할 수 있으나 타입 안정성 확신할 수 있으므로 경고 제거
@SuppressWarnings("unchecked")
Object[] arr = new Object[10];
public void createTwithObj(){
// this.itemArr = new T[arr.length]; 직접적인 T타입 제네릭 배열 생성 불가.
this.itemArr = (T[])new Object[arr.length]; // 강제 형변환을 사용해서 생성이 가능하다.
this.index = 0;
}
public String toString(){
return Arrays.toString(itemArr);
}
public boolean save(T t){
if(index >= itemArr.length){
return false;
}
else{
itemArr[index++] = t;
return true;
}
}
}
public class generic1{
public static void main(String[] args){
Box<Integer> a = new Box<Integer>();
a.createTwithObj();
a.save(1);
a.save(2);
a.save(3);
System.out.println(a); // [1, 2, 3, null, null, null, null, null, null, null]
}
}
제네릭 배열을 꼭 생성해야하는 경우에는 new 연산자 대신 Reflection API의 newInstance()와 같이 동적으로 객체를 생성하는 메소드로 배열을 생성한다거나, 배열을 생성해서 복사한 다음 T[]로 형변환하는 방법등을 사용한다.
제네릭 클래스 객체 생성과 용이성
제네릭 클래스 Box<T>라고 정의를 하면 이 객체에는 T타입 객체만 저장할 수 있다. 몇가지 주의해야할 상황을 살펴보자
package generic;
import java.util.*;
class Box<T>{
ArrayList<T> list = new ArrayList<T>();
void add(T item){
list.add(item);
}
T get(int index){
return list.get(index);
}
int size(){
return list.size();
}
public String toString(){
return list.toString();
}
}
class FruitBox<T> extends Box<T>{
public String toString(){
return "Fruit Box!" + list.toString();
}
}
class 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 generic1{
public static void main(String[] args){
Box<Fruit> fruitbox = new Box<Fruit>();
//Box<Fruit> fruitbox2 = new Box<Apple>(); // 참조변수와 생성자에 대입된 타입이 일치하지 않음
Box<Apple> applebox = new FruitBox<Apple>(); // Box와 FruitBox는 서로 상속관계이며, 대입된 타입은 동일하므로 괜찮다.
fruitbox.add(new Apple()); // Apple은 Fruit타입의 자손 타입이므로 가능
//fruitbox.add(new Toy()); // Toy와 Fruit은 상속관계가 아니므로 불가능
}
}
1. Box<T>의 객체를 생성할 때 참조변수와 생성자에 대입된 타입이 일치해야한다. 설령 두 타입이 상속 관계여도 허용되지 않는다.
2. 두 제레릭 클래스의 타입이 상속관계에 있고, 대입된 타입이 동일한 경우에는 괜찮다.
3. 대입된 타입을 가진 메소드에 대해서 자손들은 해당 메소드의 매개변수가 될 수 있다.(업캐스팅)
제한된 제네릭 클래스
타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만 모든 종류 타입을 지정할 수 있다는것에 변함은 없다. 타입의 종류를 제한하는 방법에 대해 알아보자
상속을 받아 구현할때 사용했던 키워드 extends를 사용하면 특정 타입의 자손들만 대입할 수 있게된다.
class Box<T extends Fruit>
위에처럼 사용하게 되면 Fruit와 Fruit의 자손 타입만 사용할 수 있게된다는 제한이 있다. 만일 클래스가 아니라 인터페이스르 구현해야 한다는 제약이 있는 경우에 이때도 extends키워드를 사용한다 implements를 사용하지 않는다는것에 주의해야한다. 만약 클래스 Fruit의 자손이면서 Eatable인터페이스도 구현해야한다면 아래와 같이 &기호로 연결해야한다.
package generic;
import java.util.*;
interface eatable{}
class Box<T>{
ArrayList<T> list = new ArrayList<T>();
void add(T item){
list.add(item);
}
T get(int index){
return list.get(index);
}
int size(){
return list.size();
}
public String toString(){
return list.toString();
}
}
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";}}
class FruitBox<T extends Fruit & eatable> extends Box<T>{} // Fruit자손이면서 Eatable인터페이스 구현
public class generic1{
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<Apple> applebox = new FruitBox<Grape>(); // 참조변수 타입이 서로 다르다
//FruitBox<Toy> toyBox = new FruitBox<Toy>(); // Toy는 Fruit의 자손타입이 아니다.
fruitBox.add(new Fruit());
fruitBox.add(new Apple());
fruitBox.add(new Grape());
appleBox.add(new Apple());
grapeBox.add(new Grape());
// appleBox.add(new Grape()); // Grape는 Apple의 자손타입 x
System.out.println("fruitbox : " + fruitBox);
System.out.println("Applebox : " + appleBox);
System.out.println("Grapebox : " + grapeBox);
}
}
와일드 카드
아래와 같은 클래스가 있다고 가정하자.
class Juicer{
static Juice makeJuice(FruitBox<Fruit> box){
String tmp = "";
for(Fruit f : box.getList()){
tmp += f + " ";
}
return new Juice(tmp);
}
}
우선 Juicer클래스는 제네릭 클래스가 아닌데다, 제네릭 클래스라 해도 제네릭 자체는 인스턴스 메소드 취급을 하기 때문에 static메소드에서는 사용할 수 없다. 그렇기 때문에 아예 제네릭을 사용하지 말거나 특정 타입을 완전히 지정해 주어야한다. 만약 내가 Fruit이 아닌 Apple에 대해서만 Juice객체를 반환하는 메소드를 만들고 싶다고 한다면 아래와 같이 오버로딩 방법을 생각해 볼 수 있을것이다.
class Juicer{
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);
}
}
하지만 이 코드는 컴파일 에러가 생긴다. 단지 제네릭 타입이 동일하다라는 이유로는 오버로딩이 성립되지 않는다. 위 코드에서 두 static메소드는 메소드 중복 정의로 해석되게 된다.
이럴때 사용하기 위해 만든것이 '와일드 카드'이다. 와일드 카드는 기호 '?'를 이용해서 표현한다. 와일드 카드는 어떠한 타입도 될 수 있다. 또한 와일드 카드 또한 extends와 super키워드로 상한 하한을 정할 수 있다.
- <? extends T> : 와일드 카드의 상한 제한, T와 그 자손들
- <? super T> : 와일드 카드 하한 제한, T와 그 조상들
- <?> : 제한 없음, 모든 타입 가능<? extends Object> 와 동일한 의미
extends를 사용하여 상한제한(T와 T의 자손타입만 사용 가능)
와일드 카드와 extends를 적용해 위 코드를 고쳐보면 아래와 같이 고칠 수 있다.
class Juicer {
static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp = "";
for (Fruit f : box.getList()) {
tmp += f + " ";
}
return new Juice(tmp);
}
}
이렇게 변경을 하면 Fruit과 그 자손 타입도 접근이 가능해진다.
package generic;
import java.util.*;
interface eatable{}
class Juice{
String name;
Juice(String name){
this.name = name + " juice";
}
public String toString(){
return this.name;
}
}
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();}
}
class Juicer {
static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp = "";
for (Fruit f : box.getList()) {
tmp += f + " ";
}
return new Juice(tmp);
}
}
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";}}
public class generic1{
public static void main(String[] args){
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();
fruitBox.add(new Apple());
fruitBox.add(new Grape());
appleBox.add(new Apple());
appleBox.add(new Apple());
System.out.println(Juicer.makeJuice(fruitBox));
}
}
와일드 카드와 super를 통해 하한 제한하기
아래 예제를 우선 보자
package generic;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
class FruitBox<T extends Fruit> extends Box{
}
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();
}
}
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);
}
Apple(){
super("Apple",100);
}
Apple(int weight){
super("Apple",weight);
}
}
class Grape extends Fruit{
Grape(String name, int weight){
super(name, weight);
}
Grape(){
super("Grape",200);
}
Grape(int weight){
super("Grape",weight);
}
}
class AppleComp implements Comparator<Apple>{
@Override
public int compare(Apple o1, Apple o2) {
return o2.weight - o1.weight;
}
}
class GrapeComp implements Comparator<Grape>{
@Override
public int compare(Grape o1, Grape o2) {
return o2.weight - o1.weight;
}
}
class FruitComp implements Comparator<Fruit>{
@Override
public int compare(Fruit o1, Fruit o2) {
return o1.weight - o2.weight;
}
}
public class generic1{
public static void main(String[] args){
FruitBox<Apple> appleBox = new FruitBox<Apple>();
FruitBox<Grape> grapeBox = new FruitBox<Grape>();
appleBox.add(new Apple(300));
appleBox.add(new Apple(100));
appleBox.add(new Apple(200));
grapeBox.add(new Grape(100));
grapeBox.add(new Grape(300));
grapeBox.add(new Grape(200));
Collections.sort(appleBox.getList(), new AppleComp());
Collections.sort(grapeBox.getList(),new GrapeComp());
System.out.println(appleBox);
System.out.println(grapeBox);
}
}
이 코드는 Collections.sort()를 사용해 applebox와 grapebox에 담긴 과일을 무게별로 정렬한다. Collections.sort()의 선언부를 살펴보자.
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Collections.html
이 링크에서 sort()메소드를 찾아보면 아래와 같이 되어있는것을 볼 수 있다.
보면 static옆에 제네릭이 붙어있는데, 이와 같이 제네릭이 붙어있는 메소드를 제네릭 메소드라고 부른다.(추후 다룸) 우선 첫번째 매개변수는 List인터페이스 구현 클래스가 들어가고 두번째 매개변수로 정렬할 방법이 정의된 Comparator이 들어가게 된다. Comparator를 보면 아래와 같이 되어있다.
Comparator<? super T>
extend가 아닌 super, 즉 하한제한을 한 것이다. 만약 타입 매개변수에 Apple을 대입하면 아래와 같이 정의된다.
static void sort(List<Apple> list, Comparator<Apple> c). 이는 해석을 해보면 List<Apple>을 정렬하기 위해서 Comparator<Apple>이 필요하다는것을 의미한다. 그렇기 때문에 위 코드에 아래와 같이 구현하였다.
class AppleComp implements Comparator<Apple>{
@Override
public int compare(Apple o1, Apple o2) {
return o2.weight - o1.weight;
}
}
이 코드에 대해 아래와 같이 Collections.sort()를 해주면 아무런 문제가 발생하지 않는다.
Collections.sort(appleBox.getList(), new AppleComp());
하지만 이 함수를 아래와 같이 grapebox에 적용을 시킨다면 어떻게될까?
Collections.sort(grapeBox.getList(),new AppleComp());
당연히 오류가 난다. 이유는 AppleComp는 <Apple>타입에 대해서만 작동하기 때문이다. 결국 아래와 같이 동일한 동작을 하는 코드 두개를 만들어 버리게 된다.
class AppleComp implements Comparator<Apple>{
@Override
public int compare(Apple o1, Apple o2) {
return o2.weight - o1.weight;
}
}
class GrapeComp implements Comparator<Grape>{
@Override
public int compare(Grape o1, Grape o2) {
return o2.weight - o1.weight;
}
}
동일한 동작을 하는 코드인데 다시 만든다는것은 더 문제이다. sort의 경우에는 super 키워드를 통해 와일드 카드에 하한제한을 적용한다.
Comparator<? super T> 이기 때문에 제네릭 타입에 Apple이 들어가면 Comparator< Apple > ,Comparator< Fruit >,Comparator< Object >
class FruitComp implements Comparator<Fruit>{
@Override
public int compare(Fruit o1, Fruit o2) {
return o1.weight - o2.weight;
}
}
제네릭 메소드
메소드 선언부에 제네릭 타입이 선언된 메소드를 제네릭 메소드라고 한다.위에서 본 sort도 제네릭 메소드이다. 제네릭 메소드의 제네릭 타입 위치는 반환타입 앞이다.
제네릭 클래스에 정의된 타입 매개변수와 제네릭 메소드에 정의된 타입 매개변수는 서로 다른 것이다.(단지 표기만 같게한 것이다). 우선 sort가 static메소드라는것을 보자. static멤버에는 타입 매개변수 사용이 금지되어있다.다만 제네릭 타입을 선언하고 사용하는것은 가능하다. 메소드 제네릭 타입 매개변수는 메소드 내에서만 지역적으로 사용될것이므로 메소드가 static이건 아니건 상관없다. 앞에서 나왔던 예제 중 아래 예제를 보자. 아래 예제에 대해 makeJuice()를 사용하기위해서는 아래와 같이 했어야 했다
class Juicer {
static Juice makeJuice(FruitBox<? extends Fruit> 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(Juicer.makeJuice(fruitBox));
System.out.println(Juicer.makeJuice(appleBox));
이를 제네릭 메소드로 바꾸고 호출하면 아래와 같이 바꿔줄 수 있다.
class Juicer {
static <T extends Fruit> Juice makeJuice(FruitBox<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(Juicer.<Fruit>makeJuice(fruitBox));
System.out.println(Juicer.<Apple>makeJuice(appleBox));
// 이와 같이 메소드 호출 앞에 제네렉 타입변수를 명시하지 않아도 된다
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();
System.out.println(Juicer.makeJuice(fruitBox));
System.out.println(Juicer.makeJuice(appleBox));
위와 같이 제네릭 메소드를 호출하기 위해서는 타입변수를 메소드 호출 앞에 붙여주어야 한다. 다만 대부분의 경우 컴파일러가 타입을 추정할 수 있기때문에 생략해도된다.
'Language > Java' 카테고리의 다른 글
[Java] 람다식 (0) | 2022.03.16 |
---|---|
[Java] Maven설치하기(M1 mac) (0) | 2022.03.01 |
[Java] Collection FrameWork (0) | 2022.02.08 |
[Java] 컬렉션 프레임워크(Collection Framework) (0) | 2022.01.25 |
[Java] 예외처리 (0) | 2022.01.08 |