[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개의 댓글

관련 채용 정보