[이것이자바다] chapter 13. 제네릭(Generic)

chapter13

제네릭(Generic)

왜 제네릭을 사용해야 하는가?

제네릭의 이점

  1. 컴파일 시 강한 타입 체크를 할 수 있다. => 제네릭 코드는 강한 타입 체크를 해야한다.잘못된 타입 때문에 런타임때 타입 에러가 나는 것보다 컴파일시에 미리 타입을 강하게 체크하여 에러를 방지하는 것이 좋다.
  2. 타입 변환(Casting)을 제거한다. => 타입 변환(Casting)은 프로그램 성능에 악영향을 미치는데, 제네릭 코드로 수정하면 타입 변환을 할 필요가 없어 프로그램 성능이 향상된다.

비제네릭 코드

List list = new ArrayList();
list.add("hello");
String str = (String)list.get(0); //타입 변환을 해야한다.

제네릭 코드

List<String> list = new ArrayList<String>();
list.add("hello");
String str = list.get(0); // 타입 변환을 하지 않는다.

제네릭 타입( class<T>, interface<T>)

public class 클래스명<T> {...}
public interface 인터페이스명<T> {...}
public class Box {
    private  Object object;
 
    public void set(Object object)
    {this.object = object;}
    
    public Object get()
    {return object;}
}

=> Object 타입으로 선언한 이유는 필드에 모든 종류의 객체를 저장하고 싶어서이다.(Object클래스가 최상위 부모 클래스이므로) 따라서 자식 개체는 부모 타입에 대입될 수 있다는 성질 때문에 set()메소드를 사용하는 경우에는 Promotion(자동 형변환)이 자동적으로 이루어진다. 하지만 get() 메소드에서 반환하여 필드에 저장된 원래의 타입으로 변환되려면 Casting을 해야한다. (Casting은 좋지 않다고 위에 언급했다, Casting은 번거롭다.)

Box box = new Box();
        box.set("hello");
        String str = (String) box.get(); // 이와 같이 강제 형변환을 해주어야한다. -> 좋지 않다.

=> Casting은 사용하였는데 이는 번거롭고 성능에 악영향을 끼치는 코드이므로 옳지 않다.
=> 그렇다면 모든 종류의 객체를 저장하면서 Casting이 발생하지 않도록 하는 방법이 없을까> 해결책은 제네릭에 있다. 다음은 제네릭을 이용하여 Box 클래스를 수정한 것이다.

//T는 타입 파라미터. 존나 멋있다. 진짜.
public class Box<T> {
    private T t;
    public T get() {return t;}
    public void set(T t)
    {this.t =t;}
}

=> 타입 파라미터 T를 사용해서 Object 타입을 모두 T로 대체했다. T는 Box 클래스로 객체를 생성할 때 다음과 같이 구체적인 타입으로 변경된다.

Box<String> box = new Box<String>(); // box의 필드 t의 타입이 구쳊적인 타입인 String으로 변경되었다.

=> 위와 같이 타입 파라미터 T가 String으로 변경되면 Box 클래스의 내부는 다음과 같이 자동으로 재구성된다.

public class Box<String> {
   private String t;
   public String get(){return t;}
   public void set(String t)
   {this.t = t;}

}

이와 같이 재구성되어서 다음과 같이 Casting을 전혀 하지 않아도 된다. 제네릭의 장점 2다.

Box<String> box = new Box<String>();
box.set("Hello");
String str = box.get();

정리:

  1. 제네릭은 강한 타입 체크를 해서 클래스의 타입을 통일시켜 헷갈리지 않게 하여 런타임 시 에러가 안나도록 한다. 또 타입이 틀리면 컴파일 오류가 뜨게끔하여 오류를 잡아준다.
  2. 제네릭은 타입 변환(Casting)을 제거하여 성능을 좋게 해주고, 번거롭지 않게 해준다.

멀티 타입 파라미터(class<K,V…>,interface<K,V>)

제네릭 클래스

public class Product<T,M> {
    private T kind;
    private M model;

    public T getKind(){return kind;}
    public M getModel(){return model;}

    public void setKind(T kind) {
        this.kind = kind;}
    public void setModel(M model){
        this.model = model;
    }

}

제네릭 객체 생성

public class ProductExample {
    public static void main(String[] args) {
        Product <Tv,String > product1 = new Product<>();
        // <>를 다이아몬드 연산자라 부른다.
        product1.setKind(new Tv());
        product1.setModel("스마트 tv");

        Product<Car,String> product2 = new Product<>();
        product2.setKind(new Car());
        product2.setModel("스마트 Car");

        System.out.println(product1.getKind());
        System.out.println(product2.getKind());
        System.out.println(product1.getModel());
        System.out.println(product2.getModel());
    }
}

