제네릭(Generic)

종원유·2022년 1월 18일
0

Java

목록 보기
5/11
post-thumbnail

제네릭을 이해하지 못하면 API 도큐먼트를 정획히 이해할 수 없고 컬렉션, 람다식, 스트림, NIO에서 널리 사용되므로 확실히 이해해두어야 한다.

제네릭의 역할

제네릭은 클래스와 인터페이스, 메소드를 정의할 때 타입을 파라미터로 사용할 수 있도록 하는 역할을 한다.
타입 파라미터는 코드 작성 시 구체적인 타입으로 대체되어 다양한 코드를 생성하도록 해준다.

제네릭의 장점

  • 컴파일 시 강한 타입 체크를 할 수 있다.

    자바 컴파일러는 코드에서 잘못 사용된 타입 때문에 발생하는 문제점을 제거하기 위해 제네릭코드에 강한 타입 체크를 한다. 실행 시 타입 에러가 나느 것보다는 컴파일 시에 미리 타입을 강하게 체크해서 에러를 사전에 방지하는 것이 좋다.

  • 타입변환(casting)을 제거한다.

    비제네릭 코드는 불필요한 타입 변환을 하기 때문에 프로그램 성능에 악영향을 미친다(반복적인 캐스팅 때문).
//비제네릭
List list = new ArrayList();
list.add("velog");
String str = (String) list.get(0);
<br>
//제네릭
List<String> list = new ArrayList<>();
list.add("velog");
String str = list.get(0);

제네릭 코드로 작성할 경우 List에 저장되는 요소를 String으로 국한하기 때문에 요소를 꺼낼 때 타입 변환을 할 필요가 없어 프로그램 성능이 향상된다.
또한, 모든 종류의 객체를 저장하면서 타입 변환이 발생하지 않도록 작성할 수 있다.


제네릭 타입

public class GenericClass<T>{ ... }
public interface GenericInterface<T> { ... }

제네릭 타입은 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다.
제네릭 타입은 클래스 또는 인터페이스 이름 뒤에 "<>" 부호가 붙고, 사이에 타입 파라미터가 위치한다.

  • 타입 파라미터는 변수명과 동일한 규칙에 따라 작성할 수 있지만, 일반적으로 대문자 알파벳 한 글자로 표현한다.

아래는 제네릭 코드 예시이다.

// Generic.java
public class Generic<T>{
	private T t;
    public T get(){ return t; }
    public void set(T t) { this.t = t; }
}

//GenericExample.java
public class GenericExample{
	public static void main(String[] args){
    	//String으로 타입 지정
    	Generic<String> generic = new Generic<String>();
        generic.set("velog");
        String str = generic.get();
        
        //Integer로 타입 지정 
    	Generic<Integer> generic = new Generic<Integer>();
        generic.set(100);
        int value = generic.get();
    }
}

위와 같이 제네릭은 클래스를 설계할 때 구체적인 타입을 명시하지 않고, 타입 파라미터로 대체했다가 실제 클래스가 사용될 때 구제적인 타입을 지정함으로써 타입 변환을 최소화한다.

멀티 타입 파라미터

제네릭 타입은 두 개 이상의 멀티 타입 파라미터를 사용할 수 있다.

class TestClass<T, K> { ... }
interface TestInterface<T, K> { ... }

    static class MultiType <T, K> {
        private T type;
        private K key;
        public T getType(){ return type; }
        public K getKey(){ return key; }
        public void setType( T type ){ this.type = type; }
        public void setKey( K key ) { this.key = key; }
    }

    public static void multiType(){
        MultiType<String, Integer> test1 = new MultiType<>();
        test1.setType("velog");
        test1.setKey(10000);
        String type1 = test1.getType();
        Integer key1 = test1.getKey();

        MultiType<Object, Double> test2 = new MultiType<>();
        test2.setType(new Object());
        test2.setKey(1000.0);
        Object type2 = test2.getType();
        Double key2 = test2.getKey();
    }

위 코드는 MultiType<T, K> 제네릭 타입을 정의하고 사용하는 간단한 코드이다.

또, 제네릭 타입 변수 선언과 객체 생성을 동시에 할 때, 타입 파라미터 자리에 구체적인 타입을 지정하는 코드가 중복된다. 자바는 이러한 중복코드에 대해서 <>를 제공한다.
자바 컴파일러는 타입 파라미터 부분에 <>연산자를 사용하면 타입 파라미터를 유추해서 자동으로 설정해준다.


