1. 제네릭이란?
자바(Java)에서 제네릭(Generic)은 클래스 내부에서 사용하는 데이터의 타입(Type)을 클래스의 인스턴스를 생성할 때 결정하는 것을 의미합니다. 객체의 타입을 컴파일 시점에 체크하기 때문에 타입 안정성을 높이고 형 변환의 번거로움을 줄일 수 있습니다. 즉, 서로 다른 Type 정의에 복수의 Class를 정의하는것보다 제네릭을 통해 Class를 인스턴화할 때 인자와 함께 Type을 넘겨줌으로써 하나의 Class로 복수의 Type을 처리 할 수 있습니다. 아래 간략하게 제네릭(Generic)을 사용하여 선언된 클래스와 객체를 선언한 부분을 살펴보겠습니다.
public class TestGeneric<T>
{
public T sample;
public void showYourType()
{
if(sample instanceof Integer)
System.out.println("Integer 타입");
else if(sample instanceof String)
System.out.println("String 타입");
}
}
TestGeneric 클래스의 멤버변수인 sample 멤버 변수는 T라는 Type을 가집니다. 하지만 T라는 타입은 존재하지 않는 타입입니다. T 타입은 나중에 TestGeneric 클래스의 인스턴스가 생성될 때 결정이 됩니다. TestGeneric 객체를 생성하는 main 함수를 보겠습니다.
public class Main{
public static void main(String[] args)
{
TestGeneric<String> stringType = new TestGeneric<String>();
TestGeneric<Integer> integerType = new TestGeneric<Integer>();
stringType.sample = "Hello";
integerType.sample = 1;
stringType.showYourType();
integerType.showYourType();
}
}
stringType 객체 선언부를 보면 <> 안에 타입을 지정하게 되는데 이때 들어가는 타입이 TestGeneric 클래스의 멤버변수 인 sample의 타입으로 결정이 됩니다. 즉, sample 인스턴스 변수의 타입은 String이 됩니다. integerType 인스턴스의 sample 멤버변수의 타입은 Integer가 됩니다.
이렇게 객체 생성 시점에 타입이 결정되기 때문에 객체를 참조할 때 명시적으로 형 변환을 해줄 필요가 없습니다. 대표적으로는 제네릭 기반의 클래스로 ArrayList가 존재합니다.
public class Main{
public static void main(String[] args)
{
ArrayList<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
for(int i = 0 ; i < list.size(); i++)
{
System.out.println(list.get(i));
}
}
}
또한 다형성을 사용해야하는 경우 부모타입을 지정함으로써 여러 종류의 객체를 저장할 수 있습니다.
class Product{ }
class Tv extends Product{ }
class Audio extends Product{ }
//Product 클래스의 자손객체들을 저장할 수 있는 ArrayList를 생성
ArrayList<Product> list = new ArrayList<Product>();
list.add(new Product());
list.add(new Tv());
list.add(new Audio());
Product p = list.get(0);// 형변환이 필요없다.
Tv t = (Tv)list.get(i);// 형변환을 필요로 한다.
2. 복수 제네릭
위의 코드에서처럼 한개의 제네릭을 사용할 수 있지만 복수개의 제네릭을 사용하는 것도 가능합니다.
public class TestGeneric<T, K>
{
public T sample;
public K sapmle2;
}
제네릭의 구분은 쉼표(,)를 통해 하며 <T,K>와 같은 형식으로 사용하며 어떠한 문자도 가능합니다. T,K 모두 객체가 생성될 때 결정됩니다.
public class Main{
public static void main(String[] args)
{
TestGeneric<String, Integer> generic = new TestGeneric<String, Integer>();
}
}
3. 제네릭의 특징
- 객체 생성이 가능한 타입에 대해서만 제네릭(Generic) 사용이 가능합니다. 기본 데이터 타입인 int, long 등에 대해서는 지정이 불가능합니다. 다만 기본 타입을 객체 타입으로 사용하는 Wrapper 클래스(Integer, Boolean 등)에는 제네릭 사용이 가능합니다.
public class Main{
public static void main(String[] args)
{
TestGeneric<int, long> generic = new TestGeneric<int, long>();
}
}
위의 코드와 같이 기본 데이터 타입인 int와 long을 제네릭으로 사용하면 컴파일 에러가 발생합니다. 또한 static 변수에도 제네릭을 사용할 수 없습니다. static 변수는 인스턴스에 종속되지 않는 클래스 변수이기 때문입니다.
- 생성자를 통해 제네릭을 생략할 수 있습니다. Data 클래스의 인스턴스 data를 생성할 때 생성자 함수 인자 정보로 String형 데이터를 넘겨줘서 Data의 data 멤버 변수의 타입을 알기 때문에 제네릭 생략이 가능합니다. data와 data2 객체를 생성하는 코드는 정확하게 같은 동작을 수행하게 됩니다.
public class Data<T> {
public T data;
public Data(T data)
{
this.data = data;
}
}
public class Main{
public static void main(String[] args)
{
Data data = new Data("Hello World");
Data<String> data2 = new Data("Hello World");
}
}
4. 제네릭 메소드
클래스의 메소드에서도 제네릭 메소드를 정의할 수 있습니다. 타입 매개변수의 사용은 메소드 내부로 제한됩니다.
public class Data {
public static <T> T showData(T data)
{
if(data instanceof String)
System.out.println("String");
else if(data instanceof Integer)
System.out.println("Integer");
else if(data instanceof Double)
System.out.println("Double");
return data;
}
}
public class Main{
public static void main(String[] args)
{
Data.<String>showData("Hello World");
Data.showData(1);
Data.showData(1.0);
}
}
제네릭 메소드를 호출할 때는 실제 타입을 <> 안에 넣어줘도 되고 생략해도 됩니다.
5. 타입 매개변수 제한
제네릭의 타입으로 특정 클래스나 자식 클래스 타입만 오도록 타입 매개변수를 제한할 수 있습니다.
public class Parent {
}
public class Child extends Parent {
}
public class Data<T extends Parent> {
}
public class Main{
public static void main(String[] args)
{
Data<Parent> data = new Data();
Data<Child> data2 = new Data();
//Data<String> data3= new Data(); //컴파일 오류가 난다
}
}
Data 클래스의 제네릭 매개변수 타입을 보면 <T extends parent>로 정의되어 있습니다. 이를 해석하자면 T 매개변수의 타입은 parent 객체 타입이거나 parent 클래스를 상속받는 클래스의 타입만 올 수 있도록 제한하겠다는 의미입니다. 실제로 이 타입 이외의 타입이 오는 경우 컴파일 시점에 오류가 발생합니다.
6. 제네릭 와일드카드
매개변수에 과일박스(FruitBox)를 대입하면 주스를 만들어서 반환하는 Jucier 클래스가 있습니다. 이 클래스에는 과일을 주스로 만들어 반환하는 makeJuice()라는 static 메서드가 있습니다.
class Juicer {
static Juice makeJuice(FruitBox<Fruit> box) {
String tmp = "";
for(Fruit f : box.getList()) {
tmp += f + " ";
}
return new Juice(tmp);
}
}
이 메서드의 패러미터 타입을 Fruit타입의 FruitBox 객체로 제한하였기 때문에, Fruit 이외의 타입 은 들어갈 수 없습니다.(Juicer는 제네릭 클래스도 아니고, 제네릭 클래스라고 하여도 static메서드에서는 T를 사용할 수가 없습니다.) 하지만 Fruit의 자식인 Grape나 Apple 등도 집어넣고 싶다면 어떻게 해야될까요?
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
Juicer.makeJuice(fruitBox); // OK
FruitBox<Grape> grapeBox = new FruitBox<Grape>();
Juicer.makeJuice(grapeBox); // Err!
이럴 때 사용하기 위해 나온것이 와일드카드입니다. 와일드카드는 기호 ? 로 나타내며 어떠한 타입도 될 수 있습니다.
<? extends T> // T와 자손들만 가능
<? super T> // T와 조상들만 가능
<?> // 모든 타입 가능. <? extends Object>와 같음
static Juice makeJuice(FruitBox<? extends Fruit>) {
...
}
FruitBox<Grape> grapeBox = new FruitBox<Grape>();
Juicer.makeJuice(grapeBox); // OK!
7. 제네릭을 사용하지 않았을 경우
public class SimpleArrayListForInteger {
private int size;
private int[] elementData = new int[5];
public void add(int value) {
elementData[size++] = value;
}
public int get(int idx) {
return elementData[idx];
}
}
public class SimpleArrayListForString {
private int size;
private String[] elementData = new String[5];
public void add(String value) {
elementData[size++] = value;
}
public String get(int idx) {
return elementData[idx];
}
}
같은 역할을 하는 메소드 add(), get() 이지만 두 군데에서 코드가 생깁니다. 메소드 파라미터 타입과 반환 타입이 서로 달라서 인터페이스나 상속을 통해 해결할 수도 없습니다.
REFERENCE
'Java' 카테고리의 다른 글
[Java-source quality] Redundant Modifier (0) | 2020.05.29 |
---|---|
[Java] JAVA API 도큐먼트 (0) | 2020.04.03 |
[Java] HashMap (0) | 2020.03.26 |
[Java] public static void main(String args[]) 분석 (0) | 2020.03.24 |
WAR, JAR , EAR 파일 이란 (2) | 2020.01.04 |