09009

[Java] Generic 본문

Back-End/JAVA
[Java] Generic
09009

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