본문 바로가기
멀티캠퍼스 풀스택 과정/Java의 정석

자바의 정석9-1 지네릭스

by 이쟝 2022. 1. 12.

지네릭스(Generics)?

- 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일시 타입을 체크해 주는 기능(compile-time type check) – JDK1.5

 

 

- 다룰 객체의 타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다.

- 모든 클래스가 Generic클래스로 바뀐 것은 아니고, 클래스 안에 Object 타입이 있는 것만 Generic클래스로 바뀜!(Object는 다양한 타입의 객체들을 다룰 수 있기 때문에)

 

) ArrayList(일반 클래스) -> ArrayList<E>(지네릭 클래스)

(ArrayListObject 배열을 가지고 있어서 모든 종류의 객체 저장 가능)

 

지네릭스의 장점

1) 타입 안정성을 제공한다.(다를 객체의 타입을 미리 명시해줌으로써)

2) 타입체크와 형변환을 생략할 수 있으므로 코드가 간결 해진다.

-> 지네릭스를 추가함으로써 ClassCastException 예외를 막아준다.


예제1) 컴파일은 가능하지만 Runtime시 발생에러

 

ArrayList list = new ArrayList();

		list.add(10);
		list.add(20);
		list.add("30");
		
		Integer i = (Integer)list.get(2);  // 컴파일 OK 
		
		System.out.println(list);  // ClassCastException "형변환에러" -> 실행시 발생에러

 

더보기
더보기

Exception: Runtime Error 실행 중 발생 에러

RuntimeException 프로그래머 실수로 발생 에러

실행 중에 발생하는 에러보다 컴파일 할 때 나는 에러(complie time 에러)가 더 나음(프로그램 실행 전에 수정 가능하기 때문이다!)

-> RuntimeException을 가능하면 compile time에러로 끌어오기!

 

1 String str1 = null; 2 String str2 = “ “; 빈문자열
1번보다 2번이 더 좋음
ex) str1.length( ) -> NullPointerException 발생 / str2.length( ) -> 0반환
1 Object [ ] obj = null; 2 Object[ ] obj = new Object[0];
1번보다 2번이 더 좋음

 

-> 실행 중에 NullPointerException이 덜 생기게 하기 위해서! 이렇게 코드를 작성!

 

예제2) 지네릭스 추가

 

// ArrayList list = new ArrayList();  // JDK1.5이전. 지네릭스 도입이전
ArrayList<Integer> list = new ArrayList<Integer>(); // JDK1.5이후
		
		list.add(10);    // list.add(new Integer(10));
		list.add(20);
		list.add(30);
//		list.add("30");  에러 Integer 형으로 선언해서 문자열 xx, 타입체크가 강화됨. 지네릭스 덕분에
						
		Integer i = list.get(0);  // 형변환 생략 가능, 앞에서 지네릭스로 Integer 선언해서!
		
		System.out.println(i);    // 10
		System.out.println(list); // [10,20,30]

 

지네릭스 용어

 

Box<T> 지네릭 클래스(타입 변수 T선언) ‘T Box’ 또는 ‘T Box’라고 읽는다.
T 타입 변수 또는 타입 매개변수(T는 타입 문자) 
새로운 객체를 만들 때마다 새로운 타입을 줄 수 있음.
Box 원시 타입(raw type), 일반 클래스일 때의 타입

 

지네릭 클래스의 선언

 

 


 

타입 변수

- 지네릭 클래스를 작성할 때, Object타입 대신 타입 변수(E)를 선언해서 사용

 

ArrayList의 일반 클래스 구조

 

ArrayList의 지네릭 클래스 구조

 

-> 타입변수 E(Element)를 선언하고 Object를 다 E로 변환!, T로 써도 가능!


타입 변수에 대입하기

- 객체 생성시. 타입 변수(E) 대신 실제 타입(Tv)을 지정(대입)

