🎥Back/자바의 정석

[JAVA의 정석]Generics, Generics 클래스

i_zzy 2024. 1. 18. 13:51

이 글은 유튜브 '자바의 정석 - 기초편'을 보고 정리한 글입니다. 

 

 

📂content

1. 제네릭(Generics)란?

- 컴파일시 타입을 체크해 주는 기능(compile-time type check) - JDK1.5

//Tv객체만 저장할 수 있는 ArrayList를 생성
ArrayList<Tv> tvList = new ArrayList<Tv>();

tvList.add(new Tv()); //OK
tvList.add(new Audio()); //컴파일 에러. Tv 외에 다른 타입은 저장 불가

 

- 객체의 타입 안정성을 높이고 형변환의 번거로움을 줄여줌 (하나의 컬렉션에는 대부분 한 종류의 객체만 저장)

제네릭의 장점

1. 타입 안정성을 제공한다. 

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

 

Exception(예외) : RuntimeError(실행 중에 발생하는 에러)
RuntimeException : 프로그래머 실수로 발생하는 에러
ClassCastException : 형변환 에러
NullPointerException : 참조변수 null
IndexOutOfBoundsException : 배열범위를 벗어나는 에러 

RuntimeError보다는 CompileTimeError가 낫다. 프로그램이 실행되기 전에 수정할 수 있기 때문이다. 
여기서 ClassCastException을 CompileTime에 Type정보를 주어서 컴파일러가 체크할 수 있게 만들어준 것이 제네릭이다. 마찬가지로 String str = null; 보다는 Strint str = "";이 좋다. Object[] objArr = null;보다는 Object[] objArr = new Object[0]; 또는 Object[] objArr = {};가 더 좋은 코드이다. 

 

 

⍟실습

더보기
package etc;

import java.util.ArrayList;

public class GenericTest {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add(10);
        list.add(20);
        list.add("30"); //String을 추가

        Integer i = (Integer) list.get(2); //컴파일 OK
        //하지만 실행하면 에러가 난다. 

        System.out.println(list);
    }
}

 

 

컴파일할 때는 괜찮지만, 실제로 실행을 하면 에러가 난다. list에서 꺼낼 때, Object로 꺼내기 때문에 허용을 했지만, 실제로는 String값이 들어가 있다. 

 

package etc;

import java.util.ArrayList;

public class GenericTest {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<Integer>();
        //만약에 여러개 저장하고 싶다면, Object를 사용한다. 하지만 꺼낼 때 형변환을 해야한다. 
//        ArrayList<Object> list = new ArrayList<Object>();
        list.add(10); //list.add(new Integer(10));
        list.add(20);
        list.add(30); //타입 체크가 강화됨. 제네릭 덕분에 


        Integer i = list.get(2); //컴파일 OK. 형변환 생략 가능 
        //하지만 실행하면 에러가 난다.

        System.out.println(list);
    }
}

 

 

제네릭 타입을 써주어야 하는 클래스가 있다. 

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

 

 

 

 

 

 

2. 타입 변수

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

public class ArrayList extends AbstractList { //일부 생략
		private transient Object[] elementData;
        public boolean add(Object o) {/* 내용생략 */}
        public Object get(int index) {/* 내용생략 */}
        ...
  }

 

위 코드가 아래와 같이 바뀌었다. Object대신 E로 바뀌었다. JDK1.5부터 일반클래스가 제네릭클래스로 바뀌었다. 

public class ArrayList<E> extends AbstractList<E> { //일부 생략
		private transient E[] elementData;
        public boolean add(E o) {/* 내용생략 */}
        public E get(int index) {/* 내용생략 */}
        ...
  }

 

E는 Element(배열의 요소)의 약자로 쓰인다. 

 

 

 

 

 

 

3. 타입 변수에 대입하기

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

//타입 변수 E 대신에 실제 타입 Tv를 대입
ArrayList<Tv> tvList = new ArrayList<Tv>();

 

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

전자는 tvList.get(0)하면 반환값이 Object이기 때문에 형변환을 해야 했는데,
후자는 반환값이 tv라서 형변환이 불필요하다. 

 

 

 

 

 

4. 지네릭스 용어

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

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

Box               원시 타입(raw type)

 

 

 

 

 

5.  지네릭 타입과 다형성

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

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

ArrayList<Tv> list = new ArrayList<Tv>(); //OK. 일치
ArrayList<Product> list = new ArrayList<Tv>(); //에러. 불일치
Product와 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());  //OK.
list.add(new Audio));   //OK.
타입변수로 Product가 지정이 되면, Product의 자손이 저장이 될 수 있다. 
boolean add(E e) { ... }  -> boolean add(Product e) { ... } 가 되면서 Product와 그 자손 객체가 다형성에 의해 저장이 되기 때문이다. 
E get(int index) { ... } ->   Product get(int index) { ... }가 된다. 
Product p = list.get(0); 
Tv t = (Tv)list.get(1); 
그래서 형변환이 필요가 없다. 

 

 

 

