티스토리 뷰

JAVA

[Java Study 14] 제네릭

DevBee 2021. 5. 15. 11:10

제네릭(Generic)

제네릭은 필요한 데이터 타입을 클래스 내부가 아닌 외부에서 사용자에 의해 지정하는 것을 말합니다. 즉, 특정 타입을 미리 지정하는 것이 아니라 필요할 때 지정할 수 있도록 해주는 일반적인 타입입니다.

 

제네릭 타입을 사용하면 컴파일 단계에서 미리 강한 타입 체크가 가능하기 때문에 잘못된 타입을 사용하여 발생하는 에러를 사전에 방지할 수 있습니다. 또한 제네릭 타입을 사용하면 타입을 국한하기 때문에 요소를 찾아올 때 타입 변환을 할 필요가 없어 프로그램 성능이 향상됩니다.

 

ArrayList list = new ArrayList(); //제네릭을 사용하지 않을 경우
list.add("test");
String temp = (String) list.get(0); //타입변환이 필요함
        
ArrayList<String> list2 = new ArrayList(); //제네릭을 사용할 경우
list2.add("test");
temp = list2.get(0); //타입변환이 필요없음

 

제네릭 사용법

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

 

public class 클래스명<T> {...}
public interface 인터페이스명<T> {...}

 

타입 파라미터는 정해진 규칙은 없지만 일반적으로 대문자 알파벳 한글자로 표현합니다.

 

자주 사용하는 타입 파라미터는 다음과 같습니다.

 

타입 인자 설명
<T> Type
<E> Element
<K> Key
<N> Number
<V> Value
<R> Result

 

✔️제네릭 클래스

클래스를 설계할 때 구체적인 타입을 명시하지 않고 타입 파라미터로 넣어두었다가 실제 클래스가 사용될 때 구체적인 타입을 지정하여 사용하면 타입 변환을 최소화할 수 있습니다.

 

class ExClassGeneric<T> {
    private T t;

    public void setT(T t) {
        this.t = t;
    }
			
    public T getT() {
        return t;
    }
}

 

타입은 여러 개를 같이 사용할 수도 있습니다. 멀티 타입 파라미터를 사용하는 경우 각 타입 파라미터는 콤마(,)로 구분합니다.

 

class ExMultiTypeGeneric<K, V> implements Map.Entry<K,V>{

    private K key;
    private V value;

    @Override
    public K getKey() {
        return this.key;
    }

    @Override
    public V getValue() {
        return this.value;
    }

    @Override
    public V setValue(V value) {
        this.value = value;
        return value;
    }
}

 

✔️제네릭 인터페이스

인터페이스도 클래스처럼 제네릭 타입을 지정하여 사용할 수 있습니다.

 

interface ExInterfaceGeneric<T> {
    T example();
}

class ExGeneric implements ExInterfaceGeneric<String> {

    @Override
    public String example() {
        return null;
    }
}

 

✔️제네릭 메소드

매개변수 타입과 리턴 타입으로 타입 파라미터를 가지는 메소드를 말합니다.

 

매개변수 타입에 제네릭 타입을 선언(T)하고 리턴 타입 앞에 < > 기호를 추가해 타입 파라미터를 기술(<T>)하는 방식으로 제네릭 메소드를 선언할 수 있습니다.

 

제네릭 메소드를 호출하는 방법은 두 가지가 있습니다.

 

1. 리턴타입 변수 = <구체적인 타입> 메소드명(매개값);  // 명시적으로 구체적인 타입 지정
2. 리턴타입 변수 = 메소드명(매개값);  // 매개값을 보고 구체적인 타입 추정

 

예제를 살펴보면 다음과 같습니다.

 

package study.generic;

class Person<T, N> {
    private T name;
    private N age;

    public Person(T name, N age) {
        this.name = name;
        this.age = age;
    }

    public T getName() {
        return name;
    }

    public void setName(T name) {
        this.name = name;
    }

    public N getAge() {
        return age;
    }

    public void setAge(N age) {
        this.age = age;
    }

    // Generic Method
    public static<T, N> boolean compare(Person<T, N> p1, Person<T, N> p2) {
        boolean nameCompare = p1.getName().equals(p2.getName());
        boolean ageCompare = p1.getAge().equals(p2.getAge());

        return nameCompare && ageCompare;
    }
}

public class ExGeneric {
    public static void main(String[] args) {
        Person<String, Integer> person1 = new Person<>("Alice", 20);
        Person<String, Integer> person2 = new Person<>("Bob", 24);

        boolean samePerson = Person.compare(person1, person2);
        System.out.println(person1.getName() + " and " + person2.getName() + " are the same person? " + samePerson);
    }
}

 

 

✔️바운디드 타입

바운디드 타입은 제네릭으로 사용되는 타입을 제한할 수 있는 것을 말합니다. 상속 및 구현 관계를 통해 타입을 제한할 수 있습니다.

 

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

 

상위 타입에는 클래스 뿐만 아니라 인터페이스도 사용이 가능합니다. 하지만 인터페이스라고 해서 extends 대신 implement 키워드를 사용하는 것은 아닙니다.

 