// 타입 변수 E 대신에 실제 타입 Tv를 대입

 

실제 타입(Tv)를 대입! - 타입 변수 대신 실제 타입이 지정되면, 형변환 생략가능

 

 


지네릭 타입과 다형성

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

 

 

- 지네릭 클래스간의 다형성은 성립(여전히 대입된 타입은 일치해야)

 

 

- 매개변수의 다형성도 성립

 

 

-> TvAudioProduct의 자손이어서(자손의 객체도) 가능!

Product p = list.get(0);  -> 반환타입 Product 타입이 일치해서 형변환 불필요

Tv t = (Tv)list.get(1);    -> 반환타입이 Product 이기 때문에 Tv는 형변환 해야함

 


예제)

 

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

public class Ex12_1 {

	public static void main(String[] args) {
		ArrayList<Product> productlist = new ArrayList<Product>();
		ArrayList<Tv>      tvlist = new ArrayList<Tv>();
//		List<Tv>           tvlist = new ArryaList<Tv>(); // Ok. 다형성
//		ArrayList<Product> tvlist = new ArrayList<Tv>(); // 에러 타입 불일치

		productlist.add(new Tv());     // public boolean add(Product e) { 
		productlist.add(new Audio());  // Product와 그 자손도 ok
		
		tvlist.add(new Tv());    // public boolean add(Tv e) {
//		tvlist.add(new Audio()); // 에러 Tv, 또는 그 자손만 ok
		
		printAll(productlist);
//		printAll(tvlist);  // 컴파일 에러 발생 printAll의 타입이 Product이기 때문
	}
	
	public static void printAll(ArrayList<Product> list) {
		for(Product p:list)
			System.out.println(p);
	}
}

Iterator <E>

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

 

Iterator 일반 클래스 Iterator 제네릭 클래스
public interface Iterator {
    Boolean hasNext( );
    Object next( );
    void  remove( );
}
public interface Iterator<E> {
     Boolean hasNext( );
     E next( );
     void remove( );
}
Iterator it = list.iterator( );
while(it.hasNext( )) {
    Student s = (Student)it.next( );
}
Iterator<Student> it = list.iterator( );
while(it.hasNext( )) {
    Student s = (Student)it.next( );
}

 

예제1) 

 

import java.util.*;
class Student {
	String name = "";
	int ban;
	int no;
	Student(String name, int ban, int no) {
		this.name = name;
		this.ban = ban; 
		this.no = no;
	}
}

public class Ex12_2 {
	public static void main(String[] args) {

		ArrayList<Student> list = new ArrayList<Student>();
		list.add(new Student("자바왕", 1, 1)); 
		list.add(new Student("자바짱", 1, 2));
		list.add(new Student("김영수", 2, 1));
		
		Iterator<Student> it = list.iterator();
		while(it.hasNext()) {
//			Student s = (Student)it.next();  // 지네릭스를 사용하지 않으면 형변환 필요.
//			Student s = it.next(); 
//			System.out.println(s.name);
			System.out.println(it.next().name); // 위에 두줄을 한 줄로 줄일 수 있다.
		}
	}
}

 

HashMap<K,V>

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

- Kkey Vvalue

 

HashMap의 지네릭 클래스

 

-&gt; HashMap의 key 타입이 String, value 타입이 student

 

예제2) 예제1번을 약간 변환

 

import java.util.*;
class Student {
	String name = "";
	int ban;
	int no;
	int kor; 
	int eng;
	int math;
	
	Student(String name, int ban, int no, int kor, int eng, int math) {
		this.name = name;
		this.ban = ban; 
		this.no = no;
		this.kor = kor;
		this.eng = eng;
		this.math = math;	
	}
}

public class Ex12_2 {

