[Java] 제네릭이 무엇인가 ??

YoungHo-Cha·2021년 9월 23일
2

Java

목록 보기
4/7
post-thumbnail

자바로 코딩을 하면서 다음과 같은 코드를 본 적이 있을 것이다.

ArrayList<Integer> ~~

육안으로 보아서는 "ArrayList를 생성하는데 그 타입은 Integer야!"라고 말하는 것을 알 수가 있다.

하지만 저렇게 지정해주는 것만 알아왔지, 정확히 어떻게 동작하는지 모른다.

제네릭에 대해서 제대로 알아보자.


🐱‍👓목차

  • 제네릭이란?
  • 타입 변수란?
  • 제네릭 용어, 타입, 다형성
  • iterator, HashMap과 제네릭
  • 제네릭 추가 내용
  • 와일드 카드, 지네릭 메서드
  • 제네릭 형변환

📌제네릭이란?

다음 코드를 보자.

ArrayList list = new ArrayList();
list.add(10);
list.add(20);
list.add("30");

System.out.println(list);

얼핏 보아서는 문제가 없다.
그리고 실제로 코드를 돌려보아도 문제가 없다.

하지만 위 코드는 아주 큰 위험요소가 있다.

list가 int라고 생각하여 다음 코드를 삽입하게 된다면?

Integer i = (Integer)list.get(2);

빨간 줄이 그어질까?
결론은 아니다..
컴파일러 입장에서는 Object가 return되어, Integer로 형변환 되는 것 이기 때문에 상관없다.

그렇게 된다면 컴파일 단계에서는 오류가 있는지 모르게 된다. 그래서 해당 프로그램을 배포하게 되고, 동작하던 도중 프로그램이 죽는다.

그것을 방지하기 위해서 "지네릭"을 이용하여 컴파일러에게 타입을 알려주게 된다.

지네릭을 사용하여 컴파일러에게 타입을 알려주자!


ArrayList<Integer> list = new ArrayList<Integer>();

list.add(3);
list.add("4"); //이제는 빨간 줄이 그어진다.

📌타입 변수란?

클래스를 작성할 때, Object타입 대신 타입 변수(E)를 선언해서 사용하는 것이다.

예를 들어 살펴보자.
(일반 클래스 -> 지네릭 클래스(JDK 1.5 ver))

타입변수 적용 전

public class ArrayList extends AbstractList {
	private transient Object[] elementData;
    public vollean add(Object o){ /* 생략 */ };
    public Object get(int index){ /* 생략 */ };

타입변수 적용 후(Object 부분을 자세히 보자!)

public class ArrayList<E> extends AbstractList {
	private transient E[] elementData;
    public vollean add(E o){ /* 생략 */ };
    public Object get(int index){ /* 생략 */ };

타입 변수 "E"를 이용하여 Object 자리를 대체한 것을 볼 수 있다.

  • 타입 변수는 대문자로 아무거나 한 글자만 쓰면 된다. (대부분 T, E를 사용한다.)

  • 객체를 생성시, 타입 변수 대신 실제 타입을 지정할 수 있다.

ArrayList<TV> tvList = new ArrayList<Tv>();
(E -> TV로 대입된다)

  • 지네릭의 타입 변수를 지정하면서, 형변환의 수고, 코드를 줄일 수 있다.

📌제네릭 용어, 타입, 다형성

🔎제네릭 용어

  • Box<T> : 지네릭 클래스, 'T의 Box' 또는 'T Box'라고 읽는다.

  • T : 타입 변수 또는 타입 매개변수(T는 타입 문자)

  • Box : 원시 타입

🔎제네릭 타입과 다형성

먼저 다음 클래스들을 토대로 이해해보자.


class Product{}
class TV extends Product{}
class Audio extends Product{}
  • 참조 변수와 생성자의 대입된 타입은 일치해야 한다.
ArrayList<Tv> list = new ArrayList<Tv>(); // ok 일치
ArrayList<Tv> list = new ArrayList<Tv>(); // 에러! 불일치!!
  • 지네릭 클래스간의 다형성은 성립한다.
List<Tv> list = new ArrayList<Tv>(); // ok, ArrayList가 List를 구현하고 있다.

List<Tv> list = new LinkedList<Tv>(); // ok, LinkedList가 List를 구현하고 있다.
  • 매개변수의 다형성도 성립한다.

ArrayList<Product> list = new ArrayList<Product>();
list.add(new Product());
list.add(new Tv()); // Product 자손도 ok
list.add(new Audio()); // Product 자손도 ok

📌iterator, HashMap과 제네릭

🔎Iterator<T>

