관리 메뉴

제뉴어리의 모든것

Chapter 11. 제네릭스 본문

JAVA/자바의정석

Chapter 11. 제네릭스

제뉴어리맨 2022. 7. 13. 00:28

제네릭스란?

클래스 내부적으로 쓰이는 데이터타입에 대하여 클래스 정의시에 특정타입을 지정하지 않고,

클래스 또는 메소드 사용시에 타입을 지정하는 기능.

(클래스 객체 생성시, 메소드 호출시)

 

컴파일 시점에, 지정한 타입 변수와 실제로 넘어오는 데이터의 타입을 체크하여 잘못된 경우 에러발생 시킴.

 

제네릭스의 기본 사용 예

package ch11.practice;

class Driver {
    String name;

    public Driver(String name) {
        this.name = name;
    }
}

class Walker{
    String name;

    public Walker(String name) {
        this.name = name;
    }
}

class Car <T> {   //해당 T는 drive 메소드의 T와 전혀 관련이 없다.
    public <T extends Driver> void drive(T driver) {  //제네릭스 메소드 선언!!!!

        System.out.println(driver.name + " 운전사가 운전을 합니다~ ");
    }
}

public class GenericsMethodPractice {
    public static void main(String[] args) {
        Car car = new Car();

        car.drive(new Driver("제뉴어리맨"));
//        car.drive(new Walker("뚜벅이")); //제네릭스 메소드에 선언된 제네릭 제한에 맞지 않으므로 에러 발생
    }
}

 

제네릭스 용어

위에 예를 참고하며 내용을 보도록 한다

  • GenericClass<T> : 제네릭 클래스. T GenericClass 라고 읽는다
  • T : 타입변수 또는 타입매개변수 (T는 타입문자)
  • GenericClass : 원시 타입 (raw type)

제네릭스의 장점

  • 타입 안정성 제공
    객체 생성시에 지정한 타입변수와 타입변수가 쓰인곳에서 실제 사용된 타입변수를 체크하여 지정한 타입변수와 맞지 않을 경우 자동으로 컴파일 에러를 발생시켜 줌.

  • 형변환이나 타입체크에 대한 코드를 생략 가능하게 해줌
package ch11.practice;

import java.util.ArrayList;
import java.util.List;

class GenericClass<T> {
    List<T> list = new ArrayList<>(100);

    public void add(T item) { list.add(item); } //인자의 타입 체크 필요없음. 
    public T get(int index) { return list.get(index); }
}

public class GenericEx {
    public static void main(String[] args) {
        GenericClass<String> genericClass = new GenericClass<>();
        genericClass.add(new String("value")); 
//        genericClass.add(new Integer(1)); //에러 발생 (타입 안정성)
        String item = genericClass.get(0);  //형변환 필요 없음
        System.out.println(item);
    }
}

 

제네릭스 사용 불가한 경우

1. static 멤버에 대해서는 사용 불가

원래 static 멤버에 대해서는 컴파일 시점에 이미 클래스 영역에 적재되는데,

클래스 내부에서 사용되는 타입이 정해지지 않은 상태이기 때문이다.

class Box <T> {
    static ArrayList<T> list; // 에러 
}



하지만 제네릭 정의시에 처음부터 키워드가 아니라 타입을 명시해주면 가능하다.

class Box <T> {
    static ArrayList<String> list;
}

근데 사실 이건 T를 사용하지 않았기 때문에, 제네릭스를 사용했다고 말할 수 있을지 모르겠다.

ArrayList의 관점에서는 제네릭스를 사용한거지만, Box의 관점에서는 제네릭스를 사용안한거로써 보여지기 때문이다.

 

 

2. new 키워드를 사용하여 제네릭 객체 생성 불가

컴파일 시점에 타입이 무엇인지 정해져 있지 않기 때문이다.

EX :

T item = new T(); //에러


그러나

class Box <T> {
    List<T> list = new ArrayList<>();
}


위에 코드는 가능하다. T가 객체가 아니라 ArrayList가 객체 인것이니까. 리스트 안에 T만 제네릭인것이다.

 

 

제네릭 사용시 기본적인 주의 사항

1. 제네릭스를 사용하여 객체 생성시에 참조 변수에 정의된 타입변수와 생성자 부분에서의 타입변수가 같아야 한다.

EX :

Box<String> box = new Box<Integer>() //에러
Box<String> box = new FruitBox<String>() //허용, Java의 다형성으로 인해 Box참조변수에 FruitBox 객체 생성상태. 
Box<String> box = new Box<>(); // 참조변수의 제네릭만 선언되있으면 생성자부분에서 생략 가능

 

2. 객체 생성시에 적용한 타입변수의 하위 클래스(타입)일 경우 타입체크 통과

EX :

Apple클래스가 Fruit클래스의 하위 클래스인 경우

package ch11.practice;

import java.util.ArrayList;
import java.util.List;

