[JAVA] Generic

Dayeon myeong·2021년 3월 9일
0

[JAVA]자바 기초

목록 보기
2/3

Generic이란?

  • 데이터 타입(data type)을 일반화(generalize)하는 것
  • 즉, 클래스나 메서드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법

Generic을 사용해야 하는 이유

  • 컴파일 시 type check를 해주기 때문에 타입 안전성이 보장된다.
    - 잘못된 타입이 사용될 경우 컴파일 과정에서 체크가 가능하기 때문에 실행 중 에러(Runtime Exception) 전에 에러를 사전에 방지할 수 있게 된다.
  • 형변환을 생략할 수 있으므로 코드가 간결해진다.

형변환과 타입 변수

ArrayList tvList = new ArrayList();
tvList.add(new TV());

위 코드는 제네릭을 사용하지 않고 tvList에 객체를 저장하는 코드이다.
이럴 경우 tvList에서 값을 꺼낼 때 다시 특정 타입으로 변경해줘야 한다.

ArrayList tvList = new ArrayList();
tvList.add(new TV());


TV t = (TV)tvList.get(0); // 제네릭을 사용하지 않을 경우 형변환 필요

이런 경우 제네릭을 사용한다. 특정 타입(TV)으로 제한함으로써 타입 안정성이 보장되고, 타입 체크와 형 변환을 생략할 수 있게 되어 코드가 간결해진다.

ArrayList<TV> tvList = new ArrayList<TV>(); // TV 타입으로 제한

tvList.add(new TV()); 
tvList.add(new Audio()); // 컴파일 에러, TV 외에 다른 타입은 저장이 불가능

TV t = tvList.get(0); // 제네릭 사용하는 경우 형변환 불필요

위의 예시에서 본 것 처럼 Generic은 <> 안에 특정 타입변수를 설정해주는 식으로 사용한다.

ArrayList를 살펴 보면,

public class ArrayList<E> extends AbstractList<E> {
	private transient E[] elementData;
    public boolean add(E o) { ...}
    public E get(int index) { ... }
    ...
}

ArrayList에 타입 변수 E가 설정되어있다. 타입 변수 설정시 사용되는 알파벳은 다른 알파벳은 상관없다. 그냥 Element라는 의미로 E로 나타낸 것이고, 알파벳에 따라 기능이 달라지거나 하지는 않는다.

대표적으로 타입변수를 사용할 때는

  • T : Type
  • E : Element
  • K : Key
  • V : Value

를 사용한다고 한다.

위에서는 ArrayList에 타입을 TV로 설정 했기 때문에 실제로 객체 생성시에는
타입 변수(E) 대신 실제 타입(TV)가 대입이 된다.

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


public class ArrayList<TV> extends AbstractList<TV> {
	private transient TV[] elementData;
    public boolean add(TV o) { ...}
    public TV get(int index) { ... }
    ...
}

일반적으로 클래스를 만들어 줄때에는 아래 같이 쓸 수 있다.

public class User<T> {
	private T name;
    
    public User(T name) {
    	this.name = name;
    }

}


User<String> user = new User<>("ddd");

Generic 타입과 다형성

제네릭 타입을 사용할 시 다형성과 관련하여 몇가지 주의할 사항이 있다.

다형성 : 조상타입의 참조변수로 자손타입의 개체를 다룰 수 있는 것
= 부모가 자손을 대체해준다.

  1. 참조 변수와 생성자의 대입된 타입은 일치해야 한다.

아래 예제에서 TV의 부모 클래스를 Product라고 했을 때 부모 클래스인 Product로 TV 타입의 list를 대체할 수 없다.

class Product{...}

class TV extends Product{... }
ArrayList<TV> list = new ArrayList<TV>(); // ok, 일치
ArrayList<Product> list = new ArrayList<TV>(); // 에러, 불일치
  1. 지네릭 클래스 간의 다형성은 성립.
    하지만 제네릭 클래스 간의 다형성은 성립된다.

조상인 List가 자손인 arrayList를 대체할 수 있다.

List<TV> list = new ArrayList<TV>(); // ok, 다형성, arrayList가 list를 구현
  1. 매개변수의 다형성도 가능.
    Product의 자손인 TV 클래스를 매개변수로 쓸 수 있다.
