🏃♂️ 들어가기 앞서..
본 게시물은 스터디 활동 중에 작성한 게시물로 자바의 정석-기초편 교재를 학습하여 정리하는 글입니다.
※ 스터디 Page : 〔투 비 마스터 : 자바〕
*해당 교재의 목차 순서와 구성을 참고하여 작성하며
각 내용마다 부족할 수 있는 내용이나 개인적으로 궁금한 점은
추가적인 검색을 통해 채워나갈 예정입니다.
★ "지네릭스" 와 "제네릭스" 발음 혼동주의....쓰다보니 계속 번갈아가면서 쓰게 되서..
Generics
)< 다양한 타입의 객체들을 다루는 메서드 / 컬렉션 클래스 >의
" 컴파일 시 "에 타입 체크(compile-time type check)를 해주는 기능
객체들의 타입을 컴파일할 때 타입 체크를 해주게 되면
객체의 ' 타입 안정성 '을 높이고
' 형변환 의 번거로움 '을 줄일 수 있다.
(▶ 간결한 코드 작성 )
// 특정 클래스, Tv클래스 타입의 객체만 저장할 수 있는 ArrayList를 생성
// 참조변수 & 생성자에 <>를 통해 타입 지정
ArrayList<Tv> tvList = new ArrayList<Tv>() ;
tvList.add(new Tv()) ; // 가능
tvList.add(new Audio()) ; // 컴파일 에러
위처럼
본래 ArrayList
는 Object로 다양한 종류의 객체를 담을 수 있지만
보통 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 타입 매개변수를 받게끔 되어있기 때문에
바꿔줄 필요가 없다.