  • 클래스를 작성할 때, Object타입 대신 T와 같은 타입 변수를 사용

지네릭 적용 전

Iterator it = list.iterator();
while(it.hasNext()){
	Student s = (Student)it.next();
    ...

지네릭 적용 후

Iterator<Student> it = list.iterator();
while(it.hasNext()){
	Student s = it.next(); // (Student)라는 형변환이 사라짐.

🔎HashMap<K,V>

  • 여러 개의 타입 변수가 필요한 경우, 콤마(,) 구분자로 선언

제네릭이 적용된 HashMap 코드를 보자.

public class HashMap<K, V> extends AbstractMap<K,V>{
	public V get(Object key){/* 생략 */}
    
    public V put(K key, V value){ /* 생략 */}
    
    public V remove(Object key){/* 생략 */}
    
    

위에서 key를 Object로 유지하는 이유는 HashMap이 실제 코드에서는 "hash"라는 메서드로 이루어져 있다.

hash라는 메서드가 Object의 형태로 key를 받기 때문에, 쓸때없는 형변환을 해주지 않기 위해서 key의 타입을 K가 아닌 Object로 유지해준 것이다.

HashMap을 생성해보자.

HashMap<String, Student> map = new HashMap<String, Student>();

map,put("자바왕", new Student("자바왕",1,1,100.100,100));

📌제네릭 추가 내용

🔎제한된 지네릭 클래스

  • extends로 대입할 수 있는 타입을 제한할 수 있다.
    이해가 안되니 코드로 바로 보자!
class FruitBox<T extends Fruit>{ // Fruit의 자손만 타입으로 지정가능하다.
	ArrayList<T> list = new ArrayList<T>();
}

-------------------------------------

FruitBox<Applt> appleBox = new FruitBox<Apple>(); // ok
FruitBox<Toy> toyBox = new FruitBox<Toy>(); // 에러!! toy는 fruit의 자손이 아니다.
  • 인터페이스인 경우에도 extends를 사용한다.
interface Eatable {}

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

🔎제네릭스의 제약

  • 타입 변수에 대입은 인스턴스 별로 다르게 가능하다.

  • static 멤버에 타입 변수 사용 불가
    static은 모든 인스턴스에 공통이라서 애초에 개념상 불일치하다.

  • 객체, 배열 생성할 때 타입 변수 사용불가. 선언은 가능!

T[] itemArr; // ok!

T[] tempArr = new T[itemArr.length]; // 지네릭 배열 생성 불가!!

📌와일드 카드, 지네릭 메서드

🔎와일드 카드

먼저 다음과 같은 객체 상속 관계가 있다.

  • 하나의 참조 변수로 대입된 타입이 다른 객체를 참조가 가능하도록 하는 것

코드로 이해해보자!

ArrayList<? extends Product> list = new ArrayList<Tv>(); //ok 1번

ArrayList<? extends Product> list = new ArrayList<Audio>(); //ok 2번

ArrayList<Product> list = new ArrayList<Tv>(); // 에러 타입 불일치 // 3번

3번을 살펴보면 Tv는 Product를 상속받았는데, 컴파일 에러가 나타난 것을 볼 수가 있다.

매우 불편하다.

그래서 와일드 카드를 통해서 범위를 제한할 수 있다.

🔎🔎와일드 카드 종류

와일드 카드의 종류는 3가지가 있다.

  1. <? extends> : 와일드 카드의 상한 제한. T와 그 자손들만 가능

다음과 같이 허용된다.

  1. <? super T> : 와일드 카드의 하한 제한. T와 그 조상들만 가능
  1. <?> : 제한 없음. 모든 타입이 가능.

🔎지네릭 메서드

  • 지네릭 타입이 선언된 메서드이다. (타입 변수는 메서드 내에서만 유효하다.)
static <T> void sort(List<T> list, Comparator<? super T> c)
  • 클래스의 타입 매개변수<T>와 메서드의 타입 매개변수 <T>는 별개이다.
class FruitBox<T>{ // T 1번
			
            //T 2번
    static <T> void sort(List<T> list, Comparator<? super T> c){
    ...
    
    }
    

T 1번과 T 2번은 다른 타입 변수이다. 2번은 해당 메서드에서만 사용한다.

  • 메서드를 호출할 때마다 타입을 대입해야 한다.
static <T extends Fruit> Juice makeJuice(FruitBox<T> box){
	String tmp = "";
    for(Fruit f : box.getList()) tmp += f + " ";
    return new Juice(tmp);

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

System.out.println(Juicer.<Fruit>makeJuice(fruitBox));
System.out.println(Juicer.<Apple>makeJuice(appleBox));

위 코드를 살펴보면, 출력문을 실행할 때 마다 타입 변수를 지정해준 것을 볼 수 있다. (생략이 가능하다.)

  • 생략이 불가능한 경우는 메서드를 호출할 때 타입을 생략하지 않을 때는 클래스 이름 생략 불가하다.(잘 안쓴다!)

📌제네릭 형변환

  • 지네릭 타입과 원시 타입 간의 형변환은 바람직 하지 않다.(경고)
	Box<Object> objBox = null;
    Box box = (Box) objBox; // ok but 제네릭 타입 -> 원시 타입. 경고 발생
    
    objBox = (Box<Object>)box; // ok but 원시 타입 -> 제네릭 타입. 경고 발생
    
    objBox = (Box<Object>)strBox; // 에러. Box<String> -> Box<Object>
    
    strBox = (Box<String>)strBox; // 에러. Box<Object> -> Box<String>
   
    
  • 와일드 카드가 사용된 지네릭 타입으로는 형변환이 가능하다.
	Box<Object>	objBox = (Box<Object>)new Box<String>(); //에러, Box<Object> -> Box<String> 불가능.
    
    Box<? extends Object> wBox = (Box<? extends Object>)new Box<String(); // ok
    
    Box<? extends Object> wBox = new Box<String>(); // ok

📒마치며

오늘은 Generic에 대해서 공부를 했다. 코딩을 하면서 "<>"가 나올 때 마다 찝찝했는데, 완벽히 정리를 할 수가 있었다.

하지만, 항상 제네릭으로 생성되어 있는 것만 사용한다. 내가 제네릭을 이용하여 어떠한 클래스나 메서드를 사용할 일이 있을까??..

사용해서 유연하게 만들면 좋을 것 같긴하다.

실제로 써보면서 조금 더 제네릭과 친해지는 작업을 해보도록 하자!


🧷Reference

profile
관심많은 영호입니다. 궁금한 거 있으시면 다음 익명 카톡으로 말씀해주시면 가능한 도와드리겠습니다! https://open.kakao.com/o/sE6T84kf

0개의 댓글