ArrayList<Product> list = new ArrayList<Product>();
list.add(new Product());// ok
list.add(new TV()); // ok, Product의 자손 클래스도 저장 가능하다.

Generic 형변환

  • Generic 타입과 원시 타입 간의 형변환은 바람직 하지 않다.(경고 발생)
Box<Object> objBox = null; // 제네릭 타입의 objBox
Box box = (Box) objBox; // ok, 제네릭 타입 objBox -> 원시 타입 box 가능하지만 경고 발생
objBox = (Box<Object>)box; // ok, 원시 타입 box-> 제네릭 타입 objBox 가능하지만 경고 발생
  • 서로 다른 타입이 대입된 제네릭 타입끼리의 변환은 에러 발생
Box<Object> objBox = null;
Box<String> strBox = null;

objBox = (Box<Object>) strBox; // 에러, Box<String> -> Box<Object>는 불가능 , 대입된 타입 불일치
strBox = (Box<String>) objBox; // 에러, Box<Object> -> Box<String>은 불가능 , 대입된 타입 불일치
  • 와일드 카드가 사용된 지네릭 타입으로는 형변환이 가능
Box<? extends Object> wBox = (Box<? extends Object>)new Box<String>(); 
// ok , Box<String> -> Box<? extends Object> 가능, String은 Object의 자손 클래스이기 때문에 변환이 가능.

Box<? extends Object> wBox = new Box<String>();//위 문장과 동일

제한된 Generic 클래스

  • extends로 대입할 수 있는 타입을 제한한다.(인터페이스, 클래스 둘다 extends로 제한을 둬야 함)
class FruitBox<T extends Fruit> { // Fruit의 자손만 타입으로 지정이 가능
	...
}


FruitBox<Apple> appleBox = new FruitBox<Apple>();

인터페이스인 경우에도 extends를 사용해야한다.

interface Eatable{}
class FruitBox<T extends Eatable> { 
	...
}

Generic 제약

  • static 멤버에는 타입 변수가 사용이 불가하다.
class Box<T> {
	static T item; // 에러
    static int compare(T t1,T t2) {...} //에러
}

item과 compare메서드는 static 타입이기 때문에 프로그램 실행시 Box 인스턴스가 생성되기도 전에, T가 대입이 되기도 전에 메모리에 올라간다. 메모리에 먼저 올라가버리니 당연히 item의 타입과 compare 메서드의 인자로 사용이 불가능한 것이다.

또한, static 멤버는 인스턴스에 종속되지 않는 클래스 멤버로써 모든 인스턴스가 공통된 저장공간을 공유해야 한다.
만약 static 변수 item을 Box< String >에서는 String 타입으로, Box< Integer >에서는 Integer 타입으로 사용되어야 한다면 하나의 공유 변수가 생성되는 인스턴스에 따라 타입이 바뀌는 경우는 안되기 때문에 static멤버에는 제네릭을 사용할 수 없다.

  • 배열/객체 생성시 타입 변수는 사용이 불가능하다.(new T 에러 / 타입 변수로 배열 선언은 가능하다)
class Box<T> {
	T[] itemArr; // ok, 타입 변수로 배열 선언은 가능. 
    
   T[] toArray() {
   	T[] tmpArr = new T[itemArr.length]; // 에러, 제네릭 배열 생성 불가.
    ...
   }
}
class Box<T> {
	
   T create() {
   	T tmp = new T(); // 에러, 제네릭 객체 생성 불가.
    ...
   }
}

불가능한 이유는 new 연산자 때문이다. new 연산자는 컴파일 시점에 타입 T가 정확히 무엇인지 알기를 원한다. 그러나 T가 어떠한 참조변수 타입으로 선언될지 알 수 없기 때문에 에러가 난다.(new 연산자는 heap 영역에 충분한 공간이 있는지 확인한 후 메모리를 확보하는 역할을 하는데, 충분한 공간이 있는지 확인하려면 타입을 알아야한다.)

와일드 카드

와일드카드란 하나의 참조변수로 대입된 타입이 다른 객체를 참조하게끔 하는 것이다.

  • < ? extends T > : 와일드카드의 상한 제한, T와 그 자손들 가능
  • < ? super T > : 와일드카드의 하한 제한, T와 그 조상들 가능
  • < ? > : 제한 없음, 모든 타입이 가능하다.