MultiType<String, Integer> test1 = new MultiType<>();

MultiType<String, Integer> test1 = new MultiType<String, Integer>();
//둘 다 같다. <> 연산자는 타입 파라미터를 유추해서 자동으로 설정해준다.

제네릭 메소드

public <T> T test(T t) { ... }

제네릭 메소드는 매개 타입과 리턴 타입으로 타입 파라미터를 갖는 메소드를 말한다.
제네릭 메소드를 선언하는 방법은 리턴 타입 앞에 <>를 추가하고 타입 파라미터를 기술한 다음, 리턴 타입과 매개 타입으로 타입 파라미터를 사용하면 된다.

예시 코드

    static class Box<T>{
        private T box;
        public T getBox(){ return box; }
        public void setBox(T box){ this.box = box; }
    }

    static class GenericMethod{
        public static <T> Box<T> boxing(T t){
            Box<T> box = new Box<T>();
            box.setBox(t);
            return box;
        }
    }

    public static void genericMethod(){
        Box<Integer> box1 = GenericMethod.<Integer>boxing(100);
        int intValue = box1.getBox();

        Box<String> box2 = GenericMethod.boxing("velog");
        String strValue = box2.getBox();
    }

다음 boxing() 제네릭 메소드는 <> 기호 안에 타입 파라미터 T를 기술한 뒤, 매개 변수 타입으로 T를 사용했고, 리턴 타입으로 제네릭 타입 Box<T>를 사용했다.

public static <T> Box<T> boxing(T t)

다음은 genericMethod()의 코드를 보자.
제네릭 메소드는 두 가지 방식으로 호출할 수 있다.
코드에서 타입 파라미터의 구체적인 타입을 명시적으로 지정해도 되고, 컴파일러가 매개 값의 타입을 보고 구체적인 타입을 추정하도록 할 수도 있다.

  • 코드에서 타입 파라미터의 구체적인 타입을 명시
  • 컴파일러가 매개 값의 타입을 보고 구체적인 타입을 추정
//리턴타입 변수 = <구체적인 타입> 메소드명(매개 값)	: 구체적인 타입을 명시
Box<Integer> box1 = GenericMethod.<Integer>boxing(100);

//리턴타입 변수 = 메소드명(매개값)				   : 컴파일러가 매개 값 추정
Box<String> box1 = GenericMethod.boxing(100);



제한된 타입 파라미터

타입 파라미터에 지정되는 구체적인 타입을 모든 객체가 아닌 특정 객체로 제한해서 사용할 수있다.

public <T extends 상위타입> 리턴타입 메소드(매개변수, ...){ ... }

<T extends 상위타입> 제한된 타입 파라미터를 선언하려면 타입 파라미터 뒤에 extends 키워드를 붙이고 상위 타입을 명시한다.

  • extends 키워드로 상위 타입을 명시하면 상위타입의 하위 요소들만 타입 파라미터로 사용될 수 있다.


  • extends 라는 키워드만 봐서는 클래스만 상위 요소로 제한할 수 있겠다고 생각하겠지만, 인터페이스의 경우도 implements 대신 extends 키워드로 제한할 수 있다.


  • 이 경우는 인터페이스의 구현 클래스만 타입 파라미터로 사용될 수 있다.

주의 점

제한된 타입 파라미터를 사용할 경우 주의할 점이 있다.
바로 상위 타입에 있는 필드와 메소드만 사용할 수 있다.

    static class TestParent{
        public static void printParent(){
            System.out.println("Parent 출력");
        }
    }

    static class Test extends TestParent{
        public static void printChild(){
            System.out.println("Child 출력");
        }
    }

    public static <T extends TestParent> void genericTest(T t){
        t.printParent();
        t.printChild();
    }

    public static void main(String[] args) {
        genericMethod(new Test());
    }

위 코드는 타입 파라미터의 상위 타입 메소드와 타입 파라미터의 메소드를 호출한 경우이다.
사진처럼 타입 파라미터에만 있는 메서드의 경우는 사용할 수 없고, 상위 타입에 있는 메서드만 사용할 수 있다.



와일드카드 타입

?