class GenericClass<T> {
    List<T> list = new ArrayList<>(100);

    public void add(T item) { list.add(item); }
    public T get(int index) { return list.get(index); }
}
class Fruit {}
class Apple extends Fruit {}

public class GenericEx {
    public static void main(String[] args) {
        GenericClass<Fruit> genericClass = new GenericClass<>(); //타입매개변수를 Fruit으로 지정
        genericClass.add(new Apple()); //Apple은 Fruit의 하위 클래스이므로 자바의 다형성 특징으로 인해 가능
    }
}

 

위에 내용은 마치 Object obj = new 각종 클래스() 와 같다. 

 

3. 타입변수로 기본데이터 타입을 설정할 수 없다.

class Water {}
class Cup <T> {
    T item;
}

public class WildcardPractice {
    public static void main(String[] args) {
//        Cup<int> cup = new Cup<>(); //에러, 기본데이터 타입은 불가
        Cup<Water> cup = new Cup<>(); //가능
    }
}

 

 

제한된 제네릭스

위에 내용을 보았듯이 객체생성시에 <>안에 특정 타입을 지정하면 클래스 내부적으로 해당 타입으로만 사용이 가능하다.

그렇지만 <>안에 자체에는 어떤 타입이 와도 무방한 상태이다.

즉, 위에 코드를 빌리자면

 

GenericClass<Fruit> genericClass = new GenericClass<>(); 

위에 코드의 <> 안에 String, Integer, Object 어떠한 타입이 와도 컴파일 에러는 발생하지 않는다는 것이다.

 

자 정리해보자,

제한된 제네릭스를 쓰는 이유는!

제네릭스를 써서 타입의 지정을 객체생성시로 미뤄서 다양한 타입을 받고 싶었지만, 클래스의 용도나 내부 로직상 최소한 특정 타입 혹은 특정타입의 상위, 하위 타입(클래스)이 들어와야 하는 경우에 제한된 제네릭스를 사용한다.

"한 마디로 무한대로 넓던 T의 영역을 최소한 이정도 타입으로는 들어와줘~" 라고 지정하는 것이다.

 

  • extends의 사용
    <K extends T> : T를 포함하여 T의 하위 클래스인 타입 K만
    <K extends 인터페이스> : 특정 인터페이스를 구현한 타입 K만
    <K extends T & 인터페이스> : 특정 인터페이스를 구현하면서 T의 하위 클래스(혹은 T클래스)인 K만.

 

와일드 카드

현재 내 이해도 정도에서는 와일드 카드에 대해서는 정확하게 이거다! 라고 정의를 내리긴 힘든거 같다.

하지만 아는 정도에서 정리를 해보겠다.

일단 와일드 카드는

클래스에서 사용하는것이 아니다.

즉, 메소드에서 사용되며 변수의 타입 자체를 지정할때 사용되진 않는다.

특정 타입의 요소를 지정할때 사용된다.

이 정도가 내 수준에서 와일드 카드를 이해한 정도이다.

그럼 아래에서 사용방법과 사용예를 보자

 

  • 와일드카드의 사용
    <? extends T> : T를 포함하여 T의 하위 클래스만
    <? super T> : T를 포함하여 T의 상위 클래스만
    <?> : 제한 없음. 모든 타입 가능


  • 사용 예
package ch11.practice;

import java.util.ArrayList;
import java.util.List;

class Student{
    String name;

    public Student(String name) {
        this.name = name;
    }
}

class SoccerPlayer{
    String name;

    public SoccerPlayer(String name) {
        this.name = name;
    }
}

class School {
    public static void printStudentList(List<? extends Student> list) {

        String nameList = "학생 출석부 : [ ";
        for (Student s : list) {
               nameList += s.name + " ";
        }
        nameList += " ]";
        System.out.println(nameList);
    }
}

class GeneClass <T> {

}

public class WildCardEx {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("제뉴어리맨"));
        students.add(new Student("줄라이맨"));
        students.add(new Student("악토버맨"));
        students.add(new Student("악토버맨"));

        School.printStudentList(students);

        List<SoccerPlayer> soccerPlayers = new ArrayList<>();
        soccerPlayers.add(new SoccerPlayer("제뉴어리맨"));
        soccerPlayers.add(new SoccerPlayer("줄라이맨"));
        
//        School.printStudentList(soccerPlayers); //에러 발생, 와일드카드에 어긋난 타입
    }
}

위에 내용을 보면 알 수 있듯이

School이란 클래스 선언부분에 제네릭을 선언하지 않았다.

단지 메소드 내에 있는 매개변수 타입(List)의 요소를 와일드카드(?)로 제한을 걸어둔 것이다.

그리고 제네릭 클래스에서는 타입변수를 static 메소드에 사용 할 수 없었다. 그러나 와일드 카드로는 사용이 가능하다.