제네릭 메소드(<T,R> R method(T t))

public <타입파라미터, ...> 리턴타입 메소드명(매개변수,...) {...}
public <T> Box<T> boxing(T t) {...}
Box<Integer> box = <Integer>boxing(100);
Box<Integer> box = boxing(100); // 이 방법을 쓰도록하자. 어차피 컴파일러가 우리보다 똑똑하다. 

제네릭 메소드

public static  <K,V> boolean compare(Pair<K,V> p1, Pair<K,V> p2){
          boolean keyCompare = p1.getKey().equals(p2.getKey());
          boolean valueCompare = p1.getValue().equals(p2.getValue());

          return  keyCompare && valueCompare;
    }

제네릭 타입

public class Pair<K,V> {
    private K key;
    private V value;

    public Pair(K key, V value){
        this.key = key;
        this.value = value;
    }

    public void setKey(K key){
        this.key = key;
    }
    public void setValue(V value){
        this.value = value;
    }
    public K getKey(){
        return key;
    }
    public V getValue(){
        return  value;
    }
}

제네릭 메소드 호출

public class PairExample {

    public static void main(String[] args) {
        Pair <String,Integer> pair1 = new Pair<>("hello",20);
        Pair <String,Integer> pair2 = new Pair<>("hello",20);
        boolean result = Util.compare(pair1,pair2);


        if(result){
            System.out.println("논리적으로 동등한 객체입니다.");

        }
        else{
            System.out.println("논리적으로 동등하지 않은 객체입니다.");
        }
    }

}

제한된 타입 파라미터(<T extends 최상위타입>)

=> 타입 파라미터에 지정되는 구체적인 타입을 제한할 필요가 종종 있다. 예를 들어 숫자를 연산하는 제네릭 메소드는 매개값으로 Number 타입 또는 하위 클래스 타입(Byte, Short, Integer, Long, Double)의 인스턴스만 가져야 한다. String의 인스턴스를 가져다간 NumberFormatExeption이 일어난다. 따라서 이런 경우 구체적인 타입을 숫자 타입으로만 제한해야 하는데 이때 제한된 타입 파라미터를 사용한다.

제네릭 메소드

static  <T extends Number> int numberCompare(T t1, T t2){
        Double a = t1.doubleValue();
        Double b = t2.doubleValue();

        return Double.compare(a,b);
    }

제네릭 메소드 호출

public class BoundedTypeExample {
    public static void main(String[] args) {
        //String str = Util.numberCompare("a","b");
        // String은 Number 타입이 아니라서 적용 불가능하다.

        Integer a = -1;
        Integer b =3;
        int result=Util.numberCompare(a,b);
        // a,b 중 a(앞에꺼)가 더 크다면 1
        if (result ==1 ){
            System.out.printf("%d가 %d보다 큽니다.\n",a,b);
        }
        // a와 b가 같다면 0
        else if(result == 0){
            System.out.printf("%d와 %d가 같습니다.\n",a,b);
        }
        // b(뒤에꺼)가 a보다 크다면 -1
        else{
            System.out.printf("%d가 %d보다 작습니다.\n",a,b);
        }
    }
}

와일드카드 타입(<?>,<? extends…>, <? super …>)

=> 코드에서 ?를 일반적으로 와일드카드(wildCard) 라고 부른다.

제네릭 타입


public class Course<T> {
    private String name;
    private T[] students;

    public Course(String name , int capacity) {
        this.name = name;

        students = (T[]) (new Object[capacity]);
        // 타입 파라미터로 배열을 생성하려면
        // new T[n] 형태로 배열을 생성할 수 없고
        // (T[])(new Object[capacity])으로 생성해야 한다.
    }
        public String getName(){
            return name;
        }


        public void add(T t){
           for(int i=0; i<students.length;i++){
               if(students[i] == null){  // 배열의 요소가 비워있으면 add하여 추가하기
                   students[i] = t;
                   break;
               }
           }
        }
}

수강생이 될 수 있는 클래스는 아래의 표와 같다고 하자.

왼쪽 자식 부모 클래스 오른쪽 자식
  Person  
Worker   Student
    HighStudent

=> 제일 위의 부모 클래스는 Person 클래스이고, Person 클래스의 하위 클래스로 Worker 와 Student 클래스가 있고 , Student 클래스의 하위 클래스로 HighStudent 클래스 가 있다.
=> 위의 내용을 적용해보면,

제네릭 타입

public class Course<T> {
    private String name;
    private T[] students;