ArrayList<? extends Product> list = new ArrayList<TV>(); // ok, Product의 자손인 TV이기 때문에 형변환이 가능

ArrayList<Product> list = new ArrayList<TV>(); // 에러, 서로 다른 타입이 대입된 제네릭 타입끼리의 변환은 에러 발생 (대입된 타입 불일치)
  • 메서드의 매개변수로도 와일드 카드를 사용가능하다.
static String makeJuice(FruitBox<? extends Fruit> box) { // 매개변수로 와일드카드 사용 가능
	...
}

Generic 메서드

제네릭 메서드는 메서드의 선언 부에 제네릭으로 리턴 타입, 파라미터의 타입이 정해지는 메서드이다.

//첫번째 T : generic type
//두번째 T : return type
//세번째 T : parameter type
// generic type(첫번째 T)을 선언하여 return type(두번째 T)과 파라미터의 타입(세번째 T)이 정해진다.
public static <T> T getName(T name) {
	return name;
}

제네릭 클래스에서 클래스 이름 뒤에 제네릭 타입을 명시한 것처럼 제네릭 메소드에서는 리턴 타입 앞에 제네릭 타입을 명시한다.

public class Application {
    public static void main(String[] args) {
        Application.<Integer>getID(123); // 타입 지정, 컴파일러가 Integer를 보고 타입지정 
        Application.getID(123); // 암묵적 호출, 매개 타입을 보고 컴파일러가 타입 추정
    }
    
    public static <T> T getID(T id){return (T)id;}
}

위처럼 메소드 호출 시에는 메소드 이름 앞에 매개 타입에 대입될 인자 타입을 지정해준다.
혹은 암묵적으로도 호출이 가능하다.

  • 제네릭 메서드는 제네릭 클래스와 달리 static이 가능하다.
class Box<T> {
	static T item; // 에러
    static int compare(T t1,T t2) {...} //에러
}

제네릭 클래스는 static멤버를 가질 수 없다고 했다. 위에서 말했듯이 static 멤버는 Box가 인스턴스가 되기전에 먼저 메모리에 올라가버리는데 T의 타입이 정해지지 않았기 때문에 에러가 난다.

하지만, 제네릭 메소드는 호출 시에 매개 타입을 지정하기 때문에 static이 가능하다.

public class Student<T> { // 클래스에 지정한 제네릭 타입 T (인스턴스 변수라고 생각)
    
    static <T> T getOneStudent(T id) { // 제네릭 메서드에 붙은 T (지역변수라고 생각)
        return id;
    }
}

여기서 주의할 점은 Student 클래스에 지정한 제네릭 타입 T와 제네릭 메서드에 붙은 T는 별개라는 것이다.

클래스에 표시하는 T는 인스턴스 변수라고 생각하면 된다.인스턴스가 생성될 때 마다 지정되기 때문이다. 그리고 제너릭 메소드에 붙은 T는 지역변수를 선언한 것과 같다고 생각하면 된다. (메소드의 붙은 모든 T는 클래스에 붙은 T와 다르다)

제네릭 클래스와 독립적

class Student<T>{ // 클래스의 제네릭 타입 T

    public T getOneStudent(T id){ return id }  // 1
    
    public <T> T getId(T id){return id;} // 2 제네릭 클래스의 T와 다름  
    
    public <S> T toT1(S id){return id; }  // 3
    
    public static <S> T toT2(S id){return id;}  // 4 에러 
}
  • 1 : 클래스의 제네릭 타입 T를 사용
  • 2 : 클래스의 제네릭 타입 T와 제네릭 메소드 타입 T는 다름
  • 3 : 일반메소드기 때문에 클래스의 타입과 제너릭 메소드의 타입을 같이 사용가능.
  • 4 : static 메소드기 때문에 클래스의 제너릭 타입 T를 사용하기 때문에 에러가 발생.

참고

https://ecsimsw.tistory.com/entry/%EC%9E%90%EB%B0%94-%EC%A0%9C%EB%84%A4%EB%A6%AD-%EB%A9%94%EC%86%8C%EB%93%9C

https://velog.io/@max9106/Java-Generic

https://imasoftwareengineer.tistory.com/73

자바의 정석

https://yaboong.github.io/java/2019/01/19/java-generics-1/

profile
부족함을 당당히 마주하는 용기

0개의 댓글