09009
[Java] Generic 본문
Generics
• Collection / Map에서 데이터형을 표준화하기 위해 사용
• 일반적인 코드를 작성 후 이 코드를 다양한 타입의 객체에 대하여 재사용 하는 기법
• 클래스나 메서드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법
• <>를 제네릭(Generics)이라 하는데 이 <>안에 어떠한 타입을 선언해주어 해당 ArrayList, List, Set 등이 사용할 객체의 타입을 지정해준다. → 이는 다룰 객체의 타입을 미리 작성하여 객체의 형변환을 할 필요를 없게하고 내가 사용하고 싶은 데이터 타입만 사용할 수 있게 해준다.
• 다양한 타입의 객체들을 다루는 메서드, 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능
→ 객체의 타입을 컴파일 시 체크해주므로 객체의 타입 안정성 높임.
Generic 사용 이유: 매개변수로 Object클래스를 상속받는 클래스만 올 수 있도록 형식을 지정하기 위함
Generic 클래스의 선언
Class Box<T> {}
- Box<T> : 지네릭 클래스
- T: 타입 변수, 타입 매개 변수
- Box : 원시 타입
Generic_class명<적용할_Generic_Type> 변수명; // 선언
변수명 = new Generic_class생성자명<적용할_Generic_Type>(); // 생성
Box<String> b = new Box<String>();
class Car {
Object item;
void setItem(Object item) {
this.item = item;
}
Object getItem( ) {
return item;
}
}
지네릭 타입 T 선언
class Car<T> {
T item;
void setItem(T item) {
this.item = item;
}
T getItem( ) {
return item;
}
}
Car<T>에서 T를 '타입 변수'라고 칭한다.
타입 변수는 T가 아닌 다른 것을 사용해도 무방하다.
Generics의 제한
지네릭 클래스 Box의 객체를 생성할 시, 객체 별로 다른 타입을 지정하는 것은 가능하다.
지네릭스의 목적은 인스턴스별로 다르게 동작하려고 만든 것이다.
Box<Apple> appleBox = new Box<Apple>(); // Apple 객체만 저장가능
Box<Grape> grapeBox = new Box<Grape>(); // Grape 객체만 저장가능
하지만, 모든 객체에 대해 동일하게 동작하는 static 멤버에 타입변수를 사용할 수 없다.
static 멤버는 인스턴스 변수를 참조할 수 없는데 T는 인스턴스 변수로 간주되므로 모순이 발생하기 때문이다.
static 멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일해야한다.
→ Box<Apple>.item과 Box<Grape>.item이 다른 것이여서는 안된다는 의미.
class Box<T> {
static T item; // 에러 발생
static int compare(T t1, T t2) {...} // 에러 발생
...
}
지네릭 타입의 배열을 생성하는 것도 허용되지 않는다.
지네릭 배열 타입의 참조변수 선언은 가능하나, 배열을 생성하는 것은 불가능하다.
이유 : new 연산자 때문. new 연산자는 컴파일 시점에 타입 T가 무엇인지 정확히 알아야 하는데 아래 Box<T> 클래스를 컴파일하는 시점에서는 T가 어떤 타입이 될지 알 수 없기 때문이다.
class Box<T> {
T[] Arr; // 가능
T[] Array() {
T[] tmpArr = new T[Arr.length]; // 에러 발생, 지네릭 배열 생성불가
...
}
}
Generic 클래스의 객체 생성
출처: Java의 정석 (남궁성)
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<Apple> appleBox = new Box<Apple>(); // 가능
Box<Apple> appleBox1 = new Box<Grape>(); // 에러 발생
상속 관계에 있어서도 마찬가지이다.
'Apple'이 'Fruit'의 자손이라고 가정할 때, 타입이 서로 다르면 에러가 발생한다.
Box<Fruit> appleBox = new Box<Apple>(); // 에러 발생
두 Generic 타입이 상속 관계에 있고 대입된 타입이 서로 같은 것은 가능하다.
('FruitBox'는 'Box'의 자손이라고 가정)
Box<Apple> appleBox = new FruitBox<Apple>(); // 가능
생성된 Box<T>의 객체에 'void add(T item)'으로 객체를 추가할 때, 대입된 타입과 다른 타입의 객체는 추가할 수 없다.
Box<Apple> appleBox = new Box<Apple>();
appleBox.add(new Apple()); // 가능
appleBox.add(new Grape()); // 에러 발생. Box<Apple>에는 Apple 객체만 추가할 수 있다.
하지만, 타입변수 T가 'Fruit'(Apple의 조상)인 경우 'void add(Fruit item)'이 된다.
그러므로 Fruit의 자손들은 이 메서드의 매개변수가 될 수 있다.('Apple'이 'Fruit'의 자손이라고 가정)
Box<Fruit> fruitBox = new Box<Fruit>();
fruitBox.add(new Fruit()); // 가능
fruitBox.add(new Apple()); // 가능. 다형성
참고 예제
✍ 입력
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 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();
}
}
public 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()); // 가능. void add(Fruit item)
appleBox.add(new Apple());
// appleBox.add(new Toy()); 에러 발생. Box<Apple>에는 Apple만 담을 수 있다.
toyBox.add(new Toy());
// toyBox.add(new Apple()); 에러 발생.
System.out.println(fruitBox);
System.out.println(appleBox);
System.out.println(toyBox);
}
}
💻 출력
[Fruit, Apple]
[Apple]
[Toy]
✍ GeneEx1.java
package ch11;
class GeneT<T> { // 객체 선언/생성할 때 T의 데이터형이 결정된다
T[] v;
public void set(T[] v) {
this.v = v;
}
public void print() {
for(T s : v) {
System.out.println(s);
}
}
}
public class GeneEx1 {
public static void main(String[] args) {
GeneT<String> gt = new GeneT<>();
String[] ss = {"가","나","다"};
gt.set(ss);
gt.print();
System.out.println("==================");
GeneT<Integer> gt2 = new GeneT<>();
Integer[] kk = {34, 78, 99, 37};
gt2.set(kk);
gt2.print();
}
}
💻 출력 결과
가
나
다
==================
34
78
99
37
✍ Car.java
package ch11;
public interface Car {
void print();
}
class Bus implements Car {
@Override
public void print() {
System.out.println("저는 버스입니다");
}
void move() {
System.out.println("정원이 40명입니다");
}
}
class FireEngine implements Car {
@Override
public void print() {
System.out.println("저는 자동차입니다");
}
}
class Ambulance implements Car {
@Override
public void print() {
System.out.println("저는 구급차입니다");
}
}
class Taxi {
public void print() {
System.out.println("저는 택시입니다");
}
}
Bus, FireEngine, Ambulance 클래스는 인터페이스 Car를 구현하기 때문에 인터페이스 Car의 print 메서드를 반드시 구현해야한다.
※ 인터페이스에서 선언된 모든 메서드는 public abstract가 생략되어 있다. → 즉, Car 인터페이스는 추상 메서드 보유 → 추상 메서드를 하나라도 보유한 클래스는 추상 클래스 → 추상 클래스를 상속받은 클래스는 부모에 있는 추상메서드를 반드시 재정의해야 한다. ---- 꼭 기억하기 !
✍ Gene2.java
package ch11;
import java.util.ArrayList;
public class Gene2 {
public static void main(String[] args) {
// 제네릭: 클래스뿐만 아니라 인터페이스도 쓸 수 있다.
// Generics에 클래스나 인터페이스 이름이 들어가면 그 클래스 또는 상속받거나 구현한 클래스만 사용 가능
ArrayList<Car> list = new ArrayList<>();
list.add(new Bus()); list.add(new Ambulance());
list.add(new FireEngine()); // list.add(new Taxi()); 에러 발생
// car를 구현하는 클래스만 사용 가능
// print 메서드 사용, Bus인 경우 move 메서드 실행하기
for(Car car : list) {
car.print();
if (car instanceof Bus) ((Bus) car).move();
}
}
}
Generics에 클래스나 인터페이스 이름이 들어가면 그 클래스 또는 상속받거나 구현한 클래스만 사용 가능하다. → 그러므로 Car 인터페이스를 구현하지 않은 Taxi 클래스는 Car 인터페이스를 사용한 ArrayList에 추가할 수 없다.
그리고 조상 클래스 Car는 본인에게 없는, 자손 클래스 Bus의 메서드(move())를 이용할 수 없기 때문에 꼭 형변환을 해줘야한다.
💻 출력
저는 버스입니다
정원이 40명입니다
저는 구급차입니다
저는 자동차입니다
class Orange {
int sugarContent;
public Orange(int sugar) {
sugarContent = sugar;
}
public void showSugarContent() {
System.out.println("당도 " + sugarContent);
}
}
class FruitBox {
Object item; // 뭐든지 담을 수 있게 Object 타입으로 선언
public void store(Object item) {
this.item = item;
}
public Object pullOut() {
return item;
}
}
public class ObjectBaseFruitBox {
public static void main(String[] args) {
FruitBox fbox1 = new FruitBox();
fbox1.store(new Orange(10));
Orange org1 = (Orange)fbox1.pullOut();
org1.showSugarContent();
FruitBox fbox2 = new FruitBox();
fbox2.store("오렌지");
Orange org2 = (Orange)fbox2.pullOut(); // syntax 상으로 문제가 없으나 실행 단계에서 문제가 생긴다
org2.showSugarContent();
}
}
💻 출력
pullOut() 메서드는 Object형을 반환 (Orange)로의 강제형변환은 프로그래머의 의도라고 생각해서 컴파일러는 문제삼지 않는다.
위 코드를 실행하였을 때 syntax 상으로 문제가 없으나 실행할 때 에러가 발생한다.
이유는 "오렌지"는 String형이므로 Orange형으로 반환이 불가능하기 때문이다.
위와 같은 오류를 대처하기 위해 작성하는 것이 Generic이다.
아래는 수정한 코드이다.
class Orange {
int sugarContent;
public Orange(int sugar) {
sugarContent = sugar;
}
public void showSugarContent() {
System.out.println("당도 : " + sugarContent);
}
}
class Apple {
int weight;
public Apple(int weight){
this.weight = weight;
}
public void showAppleWeight() {
System.out.println("무게 : " + weight);
}
}
class FruitBox<T> {
T item;
public void store(T item) {
this.item = item;
}
public T pullOut() {
return item;
}
}
public class Hello {
public static void main(String[] args) {
FruitBox<Orange> orBox = new FruitBox<Orange>();
orBox.store(new Orange(10));
Orange org = orBox.pullOut();
org.showSugarContent();
FruitBox<Apple> apBox = new FruitBox<Apple>();
apBox.store(new Apple(10));
Apple app = apBox.pullOut();
app.showAppleWeight();
}
}
💻 출력
당도 : 10
무게 : 10
'Back-End > JAVA' 카테고리의 다른 글
[Java] (중요) 참조형 반환타입의 메서드 (0) | 2023.03.18 |
---|---|
[Java] 쓰레드 (0) | 2023.03.13 |
[Java] Arrays 클래스 (0) | 2023.03.12 |
[Java] collection (0) | 2023.03.11 |
[Java] Object 클래스, equals(), toString() (2) | 2023.03.11 |