코드에서 ?를 일반적으로 와일드카드라고 부른다.
제네릭 타입을 매개 값이나 리턴 타입으로 사용할 때 구체적인 타입 대신 와일드 카드를 다음과 같이 세가지로 사용할 수 있다.

  • 제네릭 타입<?> : Unbounded Wildcards(제한없음)
    - 타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있다.

  • 제네릭 타입<? extends 상위타입> : Upper Bounded Wildcards (상위 클래스 제한)
    - 타입 파라미터를 대치하는 구체적인 타입으로 _상위 타입이나 하위 타입-만 올 수 있다.

  • 제네릭 타입<? super 하위타입> : Lower Bounded Wildcards(하위 클래스 제한)
    - 타입 파라미터를 대치하는 구체적인 타입으로 하위 타입이나 상위 타입이 올 수 있다.
    (super 키워드는 와일드카드에서만 사용가능한 제한 연산자다.)

public class Wildcards {

    public static class Person{
        private String type;
        Person(String type){
            this.type = type;
        }
    }

    public static class Worker extends Person{
        Worker(String type){
            super(type);
        }
    }

    public static class Student extends Person{
        Student(String type){
            super(type);
        }
    }

    public static class HighStudent extends Student{
        HighStudent(String type){
            super(type);
        }
    }


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

        public Course(String name, int capacity){
            this.name = name;
            students = (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 ){
                    students[i] = t;
                    break;
                }
            }
        }
    }

    public static void registerCourse( Course<?> course ) {
        System.out.println(course.getName() + " 수강생");
        Arrays.toString(course.getStudents());
    }

    public static void registerCourseStudent(Course<? extends Student> course){
        System.out.println(course.getName() + "수강생");
        Arrays.toString(course.getStudents());
    }

    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<Person>("일반인 과정", 5);
        personCourse.add(new Person("일반인"));
        personCourse.add(new Person("직장인"));
        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("고등학생"));

	//<?> 모든 객체를 타입 파라미터로 사용할 수 있음.
        registerCourse(personCourse);
        registerCourse(workerCourse);
        registerCourse(studentCourse);
        registerCourse(highStudentCourse);
        System.out.println();

	//<? extends Student> Student 객체 혹은 Student 하위 객체만 타입 파라미터로 사용할 수 있음.
        //registerCourseStudent(personCourse);  X
        //registerCourseStudent(workerCourse);  X
        registerCourseStudent(studentCourse);
        registerCourseStudent(highStudentCourse);
        System.out.println();

	//<? super Worker> Worker 객체 혹은 Worker 상위 객체만 파라미터로 사용할 수 있음.
        registerCourseWorker(personCourse);
        registerCourseWorker(workerCourse);
        //registerCourseWorker(studentCourse);      X
        //registerCourseWorker(highStudentCourse);  X
    }
}




제네릭 타입의 상속과 구현

  • 제네릭 타입도 다른 타입과 마찬가지로 부모 클래스가 될 수 있다.
public class ChildProduct<T, M> extends Product<T, M> { ... }
  • 또, 자식 제네릭 타입은 추가적으로 타입 파라미터를 가질 수 있다.
public class ChildProduct<T, M, C> extends Product<T, M> { ... }

제네릭 상속 사용법

부모 제네릭 클래스 Product.java

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

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

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

자식 제네릭 클래스 ChildProduct.java

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

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

    public C getCompany() { return company; }
}

제네릭 인터페이스 Storage.java

public interface Storage<T> {
    public void add(T item, int index);
    public T get(int index);
}

제네릭 인터페이스 구현객체 StorageImpl.java

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

    public StorageImpl(int capacity) {
        //타입 파라미터로 배열을 생성할 경우 new T[n]으로 생성할 수 없다.
        this.array = (T[])(new Object[capacity]);
    }

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

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

Tv.java 객체 vo

public class Tv {  }

제네릭 상속 사용 예시 코드

public class GenericExtends {
    public static void main(String[] args) {
        ChildProduct<Tv, String, String> product = new ChildProduct<>();
        product.setKind(new Tv());
        product.setModel("Smart TV");
        product.setCompany("LG");

        Storage<Tv> storage = new StorageImpl<>(100);
        storage.add(new Tv(), 0);
        Tv tv = storage.get(0);
    }
}
profile
개발자 호소인

0개의 댓글