[JAVA] 지네릭스 (Generics) ①

DongGyu Jung·2022년 3월 8일
0

자바(JAVA)

목록 보기
39/60
post-thumbnail

🏃‍♂️ 들어가기 앞서..

본 게시물은 스터디 활동 중에 작성한 게시물로 자바의 정석-기초편 교재를 학습하여 정리하는 글입니다.
※ 스터디 Page : 〔투 비 마스터 : 자바〕

*해당 교재의 목차 순서와 구성을 참고하여 작성하며
각 내용마다 부족할 수 있는 내용이나 개인적으로 궁금한 점은
추가적인 검색을 통해 채워나갈 예정입니다.

"지네릭스""제네릭스" 발음 혼동주의....쓰다보니 계속 번갈아가면서 쓰게 되서..


🔍 지네릭스 ( Generics )

< 다양한 타입의 객체들을 다루는 메서드 / 컬렉션 클래스 >의
" 컴파일 시 "타입 체크(compile-time type check)를 해주는 기능

객체들의 타입을 컴파일할 때 타입 체크를 해주게 되면
객체의 ' 타입 안정성 '을 높이고
' 형변환 의 번거로움 '을 줄일 수 있다.
(▶ 간결한 코드 작성 )

// 특정 클래스, Tv클래스 타입의 객체만 저장할 수 있는 ArrayList를 생성
// 참조변수 & 생성자에 <>를 통해 타입 지정
ArrayList<Tv> tvList = new ArrayList<Tv>() ;

tvList.add(new Tv()) ; // 가능
tvList.add(new Audio()) ; // 컴파일 에러

위처럼
본래 ArrayListObject로 다양한 종류의 객체를 담을 수 있지만
보통 ArrayList는 보통 한 종류의 객체를 담는 경우가 대다수일 것이다.

그럴 때, 의도치 않게 다른 타입의 객체가 들어오게 되면 실행 중 문제가 발생할 수 있는데
이 때, 지네릭스를 통해
정해놓은 타입의 객체만 들어올 수 있게끔 하고
이 외에 다른 타입의 객체가 들어오면 에러가 발생한다.

또한 이렇게 정해진 타입의 객체만 들어가있기 때문에

꺼낼 때도 형변환을 할 필요가 없어져 편하게 사용할 수 있다.

ArrayList tvList = new ArrayList() ;

tvList.add(new Tv()) ;
Tv t = (Tv)tvList.get(0) ; // 형변환을 해주어야 함

ArrayList<Tv> tvList = new ArrayList<Tv>() ;
tvList.add(new Tv()) ;
Tv t = tvList.get(0) ; // 형변환을 해줄 필요 없음.

/*
< JDK1.5 이후 >
실제 ArrayList 클래스를 살펴보면
일반클래스인 ArrayList에서
ArrayList<E> 지네릭 클래스로 선언되어 있다.

(Object 타입으로 선언되어있던 클래스들이 지네릭 클래스로 많이 선언되어 있다.)
*/

JDK 1.5 이전까지는 꼭 지네릭스를 쓸 필요는 없었지만
" 지네릭스가 도입된 JDK 1.5 이후 버전에서는 " 지네릭스(Generics)를 꼭 써주어야 한다.

좋은 코드를 위한.... 첫 걸음...
(여러 종류를 저장하는 Object로 지정하더라도 〈Object〉로 지네릭스 해야한다.)



🚩 " 타입 " 변수

지네릭 클래스를 작성할 때,
Object 타입 대신 선언하는 " 타입 결정 변수 (ex. <E>) "

/*
예시 : ArrayList
*/
// 본래 Object타입으로 지정되어있던 ArrayList 클래스
public class ArrayList extends AbstractList {
	private transient Object[] elementData ;
    public boolean add(Object o) {...}
    public Object get(int index) {...}
    ...
}
// ArrayList 클래스를 지네릭 클래스로 변환하기 위한 타입변수 "<E>" 사용
public class ArrayList<E> extends AbstractList<E> {
	private transient E[] elementData ;
    public boolean add(E o) {...}
    public E get(int index) {...}
    ...
}

위처럼 <E>로 선언되어있는 ArrayList를
맨 처음 예시로 봤던 것 처럼
<Tv>클래스로 타입을 지정하게 되면

/*
※ 타입 변수에 대입하기
*/
// ArrayList의 기존 타입변수 "<E>" 에 "<Tv>"가 들어가게된다.
// 진짜 코드가 알아서 바뀌는건 아니다..;; 이런 형태로 작동하는 것 쁀...
ArrayList<Tv> tvList = new ArrayList<Tv>() ;

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

// 위와 같이 변하기 때문에 형변환을 생략할 수 있는 것이다.

이렇게 Tv 타입으로 바뀌게 된다.

만약
" 타입 변수가 여러 개인 경우 "에는
, 를 구분자로 나열하면 된다.
( ex. Map<K, V> : K:Key / V:Value )


※ 지네릭스 용어


: class Box {}class Box<T> {}

  • Box<T> : " 지네릭 클래스 " ( T의 Box / T Box 라고 읽음. )

  • T : 타입 변수 / 타입 매개변수

  • Box : 원시 타입

    T 는 Type의 약자로서 사용됨.

타입 변수를 타입 매개변수라고도 불리는 이유는
앞서 알아봤듯 저 타입변수에 사용자가 지정한 타입이 들어가기 때문에
메서드의 매개변수와 유사한 면이 있기 때문이다.

그래서
" 타입 매개변수에 타입을 지정하는 것 " → " 지네릭 타입 호출 "이라 하고
" 지정된 타입 " → " 매개변수화(parameterized)된 타입 " 이라고 한다. ( 줄여서 " 대입된 타입 " )

