[자바의 정석]제네릭

YJS·2023년 12월 20일
0

1. 제네릭 사용법


제네릭이란?

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

→ 객체 타입을 컴파일 시에 체크하기 때문에 타입 안정성을 높이고 형변환 생략을 통해 간결한 코드 가능

class Box<T>             //제네릭 타입 T를 선언
{
	T item;
	void setItem(T item){ this. item = item;}
	T getItem(){ return item;}
}

Box<String> b = new Box<String>(); //타입 T대신 실제 타입을 지정

→ T를 type variable 이라고 하며 다른 글자로 대체 가능.

(e.g. ArrayList의 경우 element의 약자 E 사용.

Map<K,V> - Key, Value)

제네릭 클래스의 객체 생성

참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야 함

e.g. Box appleBox = new Box();

Box appleBox = new Box<>(); // 추정 가능한 경우 타입 생략 가능

Box appleBox = new FruitBox(); // 상속관계

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"  ;}}

class FruitBoxEx1 {
	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<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);
	}  // main의 끝
}

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

2. 제네릭 주요 개념 (바운디드 타입, 와일드 카드)


제네릭스 용어

💡 class Box{}

Box: 원시타입
T: 타입 변수 또는 타입 매개변수. (T는 타입 문자)
Box: 제네릭 클래스

1) 제네릭스의 제한

제네릭 클래스 Box의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 가능

(= 인스턴스 별로 다르게 동작하도록 만든 기능이므로)

Box<Apple> appleBox = new Box<Apple>(); // apple객체만 저장 가능
Box<Grape> grapeBox = new Box<Grape>(); // grape객체만 저장 가능

→ 그러나, static멤버에 타입 변수 T 사용 불가

(T는 인스턴스변수로 간주되기 때문에 모든 객체에 대해 동일하게 동작해야하는 static 멤버는 인스턴스 변수를 참조할 수 없음.

⇒ static 멤버는 타입 변수에 지정도니 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 함)

→ 제네릭 배열 타입의 참조변수 선언은 가능하나, 제네릭 타입의 배열을 생성하는 것은 불가.


class Box<T>{
T[] itemArr; // (o) T타입의 배열을 위한 참조변수 

T[] toArray(){
	T[] tmpArr = new T[itemArr.length]; // (x) 제네릭 배열 생성 불가

→ new 연산자는 컴파일 시점에 타입 T가 뭔지 알아야함. 그러나 위의 코드에서는 컴파일 시점에 T가 어떤 타입이 될지 알 수 없음. (instance of 역시 마찬가지)

→ 따라서, new 연산자 대신 Reflection API의 newInstance()메서드 혹은 Object 배열을 생성 후 형변환 하는 방법 사용

제네릭 타입에 extends 활용하면 특정 타입의 자손들만 대입하도록 제한 가능

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

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<Grape> grapeBox = new FruitBox<Apple>(); // 에러. 타입 불일치
//		FruitBox<Toy>   toyBox    = new FruitBox<Toy>();   // 에러.

		fruitBox.add(new 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);
	}  // main
}

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

2) 와일드 카드

여러가지 타입의 매개변수를 갖는 제네릭 타입을 만들때 제네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기때문에 고안된 방법

?로 표시하며 어떤 타입도 될 수 있음

💡 : 와일드 카드의 상한 제한. T와 그 자손들만 가능 : 와일드 카드의 하한 제한. T와 그 조상들만 가능 : 제한 없이 모든 타입이 가능. 와 동일
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 Juice {
	String name;

	Juice(String name)	     { this.name = name + "Juice"; }
	public String toString() { return name;		 }
}

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

class FruitBoxEx3 {
	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));
		System.out.println(Juicer.makeJuice(appleBox));
	}  // main
}

class FruitBox<T extends Fruit> extends Box<T> {}

class Box<T> {
//class FruitBox<T extends Fruit> {
	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();}
}

3. 제네릭 메소드 만들기


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

→ 제네릭 메서드 호출 시 대입된 타입을 생략할 수 없는 경우에는 참조변수나 클래스 이름을 생략 할 수 없음.

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

	System.out.println(Juicer.makeJuice(fruitBox));// 대입된 타입 생략 가능
	System.out.println(Juicer.makeJuice(appleBox));

	System.out.println(makeJuice(fruitBox));//에러. 클래스 이름 생략 불가

4. Erasure


  1. 제네릭 타입의 경계를 제거한다.
class Box<T extends Fruit> 
{
	void add(T t)
		{...}
}

👇

class Box {
	void add (Fruit t) 
		{ ... }
}
  1. 제네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가한다.
profile
우당탕탕 개발 일기

0개의 댓글