	public static void main(String[] args) {

		HashMap<String, Student> map = new HashMap<>();  // JDK1.7부터 생성자에 타입지정 생략가능
		map.put("자바왕", new Student("자바왕", 1, 1, 80, 100, 100));
		
//		Student s = (Studnet)map.get("자바왕"); 
		Student s = map.get("자바왕");
		System.out.println(s.kor);  // 80 
	
	}
}

 


제한된 지네릭 클래스

- extends로 대입할 수 있는 타입을 제한

 

모든 타입 가능 -&gt; Fruit(포함)의 자손만 대입 가능

 

 

- 인터페이스인 경우에도 extends를 사용

 

 

예제)

 

import java.util.ArrayList;

interface Eatable {}
class Fruit implements Eatable {  // Eatable인터페이스를 구현한 Fruit 클래스
	public String toString() { return "Fruit"; }
}
class Apple extends Fruit { public String toString() {return "Apple";}} // Fruit클래스의 자손
class Grape extends Fruit { public String toString() {return "Grape";}} // Fruit클래스의 자손
class Toy { public String toString() { return "Toy"; }}

//(1) Fruit의 자손 & Eatable을 구현한 클래스만, Box의 자손 클래스
class FruitBox<T extends Fruit & Eatable> extends Box<T> {} 

class Box<T> {
	ArrayList<T> list = new ArrayList<T>();    // item을 저장할 list
	void add(T item) { list.add(item); }       // 박스에 item을 추가
	T get(int i)     { return list.get(i); }   // 박스에서 item을 꺼낼때
	int size()       { return list.size(); }
	public String toString() { return list.toString(); } 
}
public class Ex12_3 {

	public static void main(String[] args) {
		FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
		FruitBox<Apple> appleBox = new FruitBox<Apple>();
		FruitBox<Grape> grapeBox = new FruitBox<Grape>();
//		FruitBox<Grape> grapeBox = new FruitBox<Apple>();  // 에러. 타입 불일치
//		FruitBox<Toy>   toyBox = new FruitBox<Toy>();      // 에러 Fruit의 자손이 아님
		Box<Toy>        toyBox = new Box<Toy>();           // Box에서는 가능함(Box는 제약이 없음)

		fruitBox.add(new Fruit());
		fruitBox.add(new Apple());
		fruitBox.add(new Grape());
		appleBox.add(new Apple());
		grapeBox.add(new Grape());
		toyBox.add(new Toy());
//		appleBox.add(new Fruit());  // 에러. Fruit은 apple의 조상, xx
//		appleBox.add(new Grape());  // 에러. Grape는 apple의 자손 xx
		
		System.out.println(fruitBox); // [Fruit, Apple, Grape]
		System.out.println(appleBox); // [Apple]
		System.out.println(grapeBox); // [Grape]
		System.out.println(toyBox);   // [Toy]
		
	}
}

 

더보기
더보기

(1)

Fruit이 자손이면서 Eatable 인터페이스를 구현한 클래스만 들어올 수 있음, 인터페이스 쓸 때는 &(사실 Fruit이 이미 Eatable을 구현하고 있어서 생략가능)

-> class FruitBox<T extends Fruit> extends Box<T> { }

 


지네릭스의 제약

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

 

 

1) static 멤버에 타입 변수 사용 불가

 

 

-> 모든 객체에 대해 동일하게 동작해야하는 static 멤버에 타입 변수 T를 사용할 수 없다. T는 인스턴스변수로 간주되기  때문.(static멤버는 인스턴스변수를 참조할 수 없음) 

-> static멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일해야 하기 때문

 

2) 배열 생성할 때 타입 변수 사용불가. 타입 변수로 배열 선언은 가능

 

 

-> 객체 생성이나, 배열 생성할 때 new T( ), new T[ ] 이렇게 사용할 수 없음

-> new 연산자는 컴파일 시점에 타입 T를 정확히 알아야 하기 때문에 new(연산자) + T 는 안된다.

-> instanceof연산자도 new 연산자와 같은 이유로 T를 피연산자로 사용할 수 없다.