KyungHwan's etc.

자바 제네릭(generic) 본문

Java

자바 제네릭(generic)

KyungHwan_0 2018. 6. 20. 15:28

자바 제네릭(generic)

  • 자바에서 제네릭(generic)이란 데이터의 타입(data type)을 일반화한다(generalize)는 것을 의미한다.

  • 제네릭은 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법이다.

즉, 클래스 내부에서 사용할 데이터 타입을 나중에 인스터스를 생성할 때 확정하는 것을 제네릭이라 한다.

이렇게 컴파일 시에 미리 타입 검사를 하면 다음과 같은 장점을 가진다

  1. 클래스나 메소드 내부에서 사용되는 객체의 타입 안정성을 높일 수 있다.

  2. 반환값에 대한 타입 변환 및 타입 검사에 들어가는 노력을 줄일 수 있다.

객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다. ArrayList와 같은 컬렉션 클래스는 다양한 종류의 객체를 담을 수 있긴 하지만 보통 한 종류의 객체를 담는 경우가 더 많다.

제네릭의 선언 및 생성

자바에서 제네릭은 클래스와 메소드에만 다음과 같은 방법으로 선언할수 있다.

class MyArray<T> {

   T element;
   void setElement(T element) { this.element = element; }
   T getElement() { return element; }

}

위의 예제에서 사용된 'T'를 타입 변수(type variable)라고 하며, 임의의 참조형 타입을 의미한다.

꼭 'T'뿐만 아니라 어떠한 문자를 사용해도 상관없으며, 여러 개의 타입 변수는 쉼표(,)로 구분하여 명시할 수 있다.

타입 변수는 클래스에서뿐만 아니라 메소드의 매개변수나 반환값으로도 사용할 수 있다.

위와 같이 선언된 제네릭 클래스(generic class)를 생성할 때에는 타입 변수 자리에 사용할 실제 타입을 명시해야 합니다.

MyArray<Integer> myArr = new MyArray<Integer>();

class Person<T>{

public T info;// p1 일시 데이터 타입은 String이된다.(인스턴스 생성시 String 제네릭 생성)
// p2 일시 데이터 타입은 StringBuilder이 된다.
}



public class GenericDemo {
   public static void main(String[] args) {
       Person<String> p1 = new Person<String>();
       Person<StringBuilder> p2 = new Person<StringBuilder>();
  }

}

public T info;

클래스 Person의 필드 info의 데이터 타입은 T로 되어 있다. 그런데 T라는 데이터 타입은 존재하지 않는다. 이 값은 아래 코드의 T에서 정해진다.

class Person<T>{

위 코드의 T는 아래 코드의 <> 안에 지정된 데이터 타입에 의해서 결정된다.

Person<String> p1 = new Person<String>();

위의 코드를 나눠보자. 아래 코드는 변수 p1의 데이터 타입을 정의하고 있다.

Person<String> p1

아래 코드는 인스턴스를 생성하고 있다.

new Person<String>();

즉 클래스를 정의 할 때는 info의 데이터 타입을 확정하지 않고 인스턴스를 생성할 때 데이터 타입을 지정하는 기능의 제네릭이다.

컬렉션 클래스

컬렉션 클래스 이름 바로 뒤에 저장할 객체의 타입을 적어주면, 컬렉션에 저장할 수 있는 객체는 지정한 타입의 객체 뿐이다.

컬렉션클래스<저장할 객체의 타입> 변수명 = new 컬렉션클래스<저장할 객체의 타입>();

ArrayList<Tv> tvList = new ArrayList<Tv>();

// Tv객체만 저장할 수 있는 ArrayList를 생성

tvList.add(new Tv());

tvList.add(new Radio());// 컴파일 에러

  • 다형성을 사용해야 하는 경우에는 부모타입을 지정함으로써 여러 종류의 객체를 저장할 수 있다.

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);// 형변환을 필요로 한다.

Product 클래스가 Tv클래스의 조상이라 할지라도 아래와 같이는 할 수는 없다.

ArrayList<Product> list = new ArrayList<Tv>();//허용안함
List<Tv> tvList = new ArrayList<Tv>();// But, 허용된다.

 와일드카드

보통 제네릭에서는 단 하나의 타입을 지정하지만, 와일드 카드'?'를 사용하면 된다.

보통 제네릭에서는 단 하나의 타입을 지정하지만. 와일드 카드는 하나 이상의 타입을 지정하는 것을 가능하게 해준다.

<?>           // 타입 변수에 모든 타입을 사용할 수 있음.

<? extends T> // T 타입과 T 타입을 상속받는 자손 클래스 타입만을 사용할 수 있음.

<? super T>   // T 타입과 T 타입이 상속받은 조상 클래스 타입만을 사용할 수 있음.

