ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 자바 - Generics
    언어/JAVA 2023. 3. 16. 12:09

    1. Generics란?

    -데이터타입의 일반화를 의미 (generalize)

    -다양한 타입의 객체를 다루는 메서드나, 컬렉션 클래스에 타입체크 기능

    -컴파일 시 이러한 타입 체크를 통해 타입의 안정성을 제공하고, 타입체크와 형변환 생략으로 코드가 간결해진다.

     

    간단하게 객체의 타입을 미리 명시하므로써 형변환의 번거로움을 줄이고, 잘못된 타입의 기입을 컴파일 과정에서 잡아내어 오류를 줄여준다.

                    

    *generics가 도입되기 이전인 JDK 1.5이전에는 여러타입을 매개변수로 받거나, 반환할 때 Object를 사용했다.

    ->이 경우에 Object타입을 다시 형변환해야하는 번거로움이 있고, 이 과정에서 오류 발생 가능성이 있다. generics는 이를 보완해준다.

     

    * List<T>처럼 인스턴스 필드에 타입 변수 객체 가져야한다고 생각하지말자 

      -> 클래스에 선언된 타입변수는 인스턴스 메서드 반환 값, 매개변수 등 필요한 곳에 사용된다.  

    2. Generics클래스 만들기

    class Box<T>{
        T item;
        void setItem(T item){
            this.item = item;
        }
        T getItem() {return item;}
    }

    위와 같이 지네릭 클래스 box를 정의 할 수 있다. 

     

    2.1 Generics용어 

     

    -지네릭 클래스: Box<T>를 의미 

    -타입변수: T - 타입변수 혹은 타입 매개변수

                         - 이 T에는 아무런 이름이나 지정해도 괜찮다.

                         - 임의의 참조형 타입을 의미한다. 

                         - 여러개의 타입변수는 쉼표로 구분하여 명시할 수 있다.

                         - 위와같은 타입변수는 클래스 뿐 아니라 메소드 매개변수나 반환값으로 사용할 수 있다.

    -원시타입: BOX 

     

    Box<T>클래스의 타입변수를 실제의 특정 타입으로 지정하는 것을 지네릭 타입 호출이라고 부른다. 이때 지정된 타입을 

    매개변수화된 타입이라고 한다.

     

     Box<Integer> b = new Box<Integer>(); 

     

    public static void main(String[] args){
        Box<String> box1 = new Box<>();
        Box<Integer> box2 = new Box<Integer>();
    
        box1.setItem("String box");
        box2.setItem(1);
    
        System.out.println(box1.getItem());
        System.out.println(box2.getItem());
    
    }

    서로 다른 지네릭 타입 호출로 Box<T>를 호출해 보았다. 결과는 각각 String box와 1이다.

    두개의 인스턴스는 서로다른 클래스가 아니다.

    이러한 결과를 보면, generics을 이용은 마치 클래스를 메서드 오버로딩과 같이 사용하는 것과 같다. 

     

    2.2 지네릭스의 제한

     

    -static멤버는 타입변수를 사용할 수 없다. (와일드 카드 아닌, 지네릭 클래스의 타입변수의미)

     

    타입변수 T는 인스턴스 변수로 간주된다. -> static멤버는 인스턴스 멤버를 참조할 수 없다

    static멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 한다. 

                                                                    ->임의의 타입을 의미하는 T는 사용될 수 없다.

    static T item; //에러 
    static int compare(T t1, T t2){....}; //에러 

     

    -지네릭 타입의 배열을 생성하는 것도 허용되지 않는다.

    T[] iArr; // T타입(임의타입)형 배열을 위한 참조변수는 ok
    T[] toArr(){
        T[] tempArr = new T[6]; //지네릭 배열은 생성불가
        return tempArr;
    }

    이는 new연산자 때문이다. new연산자는 컴파일 시점에 타입 T가 무엇인지 정확하게 알아야한다. 

    하지만, Box<T>클래스를 컴파일하는 시점에는 T가 어떤 타입이 될지 전혀 알 수 없다. 따라서 사용불가

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

     

    *지네릭 배열을 생성할 필요가 있다면, ReflectionAPI 혹은 Object배열을 생성해서 T[]로 형변환하는 방법을 생각해보자.

    T[] tempArr = (T[])new Object[6]; //사용가능

     

    2.3 지네릭 클래스 인스턴스의 생성과 사용

     

    -지네릭 객체를 생성할 때 참조변수와 생성자에 대입된 타입은 일치해야한다. 이는 대입된 타입끼리 상속관계에 있어도 마찬가지.

    Box<Object> box3 = new Box<String>(); //다형성 해줄 것 같지만 아니다.

    -지네릭 클래스 사이의 다형성은 허용된다. (단, 대입된 타입은 일치해야함)

    Box<String> box3 = new FBox<String>(); // FBox는 Box의 자손

    -타입변수형 매개변수의 다형성도 허용된다.

    //상속관계가 있는 두 클래스
    class Fruit{}
    class Apple extends Fruit {}
    
    //Box에 add메서드 타입변수를 매개변수로 선언했다. 
    void add(T item){...}
    
     Box<Fruit> ffbox = new Box<>(); // Fruit형으로 선언하면 -->void add(Fruit item)이다.
     ffbox.add(new Apple()); //다형성이 허용된다.

     

    -즉 제네릭 타입은 상하관계가 없다. 

     

    2.4 제한된 지네릭 클래스 만들기 ( extends 의 사용 )

     

    타입 변수를 사용하여 지네릭 클래스를 정의하면, 임의의 타입으로 클래스를 정의하도록 하여

    -실제 인스턴스를 생성하여 사용할 때에는 한가지 타입으로만 지정하도록 제한되는 것은 맞지만,

    -처음 대입시에는 모든 타입을 지정할 수 있다는 점이 남는다.

    -그렇다면 처음 타입변수 T에  대입할 때 부터 타입을 제한할 수 있는 방법을 알아보자 

     

    class FBox<T extends  Fruit & Eatable> extends Box<T>{...Box와 모두 동일}
    
    interface Eatable {//빈 인터페이스이다}

    타입변수에 extends를 사용하면 특정 타입의 자손만 대입 할 수 있게 제한된다.  

    인터페이스를 구현해야한다는 제약을 둘 때도 extends를 사용한다.

    만약 특정클래스의 자손이면서, 인터페이스도 구현해야한다면 &로 이어준다.

     

    3. 완성된 Generic 클래스의 활용

     

    3.1 와일드 카드 

     

    위에서 다루었듯이 제네릭 타입은 상하관계가 없다. 이러한 문제로 아래와 같은 문제점이 있다.

    static void print(Box<Fruit> box){}

    fruit형 box의 인스턴스를 가져와서 적절히 print해주는 메서드가 있다고 가정해보자. 

    위 메서드에는 두 가지사항이 걸린다.

     - static에는 타입변수 T를 사용할 수 없으므로 제네릭을 사용하지 말던가 위와 같이 특정 타입이 대입된 제네릭 클래스를 사용해야함

    - 위와 같이 특정한 제네릭 클래스를 대입하면, Fruit형 box인스턴스 외에는 print에 대입될 수 없고, 제네릭 타입이 다른 것 만으로는 오버로딩이 불가 --> Box<apple> box로 print만들기 불가

     

    그렇다고 FruitPrint , ApplePrint등등 제네릭 타입에 따라 print메서드를 따로따로 만드는 것도 웃기다. 

    위와같은 문제를 해결하기 위해 와일드카드<?>가 고안되었다.

     

    와일드카드 ?은 어떠한 지네릭 타입도 허용한다는 의미이지만, (그렇다고 any type은 아니고 unknown type에 가깝다.)

     

    - <?> 제한 없음. 모든 타입 가능 

    -<? super T>  T와 그 조상들만 가능 와일드 카드 하한.

    -<? extends T> T와 그 자손들만 가능 와일드 카드 상한.

     

    static void print(FBox<? extends Object> fbox){
    	Fruit ff = fbox.getItem(); // 에러 Fruit이 아닐 수 있다.
    }
    
    static void print(FBox<? extends Fruit> fbox){
    	Fruit ff = fbox.getItem(); // Fruit과 그 조상만 대입되므로 이건 괜찮다.
    }

    위와 같이 변경하면, 모든 제네릭 타입의  FBox<>가 대입 가능하지만, 어떤 타입으로 선언된 FBox<T extends  Fruit & Eatable>일지 모르므로 위와같이 Fruit 참조변수에 함부로 값을 담을 수 없다. 

     

    *와일드 카드는 지네릭클래스를 만들 때 사용하는 것이 아니라, 이미 만들어진 지네릭 클래스를 활용할 때 사용하는 것임을 명심하자.

     

     

    3.2 Generic 메서드

     

    메서드 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라 한다. 지네릭 타입의 선언 위치는 반환 타입 바로 앞이다.

    static <T> void sort(List<T> list, Comparator<? super T> c); //지네릭 메서드 예시

    ***지네릭 클래스에 정의된 타입 매개변수와 지네릭 메서드에 정의된 타입 매개변수는 전~혀 다른 것이다.

    ***앞서 계속 나온 것 처럼 static 멤버에는 타입 매개변수는 사용할 수 없지만, 메서드에 지네릭 타입을 선언하고 사용하는 것은 가능하다. 

     

    -위와 같은 메서드에 선언된 지네릭 타입은 지역 변수를 선언한 것과 같다.

    -이 타입 매개변수는 메서드 내에서만 지역적으로 사용될 것이므로 메서드가 static이냐 아니냐는 중요하지 않다.

     

    static <T extends Fruit> void print(Box<T> box){
        System.out.println(box.toString());
    }
    //////////////////////위는 아래의 수정//////////////
    static void print(FBox<? extends Object> fbox)
    ////////////////////////////실제 사용/////////////
    
    //static 메서드이므로 클래스명.<지네릭 메서드 타입변수>메서드명(매개변수)
    ???클래스명???.<Fruit>print(ffbox)

    메서드 호출 시에 지네릭 타입을 적는 것은 생략될 수 있다. ->선언된 지네릭 인스턴스의 지네릭타입을 보고 알아서 유추

     

    *한가지 주의점은 지네릭 메서드를 호출할 때, 메서드에 대입된 타입을 생략할 수 없다면, 참조변수나 클래스 이름을 생략할 수 없다.

    ->즉 원래 클래스 내에 있는 멤버끼리는 참조변수명이나 클래스명을 생략하고 메서드 이름만으로 호출이 가능하지만, 

    대입된 타입<Fruit>등이 있을 때는 클래스명이나 참조변수명 생략할 수 없다.

     

    와일드 카드와 마찬가지로 이미 완성된 지네릭 클래스를 활용할 때 와일드 카드와 함께 사용할 수 있는 하나의 방식일 뿐이다.

    어렵게 생각할 것 없다.

     

    4. Generic 타입의 제거

     

    컴파일러는 지네릭 타입을 이용해 소스파일(.java)를 체크하고 필요한 곳에 형변환을 넣어준 뒤 지네릭 타입을 제거한다.

    즉 컴파일이 완료된 바이트코드(.class)에는 지네릭 타입에 대한 정보가 없다.

     

    *위와 같은 이유는 지네릭 도입 이전에 소스 코드와의 호환성을 유지하기 위해서 이다.

     

    간단하게 지네릭 타입의 제거과정을 보면 다음과 같다.

     

    1. 지네릭 타입의 경계를 제거한다.

    만약 <T extends Fruit>이라면 T를 Fruit으로 치환한다. <T>인 경우에는 Object로 치환한다. 그리고 클래스 옆의 선언은 제거

     

    2. 지네릭 타입을 제거한 후 타입이 일치하지 않으면, 형변환을 추가한다.

    T get(int i){
    return list.get(i);
    }
    
    Fruit get (int i){
    	return (Fruit)list.get(i)
    }

    ->컴파일 후 T가 Fruit이라면, List의 get()은 Object를 변환하므로 형변환 필요 

     

    와일드 카드가 포함되어 있어도 적절히 형변환을 추가한다.

    static void print(FBox<? extends Fruit> fbox){
    	Fruit ff = fbox.getItem(); 
    }
    
    static void print(FBox fbox){
    	Fruit ff = (Fruit)fbox.getItem(); .
    }

     

    ----------------------------------------------------------------------------------------------------------

    *제한된 지네릭 <T extends Class>와 와일드 카드<? extends Class>

      공통점: 타입변수의 범위를 제한 

      차이점:

     

     제한된 지네릭: 타입변수(파라미터)를 제한하는 것

                              - 지네릭 클래스,메서드 선언 시

                              - 매개변수, 리턴타입으로 타입변수 사용시 

                              -  지네릭 클래스 없이 <T extends ~> 등으로 사용가능

      와일드카드: 특정 지네릭클래스의 타입변수를 제한하는 것

                           메서드의 리턴값, 매개변수로  지네릭 클래스가 사용될 때 

                           - 지네릭 클래스<? extends ~> 등의 형식으로만 사용가능

    -------------------------------------------------------------------------------------------------------------------

    * <T extends Comparable<? super T>>

    - T 는 Comparable 구현 클래스이며, 구현 Comparable 클래스는 T 또는 그 조상 타입 비교하는 Comparable이어야함  

    - 만일 T 가 Student이고 Person의 자손이라면 

    - T는 Comparable<Student>,  Comparable<Person> , Comparable<Object> 셋 중 하나 가능 

    -------------------------------------------------------------------------------------------------------------------

    참고자료

     

    -자바의 정석

    -https://inpa.tistory.com/

     

    Inpa Dev 👨‍💻

    성장 욕구가 가파른 초보 개발자로서 공부한 내용을 쉽게 풀어쓴 기술 개발자 블로그를 운영하고 있습니다.

    inpa.tistory.com

    -https://sujl95.tistory.com/73

     

    14주차 과제: 제네릭

    14주차 과제: 제네릭 시작하기 전에 제네릭이 무엇이고 제네릭을 왜 사용해야하는지 알아보자 제네릭이란? 데이터 타입(data type)을 일반화(generalize)하는것을 의미한다 제네릭은 클래스나 메소드

    sujl95.tistory.com

     

    '언어 > JAVA' 카테고리의 다른 글

    자바 - Arrays 클래스와 Comparator  (0) 2023.03.23
    자바 - Thread(4) 동기화  (0) 2023.03.21
    자바 - thread(3) 실행제어 메서드 예제  (0) 2023.03.21
    자바 -thread(2) [5~8]  (0) 2023.03.20
    자바 - thread(1) 기본특성 (1~4)  (0) 2023.03.20
Designed by Tistory.