/*
대입된 타입이 다른다고 해서 
"서로 다른 클래스인 것이 아닌"
Box<T>에 서로 다른 타입을 대입하여 호출한 것 뿐이다.
*/
// 참조변수와 생성자의 대입된 타입 일치
Box<String> b = new Box<String>() ;

"서로 다른 클래스인 것이 아닌"
Box에 서로 다른 타입을 대입하여 호출한 것 뿐이다.
▶ " 컴파일 이후 "에는 이들의 원시타입인 Box로 바뀐다. ( 지네릭 타입 제거 )



🌌 다형성

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

여기서 만약 참조변수 타입과 생성자 타입 클래스가
서로 상속관계이더라도 지네릭이 일치해야한다.
( 상속관계 다형성 X )

  /*
  조상 : Product
  자손 : Tv, Audio
  */
  ArrayList<Tv> list = new ArrayList<Tv>() ; // 일치 O
  ArrayList<Product> list = new ArrayList<Tv>() ; // 조상-자손 관계 _ 에러 X

하지만
지네릭 클래스 간 다형성은 성립되고
심지어 매개변수의 다형성도 성립되는 것을 할 수 있다.

/*
< 지네릭 클래스 >
조상 : List
상속 구현 : ArrayList, LinkedList
*/
List<Tv> list = new ArrayList<Tv>() ; // 가능 _ ArrayList가 List 구현
List<Tv> list = new LinkedList<Tv>() ; // 가능 _ LinkedList가 List 구현
  
ArrayList<Product> pList = new ArrayList<Product>();
// 기존 boolean add(E e) 메서드가 (Product e)로 매개변수에 적용되게 됨.
  
// 대입된 타입 Product 클래스의 자손들을 매개변수로 들어갈 수 있다.
pList.add(new Product());
pList.add(new Tv()); // 가능
pList.add(new Audio()); // 가능
  
// 만약 저 2번째 add된 Tv객체를 get()메서드로 가져올 땐
Tv t = (Tv)list.get(1); // 이렇게 형변환을 해주고 가져오면 된다.

어쨋든
참조변수와 객체 생성자의 원시타입은 달라도 되지만
" 대입된 타입이 일치해야 한다 "는 조건이 있는 것이다.

class Product {}
class Tv extends Product {}
class Audio extends Product {}

public static void main(String[] args) {
    ArrayList<Product> productList = new ArrayList<Product>() ;
    // 만약 <Product>가 아닌 <Tv>나 <Audio>로 대입된 타입이 지정되면
    // 아래 <Product>가 대입된 타입인 printAll 메서드를 사용할 수 없다
    
    productList.add(new Tv());
    productList.add(new Audio());
    
    printAll(productList);
    // 위 생성한 객체의 해시코드와 함께 Tv객체 출력
    // 위 생성한 객체의 해시코드와 함께 Audio객체 출력
}
/*
대입된 타입이 Product인 ArrayList 지네릭 클래스 값을
매개변수로 가지는 메서드
*/    
public static void printAll( ArrayList<Product> list ){
	for ( Product p : list )
    	System.out.println(p) ;
}


💡 「 Iterator<E> 」 와 「 HashMap<K, V>

제네릭스가 적용되어 있는 클래스/인터페이스

Iterator

앞서 제네릭스는
Object 타입 대신 T와 같은 타입 변수를 사용한다고 배웠는데
위와는 달리 Iterator에도 제네릭스가 적용될 수 있다.

// 기존 일반 Iterator 인터페이스
public interface Iterator {
	boolean hasNext();
    Object next();
    void remove();
}
// 제네릭스 적용 Iterator
public interface Iterator<E> { // 타입 변수 <E>
	boolean hasNext();
    E next(); // Object → E
    void remove();
}

이렇게 되면
기존에 형변환을 해서 사용했던 것과는 달리
제네릭스가 적용되었기 때문에
형변환 과정이 필요없다.

...
Iterator it = list.iterator();
while(it.hasNext()) {
	Student s = (Student)it.next() ; // 형변환이 필요했었음
}


ArrayList<Student> list = new ArrayList<Student>() ;
...
Iterator<Student> it = list.iterator(); //<E>에 Student 대입
while(it.hasNext()) {
	Student s = it.next() ; // 지네릭 클래스로 바뀌게 되면 타입이 일치되기 때문에 형변환이 필요없음
}

HashMap

HashMap은 키와 값의 형태로 저장하는 컬렉션 클래스인데
이 경우에는
지정해줘야 할 타입이 2개이다.

이럴 땐,
초반부에 언급했듯이 ,(콤마) 구분자를 통해 2개의 타입 모두 선언해주면 된다.

/*
이렇게 HashMap클래스가 선언되어 있다.
*/
public class HashMap<K, V> extends AbstractMap<K, V> {
	...
    public V get(Object key) {...}
    public V put(K key, V value) {...} // 타입 K, V로
    public V remove(Object key) {...}
    ...
}

// ,를 통해 구분해서 Key 값은 String 타입, Value 값은 Student 타입을 대입한다.
HashMap<String, Student> map = new HashMap<String, Student>() ;
map.put("동규", new Student("동규", 24, 100,100,100)) ;


/*
아래와 같이 적용되어 작업을 수행하게 되는 것이다.
*/
public class HashMap extends AbstractMap<K, V> {
	...
    public Student get(Object key) {...}
    public Student put(String key, Student value) {...} // 타입 K, V로
    public Student remove(Object key) {...}
    ...
}

위 코드에서 put메서드 이외에
get이나 remove의 매개변수는 여전히 Object인 이유는
해당 메서드들의 연산에 hash() 메서드를 사용하는데
hash메서드는
결국 Object 타입 매개변수를 받게끔 되어있기 때문에
바꿔줄 필요가 없다.

0개의 댓글