    public Course(String name , int capacity) {
        this.name = name;

        students = (T[]) (new Object[capacity]);
        // 타입 파라미터로 배열을 생성하려면
        // new T[n] 형태로 배열을 생성할 수 없고
        // (T[])(new Object[capacity])으로 생성해야 한다.
    }
        public String getName(){
            return name;
        }
        public T[] getStudents(){
           return  students;
        }

        public void add(T t){
           for(int i=0; i<students.length;i++){
               if(students[i] == null){  // 배열의 요소가 비워있으면 add하여 추가하기
                   students[i] = t;
                   break;
               }
           }
        }
}

와일드카드 타입 매개 변수


import java.util.Arrays;

public class WildCardExample {
   // Person, Worker, Student, HighStudent
   public static void registerCourse( Course<?> course){
       System.out.println(course.getName() + " 수강생: " + Arrays.toString(course.getStudents()));
   }
   // Student, High Student
   public static void registerCourseStudent( Course<? extends Student> course){
       System.out.println(course.getName() + " 수강생: " + Arrays.toString(course.getStudents()));
   }

   // Worker, Person
   public static void registerCourseWorker( Course<? super Worker> course){
       System.out.println(course.getName() + " 수강생: " + Arrays.toString(course.getStudents()));
   }


    public static void main(String[] args) {
        Course <Person> personCourse = new Course<>("일반인과정",5);
        personCourse.add(new Person());
        personCourse.add(new Worker());
        personCourse.add(new Student());
        personCourse.add(new HighStudent());

        Course <Worker> workerCourse = new Course<>("직장인과정", 5);
        workerCourse.add(new Worker());

        Course <Student> studentCourse = new Course<>("학생과정", 5);
        studentCourse.add(new Student());
        studentCourse.add(new HighStudent());

        Course <HighStudent> highStudentCourse = new Course<>("고등학생과정",5);
        highStudentCourse.add(new HighStudent());
        
        // Course<?>
        registerCourse(personCourse);
        registerCourse(workerCourse);
        registerCourse(studentCourse);
        registerCourse(highStudentCourse);
        
        // Course<? extends...>
        //registerCourseStudent(personCourse); (x)
        //registerCourseStudent(workerCourse); (x)
        registerCourseStudent(studentCourse);
        registerCourseStudent(highStudentCourse);
        
        // Course<? super ...>
        //registerCourseWorker(studentCourse); (x)
        //registerCourseWorker(highStudentCourse); (x)
        registerCourseWorker(personCourse);
        registerCourseWorker(workerCourse);
    }
}

제네릭 타입의 상속과 구현

public class ChildProduct<T,M> extends Product<T,M> {...}

public class ChildProduct<T,M,C> extends Product<T,M> {...}

=> 자식 제네릭 타입은 추가적으로 타입 파라미터를 가질 수 있다. 다음은 세 가지 타입 파라미터를 가진 자식 제네릭 타입은 선언한 것이다.

부모 제네릭 클래스

package Generic;

class Product<T,M> {
    private T kind;
    private M model;

    T getKind(){return this.kind;}
    M getModel(){return this.model;}

    void setKind(T kind) {
        this.kind = kind;}
    void setModel(M model){
        this.model = model;
    }

}

자식 제네릭 클래스

public class ChildProduct<T,M,C> extends Product<T,M>{

    private C company;

    public C getCompany() {
        return this.company;
    }

    public void setCompany(C company){
        this.company = company;
    }
}

제네릭 인터페이스

package Generic;

public interface Storage<T> {
    public void add(T item, int index);  // 추상 클래스
    public T get(int index); // 추상 클래스
}

제네릭 구현 클래스

package Generic;

public class StorageImpl<T> implements Storage<T> {
    private T[] array;

    public StorageImpl(int capacity){
        this.array = (T[])(new Object[capacity]); // 배열 생성
        // 타입 파라미터로 배열을 생성하려면 new T[capacity]로 할 수 없고,
        // new Object[capacity]로 선언한 뒤 T[]로 Casting 해야한다.
    }

    @Override
    public void add(T item, int index) {
        array[index] = item;
    }

    @Override
    public T get(int index) {
        return array[index];
    }
}

ChildProduct 클래스와 Storage 인터페이스의 사용 예

package Generic;

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

        //kind, model, company
        ChildProduct<String,String,String> childProduct = new ChildProduct<>();
        childProduct.setKind("cup");
        childProduct.setModel("mug");
        childProduct.setCompany("starbucks");
        Storage<String> storage = new StorageImpl<>(5);
        storage.add("sunglasses", 0);
        storage.add("cellphone", 1);

        System.out.println(childProduct.getKind());
        System.out.println(childProduct.getModel());
        System.out.println(childProduct.getCompany());
        System.out.println(storage.get(0));
        System.out.println(storage.get(1));

    }
}