아래와 같이 어떤 타입('?')이 있고 그 타입이 Product의 자손이라고 선언하면, Tv객체를 저장하는 'ArrayList<Tv>' 또는 Audio객체를 저장하는 'ArrayList<Audio>'를 매개변수로 넘겨줄 수 있다.

Tv와 Audio 모두 Product의 자손이기 때문이다.

public static void printAll(ArrayList<? extends Product> list){
  //Product 또는 그 자손들이 담긴 ArrayList를 매개변수로 받는 메서드
for(Unit u : list){
System.out.println(u);

}

}

다음 예제는 클래스 메소드(static method)인 cryingAnimalList() 메소드의 매개변수의 타입을 와일드카드를 사용하여 제한하는 예제이다.

import java.util.*;


class LandAnimal { public void crying() { System.out.println("육지동물"); } }
class Cat extends LandAnimal { public void crying() { System.out.println("냐옹냐옹");} }
class Dog extends LandAnimal { public void crying() { System.out.println("멍멍"); } }
class Sparrow { public void crying() { System.out.println("짹짹"); } }


class AnimalList<T> {
   ArrayList<T> al = new ArrayList<T>();

   public static void cryingAnimalList(AnimalList<? extends LandAnimal> al)
       LandAnimal la = al.get(0);
       la.crying();

  }



   void add(T animal) { al.add(animal); }
   T get(int index) { return al.get(index); }
   boolean remove(T animal) { return al.remove(animal); }
   int size() { return al.size(); }

}


public class Generic03 {

   public static void main(String[] args) {
       AnimalList<Cat> catList = new AnimalList<Cat>();
       catList.add(new Cat());
       AnimalList<Dog> dogList = new AnimalList<Dog>();
       dogList.add(new Dog());


       AnimalList.cryingAnimalList(catList);
       AnimalList.cryingAnimalList(dogList);

  }

}

실행 결과
-------------------------------------------------------------------------------------
냐옹냐옹
멍멍

extends는 상속(extends)뿐 아니라 구현(implements)의 관계에서도 사용할 수 있다.

interface Info{
   int getLevel();
}

class EmployeeInfo implements Info{

   public int rank;
   EmployeeInfo(int rank){ this.rank = rank; }
   public int getLevel(){
       return this.rank;

  }
}

class Person<T extends Info>{// extends는 상속이 무엇인가가 아니라, 부모가 누군가를 확인하는 코드이다.(implement가 아니다.)

   public T info;
   Person(T info){ this.info = info; }

}

public class GenericDemo {

   public static void main(String[] args) {
       Person p1 = new Person(new EmployeeInfo(1));
       Person<String> p2 = new Person<String>("부장");
}

복수의 제네릭

class EmployeeInfo{
   public int rank;
   EmployeeInfo(int rank){ this.rank = rank; }
}

class Person<T, S>{//복수의 제네릭을 사용할 시에는 ','를 사용한다.
   public T info;
   public S id;
   Person(T info, S id){
       this.info = info;
       this.id = id;
  }
}

public class GenericDemo {
   public static void main(String[] args) {
       Person<EmployeeInfo, int> p1 = new Person<EmployeeInfo, int>(new EmployeeInfo(1), 1);
  }
}

기본 데이터 타입과 제네릭

제네릭은 참조 데이터 타입에 대해서만 사용할 수 있다. 기본 데이터 타입에서는 사용할 수 없다.

class EmployeeInfo{

   public int rank;
   EmployeeInfo(int rank){ this.rank = rank; }
}

class Person<T, S>{
   public T info;
   public S id;
   Person(T info, S id){
       this.info = info;
       this.id = id;
  }
}

public class GenericDemo {

   public static void main(String[] args) {

       EmployeeInfo e = new EmployeeInfo(1);
       Integer i = new Integer(10);
       Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i);
       System.out.println(p1.id.intValue());
  }
}

new Integer는 기본 데이터 타입인 int를 참조 데이터 타입으로 변환해주는 역할을 한다.

이러한 클래스를 래퍼(wrapper) 클래스라고 한다. 덕분에 기본 데이터 타입을 사용할 수 없는 제네릭에서 int를 사용할 수 있다.

제네릭의 생략

Java SE 7부터 인스턴스 생성 시 타입을 추정할 수 있는 경우에는 타입을 생략할 수 있다.

MyArray<Integer> myArr = new MyArray<>(); // Java SE 7부터 가능함.

EmployeeInfo e = new EmployeeInfo(1);
Integer i = new Integer(10);
Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i);
Person p2 = new Person(e, i);// 제네릭 생략함

Reference

http://tcpschool.com/java

http://devbox.tistory.com/entry/Java-%EC%A0%9C%EB%84%A4%EB%A6%AD

Comments