이것은 와일드 카드여서라기 보다는 다른 이유 때문인것같다... (다른 블로그를 찾아봐도 명확한 이유가 없다. 단지 이러해서 이렇다 정도밖에 없는듯 한다)

밑에서 나오겠지만 제네릭 메소드를 사용해서도 static에 제네릭을 사용가능하다.

 

 

 

아래의 땡땡이 구분선 안의 내용은 좀 더 심화된 내용이므로 이미 어려운 경우 넘어가자.


매개변수인 List의 관점에서 본다면 List 인터페이스 안에는 제네릭이 선언 되어 있는데!

그 제네릭을 ? extends Student 라고 본 필자가 지정해준것이다! 

위에서 T를 객체 생성시에 <String> 으로 지정해 준것처럼 말이다.

아래의 main 함수 부분의 주석 내용을 보면 이해 할 수 있다.

package ch11.practice;

import java.util.ArrayList;
import java.util.List;

class Student{
    String name;

    public Student(String name) {
        this.name = name;
    }
}

class School {
    public static void printStudentList(List<? extends Student> list) {

        String nameList = "학생 출석부 : [ ";
        for (Student s : list) {
               nameList += s.name + " ";
        }
        nameList += " ]";
        System.out.println(nameList);
    }
}

class GeneClass <T> {

}

public class WildCardEx {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("제뉴어리맨"));
        students.add(new Student("줄라이맨"));
        students.add(new Student("악토버맨"));

        School.printStudentList(students);

        GeneClass<? extends Student> geneClass = new GeneClass<>(); // 타입변수 T를 "? extends Student" 로 지정
    }
}

제네릭스 메소드

메소드의 선언부분(시그니처) 에 타입변수를 선언한 메소드를 말한다.

즉, 해당 메소드에서만 사용 가능한 타입변수를 선언하겠다는것이다.

지금까지 알아본 제네릭스 (와일드카드 제외)는 모두 클래스 단위에서의 제네릭스였고

선언도 클래스명 뒤에 선언해주었다.

그러나 제네릭스 메소드는 메소드의 반환형 앞에 선언하여 준다.

와일드카드와 마찬가지로 static 메소드에도 사용 가능하다.

 

  • 기본 사용 법
    public <T extends Driver> void drive(T driver) {}
    물론 그냥 T로 사용하여도 된다.
    해당 타입변수는 해당 메소드내에서만 유효한 타입이다.

 

  • 사용 예
package ch11.practice;

class Driver {
    String name;

    public Driver(String name) {
        this.name = name;
    }
}

class Walker{
    String name;

    public Walker(String name) {
        this.name = name;
    }
}

class Car <T> {   //해당 T는 drive 메소드의 T와 전혀 관련이 없다.
    public <T extends Driver> void drive(T driver) {

        System.out.println(driver.name + " 운전사가 운전을 합니다~ ");
    } 
}

public class GenericsMethodPractice {
    public static void main(String[] args) {
        Car car = new Car();
        
        car.drive(new Driver("제뉴어리맨"));
//        car.drive(new Walker("뚜벅이")); //제네릭스 메소드에 선언된 제네릭 제한에 맞지 않으므로 에러 발생
    }
}

 

 

제네릭스 형변환

Box box = null;

Box<Object> objBox = null;

 

box = (Box) objBox;   // 허용, 지네릭 타입 -> 원시타입 . 경고발생 

objBox = (Box<Object> box;  // 허용,  원시타입 -> 지네릭 타입. 경고발생 

 

---

 

Box<Object> objBox = null;

Box<String> strBox = null;

 

objBox = (Box<Object>) strBox; //에러 

strBox = (Box<String>) objBox; //에러

 

---

 

Box<? extends Object> wBox = new Box<String>(); //형변환 가능

 

정리를 해보자면,

제네릭이 적용된 타입과 제네릭이 적용되지 않은 타입간에는 변환이 가능하다.

그런데 제네릭 타입간의 변환은 불가능하다.

그런데 참조변수의 제네릭(<>)이 와일드카드로 지정되어 있는 경우는, 와일드카드 제한의 맞는 수준에서 객체의 제네릭을 설정할 수 있다.

 

제네릭스 메소드와 와일드카드의 차이

제네릭 메소드의 타입변수는 타입자체로 쓰일 수 있지만, (T item 으로 변수 선언 가능)

와일드 카드는 클래스, 인터페이스 내부에서 쓰이는 요소로만 사용 가능하다. (? item 으로 변수 선언 불가, List<?> list 가능 ).

그리고 와일드 카드는 제네릭처럼 선언을 해두거나 그런거 없이,

사용시에 ?사용하여 적용.

 

 

제네릭스 회고

제네릭스는 편하려고 만든건데 날 더 어렵게 만든다.

우선 기본 개념을 익힌 후 사용해보면서 익혀야겠다.

그러다 보면 깨우치는 날이 오겠지.