⍟실습

Ex12_1

더보기
package etc;

import java.util.*;

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

class Ex12_1 {
    public static void main(String[] args) {
        ArrayList<Product> productList = new ArrayList<Product>();
        ArrayList<Tv>      tvList = new ArrayList<Tv>();
//      ArrayList<Product> tvList = new ArrayList<Tv>(); // 에러.
//     List<Tv>           tvList = new ArrayList<Tv>(); // OK. 다형성

        productList.add(new Tv()); //public boolean add(Product e) {} => Product와 그 자손 OK
        productList.add(new Audio());

        tvList.add(new Tv()); //public boolean add(Tv e) {}
        tvList.add(new Tv());
//        tvList.add(new Audio()); //에러

        printAll(productList);
        // printAll(tvList); // 컴파일 에러가 발생한다.
    }

    public static void printAll(ArrayList<Product> list) {
        for (Product p : list)
            System.out.println(p);
    }
}

 

 

 

 

 

6. 지네릭 클래스의 예

1. Iterator<E>

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

Iterator가 일반 클래스일 때, Object로 되어있었다. 근데 지네릭클래스로 바뀌면서 Object가 타입변수로 E로 변함. 
옛날에는 형변환이 필요했다. 그런데 제네릭클래스로 바뀌고 난 후, E에 Student가 대입이 되면서 Student를 반환하게 되면서 형변환을 할 필요가 사라졌다. 

 

 

 

⍟실습

Ex12_2

더보기
import java.util.*;

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);
		}
	} // main
}

class Student {
	String name = "";
	int ban;
	int no;

	Student(String name, int ban, int no) {
		this.name = name;
		this.ban = ban;
		this.no = no;
	}
}

 

 

2. HashMap<K,V>

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

역시 마찬가지로 형변환을 할 필요가 없어졌다. 

 

 

 

⍟실습

더보기
package etc;

import java.util.*;

class Ex12_2 {
    public static void main(String[] args) {
        HashMap<String, Student> map = new HashMap<>(); //jdk 1.7부터 생성자에 타입지정 생략가능
        map.put("자바왕", new Student("자바왕", 1,1,100,100,100));

        //public Student get(Object key) {}
        Student s = map.get("자바왕");


        System.out.println(map);
    } // main
}

class Student {
    String name = "";
    int ban; //반
    int no; //번호
    int kor;
    int eng;
    int math;

    public 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;
    }
}

 

 

 

 

 

7. 제한된 지네릭 클래스

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

class FruitBox<T extends Fruit> { //Fruit의 자손만 타입으로 지정가능
	ArrayList<T> list = new ArrayList<T>();
    ...
}

FruitBox<Apple> appleBox = new FruitBox<Apple>(); //OK
FruitBox<Toy> toyBox = new FruitBox<Toy>(); //에러. Toy는 Fruit의 자손이 아님.

 

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

interface Eatable {}
class FruitBox<T extends Eatavle> { ... }
implements를 쓸 것 같지만 extends를 사용한다. 

 

 

 

⍟실습

Ex12_3

더보기
package etc;

import java.util.ArrayList;

class Fruit implements Eatable {
    public String toString() { return "Fruit";}
}
class Apple extends Fruit { public String toString() { return "Apple";}}
class Grape extends Fruit { public String toString() { return "Grape";}}
class Toy                 { public String toString() { return "Toy"  ;}}

interface Eatable {}

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>();   // 에러.

        fruitBox.add(new Fruit());
        fruitBox.add(new Apple());
        fruitBox.add(new Grape());
        appleBox.add(new Apple());
//		appleBox.add(new Grape());  // 에러. Grape는 Apple의 자손이 아님
        grapeBox.add(new Grape());

        System.out.println("fruitBox-"+fruitBox);
        System.out.println("appleBox-"+appleBox);
        System.out.println("grapeBox-"+grapeBox);
    }  // main
}

class FruitBox<T extends Fruit & Eatable> extends Box<T> {}
//사실 Fruit이 Eatable 인터페이스 상속을 받아서 Eatable을 받을 필요가 없다.
//2개를 상속받을 때 ','를 쓰지 않고 '&'을 쓴다.

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();}
}

 

- 헷갈리면 그림으로 그려보기. 위 코드의 클래스 관계는 아래와 같다. 

 

 

 

 

 

8. 지네릭스의 제약 

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

Box<Apple> appleBox = new Box<Apple>(); //OK. Apple객체만 저장가능
Box<Grape> grapeBox = new Box<Grape>(); //OK. Grape객체만 저장가능

 

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

class Box<T> {
	static T item; //에러
    static int compare(T t1, T t2) { ... } //에러
    ...
static은 모든 인스턴스의 공통이기 때문이다. 

 

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

class Box<T> {
	T[] itemArr; //OK. T타입의 배열을 위한 참조변수
    ...
    T[] toArray() {
    	T[] tmpArr = new T[itemArr.length]; //에러. 지네릭 배열 생성불가
        ...

 

 

 

 

 

출처