타입 파라미터를 대체할 타입으로는 상위 타입, 상위 타입을 상속받은 하위 클래스 또는 상위 타입을 구현한 클래스가 올 수 있습니다.

 

메소드의 중괄호({, }) 안에서 타입 파라미터 변수로 사용 가능한 것은 상위 타입의 멤버(필드, 메소드)로 제한됩니다.

 

간단한 예제를 살펴보면 다음과 같습니다.

 

package study.generic;

class Util {
    public static <T extends Number> int compare(T number1, T number2) {
        double n1 = number1.doubleValue();
        double n2 = number2.doubleValue();

        return Double.compare(n1, n2);
    }
}

public class ExBoundedType {
    public static void main(String[] args) {
        int result = Util.compare(8, 4);
        System.out.println(result);

        // compile error!!!
        // result = Util.compare("8", "4");
    }
}

 

바운디드 타입을 통해 Number 타입 하위 클래스만 허용함으로써 Number 타입 하위 클래스가 아닌 String 같은 타입을 전달하면 컴파일 에러가 발생합니다.

 

✔️와일드 카드

제네릭 타입을 매개변수나 리턴 타입으로 사용할 때 타입 파라미터를 제한할 목적으로 사용합니다.

 

와일드 카드 타입은 다음과 같이 세 가지가 있습니다.

 

- 제네릭타입<?>: Unbounded Wildcards (제한없음)

타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있습니다.

 

public static void registerCourse(Course<?> course) { ... }

 

- 제네릭타입<? extends 상위타입>: Upper Bounded Wildcards (상위 클래스 제한)

타입 파라미터를 대치하는 구체적인 타입으로 상위 타입이나 그 상위 타입의 하위 타입만 올 수 있습니다. 상위 타입이 해당 자리에 들어갈 수 있는 가장 상위 타입이므로 상위 클래스 제한이라고 합니다.

 

public static void registerCourseStudent(Course<? extends Student> course) { ... }

 

- 제네릭타입<? super 하위타입>: Lower Bounded Wildcards (하위 클래스 제한)

타입 파라미터를 대치하는 구체적인 타입으로 하위 타입이나 그 하위 타입의 상위 타입만 올 수 있습니다. 하위 타입이 해당 자리에 들어갈 수 있는 가장 하위 타입이므로 하위 클래스 제한이라고 합니다.

 

public static void registerCourseWorker(Course<? super Worker> course) { ... }

 

예제를 통해 살펴보겠습니다.

 

먼저 강의 클래스를 생성합니다.

package study.generic.wildcards;

// 강의 클래스
// 강의명과 수강 인원을 가진 클래스로 어떤 타입의 학생들이 들을 수 있는지는 타입 파라미터로 선언
public 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 this.name;
    }

    public T[] getStudents() {
        return this.students;
    }

    public void add(T student) {
        for (int i = 0; i < students.length; i++) {
            if (students[i] == null) {
                students[i] = student;
                break;
            }
        }
    }
}

 

다음으로 사람 클래스와 이 클래스를 상속받은 학생, 직장인 클래스를 생성합니다.

 

package study.generic.wildcards;

public class Person {
    private String name;

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

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return name;
    }
}

 

package study.generic.wildcards;

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

 

package study.generic.wildcards;

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

 

package study.generic.wildcards;

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

 

이제 이 클래스들을 사용하는 메인 클래스를 살펴보겠습니다.

 

package study.generic.wildcards;

import java.util.Arrays;

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

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

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

        Course<HighStudent> highStudentCourse = new Course<>("고등학생 과정", 5);
        highStudentCourse.add(new HighStudent("고등학생"));

        registerCourse(personCourse);
        registerCourse(studentCourse);
        registerCourse(workerCourse);
        registerCourse(highStudentCourse);

        System.out.println();

        registerCourseStudent(studentCourse);
        registerCourseStudent(highStudentCourse);

        System.out.println();

        registerCourseWorker(workerCourse);
        registerCourseWorker(personCourse);
    }
}

 

Erasure

타입 파라미터를 컴파일 타임에만 검사하고 런타임 시에는 해당 타입 정보를 알 수 없게 하는 것을 말합니다.

 

특징은 다음과 같습니다.

 

  • unbounded type(<?>, <T>)는 Object로 변환합니다.
  • bound type(<E extends Comparable>)의 경우 Object가 아닌 Comparable로 변환합니다.
  • 제네릭 타입을 사용할 수 있는 일반 클래스, 인터페이스, 메소드에서만 소거 규칙을 적용합니다.
  • 타입 안정성 보존을 위해 필요하다면 type casting을 넣습니다.
  • 확장된 제네릭 타입에서 다형성을 보존하기 위해 bridge method를 생성합니다.

 

참고

- https://st-lab.tistory.com/153

- https://coding-factory.tistory.com/573

- https://ict-nroo.tistory.com/42

- https://devlog-wjdrbs96.tistory.com/263

'JAVA' 카테고리의 다른 글

[Java] stream 알아보기  (0) 2022.02.11
[Java Study 15] 람다식  (0) 2021.05.15
[Java Study 13] I/O  (0) 2021.05.13
[Java Study 12] 애노테이션  (0) 2021.05.13
[Java Study 11] Enum  (0) 2021.05.11
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함