Notice
Recent Posts
Recent Comments
Link
«   2025/12   »
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 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

기록하자..

제네릭 | 백기선님 LIVE-STUDY 본문

자바

제네릭 | 백기선님 LIVE-STUDY

P23Yong 2022. 8. 6. 23:56

제네릭

학습할 것

  • 제네릭 사용법
  • 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
  • 제네릭 메서드 만들기
  • Erasure

제네릭

Java 5부터 제네릭 타입이 새로 추가되었다. 제네릭 타입을 이용하게 되면서 잘못된 타입이 사용될 수 있는 문제를
컴파일 과정에서 제거할 수 있게 되었다. 제네릭은 클래스와 인터페이스, 그리고 메서드를 정의할 때 타입을 파라미터로
사용할 수 있도록 한다.

제네릭을 사용하는 이점은 다음과 같다.

  • 컴파일 타임에 타입을 체크할 수 있다.
    • 컴파일 타임에 타입을 체크하면서 객체의 타입 안정성을 높이고 실행 시간에 에러가 나는 것이 아닌 컴파일시 타입을 체크해 에러를 사전에 방지할 수 있다.
  • 타입 변환을 제거한다.
    • 비제네릭 코드는 불필요한 타입변환을 하기 때문에 프로그램 성능에 영향을 미친다.
    • 하지만 제네릭을 사용하게 되면 불필요한 타입 변환을 줄여준다.

제네릭 사용법

먼저 제네릭 클래스, 인터페이스의 사용법에 대해 알아보자.

제네릭 타입은 클래스 또는 인터페이스 뒤에 "<>" 부호가 붙고 그 사이에 타입 파라미터가 위치한다.

public class Hello<T> { ... }
public interface Hello<T> { ... }

다음과 같은 클래스가 있다고 해보자.

public class Box {

    private Object object;

    public void setObject(Object object) { this.object = object; }

    public Object get() { return object; } 
}

Box 클래스의 필드 타입이 Object로 설정되어 있다. 이는 필드에 어떤 타입의 객체가 와도 된다는 것이다. 만약 다음과 같은 클래스의 get() 메서드를 사용할 때 필드에 저장된 원래 타입의 객체를 얻기 위해서는 타입변환을 해야한다.

Box box = new Box();
box.set(new Something());

Something s = (Something) box.get();

이제 이 클래스를 제네릭을 사용해 바꿔보자.

public class Box<T> {

    private T t;

    public void set(T t) { this.t = t; }

    public T get() { return t; }
}

이렇게 타입 파라미터 T를 사용해서 Object타입을 전부 T로 대체했다. T는 Box 클래스로 객체를 생성할 때 구체적인 타입으로 변환된다.

Box<Something> box = new Box<Something>();

이렇게 되면 T는 자동적으로 Something 타입으로 변경되어 내부가 다음과 같이 재구성된다.

public class Box<Something> {

    private Something t;

    public void set(Something t) { this.t = t; }

    public Something get() { return t; }
}

이렇게 되면 다음과 같이 타입변환을 할 필요가 없어지게 된다.

Box<Something> box = new Box<Something>();
box.set(new Something());

Something s = box.get();

멀티 타입 파라미터

제네릭 타입은 두 개 이상의 멀티 타입 파라미터를 사용할 수 있는데, 이 경우 각 타입 파라미터를 콤마로 구분하게 된다.
다음을 보자.

public class TwoHandPlayer<T, M> {

    private T leftHandWeapon;
    private M rightHandWeapon;

    public void setLeftHandWeapon(T leftHandWeapon) {
        this.leftHandWeapon = leftHandWeapon;
    }
    public void setRightHandWeapon(M rightHandWeapon) {
        this.rightHandWeapon = rightHandWeapon;
    }

    public T getLeftHandWeapon() {
        return leftHandWeapon;
    }
    public M getRightHandWeapon() {
        return rightHandWeapon;
    }
}

단순히 하나의 파라미터와 있는 것과 사용법은 같다.

TwoHandPlayer<Sword, Shield> thp = new TwoHandPlayer<Sword, Shield>();

thp.setLeftHandWeapon(new Sword());
thp.setRightHandWeapon(new Shield());

Sword swrod = thp.getLeftHandWeapon();
Shield shield = thp.getRightHandWeapon();

제네릭 타입 변수 선언과 객체 생성을 동시에 할 때 타입 파라미터 자리에 구체적인 타입을 지정하는 코드가 중복해서 나오는 것을 볼 수 있다.
자바 7부터 제네릭 타입 파라미터 중복 기술을 줄이기 위해 다이아몬드 연산자 <>를 제공한다. 자바 컴파일러가 타입 파라미터 부분에 <>연산자를 사용하게 되면 타입을 유추해 자동으로 설정해준다.

TwoHandPlayer<Sword, Shield> thp = new TwoHandPlayer<>();

제네릭 메서드 만들기

제네릭 메서드는 매개 타입과 리턴타입으로 타입 파라미터를 갖는 메서드를 말한다.

public <T> Box<T> boxing(T t) { ... }

제네릭 메서드는 두 가지 방법으로 호출할 수 있다.

Box<String> box = <String>boxing("apple");
Box<String> box = boxing("apple");
  1. 명시적으로 구체적 타입을 지정
  2. 매개 값을 보고 구체적 타입을 추정

사용법은 다음과 같다.

public class BoxUtil {
    public static <T> Box<T> boxing(T t) {
        Box<T> box = new Box<>();
        box.set(t);
        return box;
    }
}
Box<String> box1 = BoxUtil.boxing("apple");
String val = box1.get();

제네릭 주요 개념 (바운디드 타입, 와일드 카드)

제네릭의 주요 개념을 알아보자.

바운디드 타입

먼저 바운디드 타입에 대해 알아보자. 타입 파라미터에 지정되는 타입을 제한할 필요가 있다. 예를 들면 숫자 연산을 해야할 때, 타입 파라미터에 오는 타입은 Byte, Short, Integer, Long, Double 등으로 제한되어야 할 것이다.
바운디드 타입은 특정 타입의 서브 타입으로 제한한다. 이를 선언하려면 extends 키워드를 타입 파라미터 뒤에 붙이고 상위 타입을 명시하면 된다. 상위 타입은 클래스뿐만 아니라 인터페이스도 가능하다.

public <T extends Number> int compare(T t1, T t2) {
    double v1 = t1.doubleValue();
    double v2 = t2.doubleValue();
    return Double.compare(v1, v2);
}

다음과 같이 박스에는 과일종류만 담아야 할 때도 T 타입을 Fruit으로 제한하면 컴파일 타임에 타입 에러를 발견할 수 있다.

public class BoxUtil {
    public static <T extends Fruit> Box<T> boxing(T t) {
        Box<T> box = new Box<>();
        box.set(t);
        return box;
    }
}

와일드 카드

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

  1. 제네릭 타입 <?> : Unbounded Wildcards
    • Unbounded Wildcard는 Box<?> 와 같이 물음표만 가지고 정의된다.
    • 모든 클래스나 인터페이스 타입이 올 수 있다.
  2. 제네릭타입 <? extends 상위타입> : Upper Bounded Wildcards
    • Upper Bounded Wildcards는 Box<? extends Fruit>와 같이 사용할 수 있다.
    • 상위타입으로 제한한 타입이나 그 자식 타입만 올 수 있게 된다.
  3. 제네릭타입<? super 하위타입> : Lower Bounded Wildcards
    • Lower Bounded Wildcards는 Box<? super Apple>과 같이 사용할 수 있다.
    • 하위타입이나 상위타입이 올 수 있다.

아래 예제는 '이것이 자바다' 책에 있는 예제 입니다.

다음과 같이 제네릭 타입 Course는 과정 이름과 수강생을 저장할 수 있다고 하자.

public class Course<T> {

    private String name;
    private T[] students;

    public Course(String name, int capacity) {
        this.name = name;
        this.students = (T[]) (new Object[capacity]);
    }

    public String getName() { return name; }
    public T[] getStudents() { return students; }
}

위와 같이 타입 파라미터로 배열을 생성하려면 new T[n] 형태로 배열을 생성할 수 없다.
그래서 (T[]) new Object[capacity]);로 생성했다.

수강생이 될 수 있는 타입을 다음 4가지로 가정하자.

  • Person
  • Worker (Person의 자식)
  • Student (Person의 자식)
  • HighStudent (Student의 자식)

registerCourseXXX() 메서드의 매개값으로 와일드카드 타입을 사용해 타입을 제한할 수 있게 하였다.

public class WildcardEx {

    public static void registerCourse(Course<?> 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 registerCourse(Course<? extends Student> course) {   // 학생 과정
        System.out.println(course.getName() + " 수강생: " + Arrays.toString(course.getStudents()));
    }
}
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<>("직장인과정", 2);
workerCourse.add(new Worker("직장인"));

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

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

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

registerCourseStudent(studentCourse);
registerCourseStudent(hightStudentCourse);

registerCourseWorker(workerCourse);
registerCourseWorker(personCourse);

Erasure

컴파일러는 제네릭 타입을 이용해서 소스 파일을 체크하고 필요한 곳에 알맞은 타입을 넣어준다. 그리고 제네릭 타입을 제거한다.
즉 컴파일된 파일 (.class 파일)에는 제네릭 타입에 대한 정보가 없다는 것이다. 즉, erasure는 타입을 컴파일타입에만 감시하고 런타임에는 해당 타입의 정보를 알 수 없다는 것이다.

이렇게 처리되는 이유는 제네릭이 도입되기 전의 소스코드와의 호환성을 유지하기 위해서라고 한다.

자바 컴파일러는 타입소거(Erasure)를 다음과 같이 적용한다.

  • 제네릭 타입에서 T나 ?는 Object 타입으로 전환한다.
  • 다음과 같이 의 경우에는 Comparable 타입으로 전환해준다.
  • 제네릭의 타입 안정성을 위해서 Bridge 메서드도 만들 수 있다.
    • Bridge 메서드는 wkqk 컴파일러가 컴파일 할 때 메서드 시그니처가 조금 다르거나 애매할 경우에 대비하여 작성된 메서드이다.
    • public class IntegerStack extends Stack { public void push(Object value) { push((Integer)value); } public void push(Integer value) { super.push(value); } }
    • public class IntegerStack extends Stack<Integer> { public Integer push(Integer value) { super.push(value); return value; } }
    • 위에서 자바 컴파일러는 IntegerStack의 push와 Stack의 push 방법에서 메서드 시그니처가 일치하지 않도록 해서 다음과 같이 Bridge 메서드를 만들어 다형성을 보존한다.

추가적으로 얘기할게 있는데, 제네릭은 Array보다 List에서 사용하자 이다.
다음과 같은 예를 생각해보자.

public class Example<T> {

    private T[] array;

    Example(int size){
        // array = new T[size];   // Type Parameter 'T' cannot be instantiated directly
        array = (T[])new Object[size];
    }
}

다음과 같이 배열을 사용할 때는 (T[]) 과 같이 타입변환을 해주어야 한다.
그 이유는 new 연산자를 사용하기 때문인데, new 연산자는 힙 영역에 객체를 할당해주지만 제네릭은 컴파일 타임에 일어나는 문법이기 때문에 바로 T[] 를 못쓰는 것이다.

제네릭과 static

static 변수에는 제네릭을 사용할 수 없다.

static 변수는 메서드 영역에 메모리를 할당받는 클래스 변수이기 때문에 모든 인스턴스에서 공유가 된다. 그런데, 만약에 제네릭이 가능하다면 모든 인스턴스에 따라 타입이 변한다는 소리인데 이는 불가능하기 때문이다.

대신에 static 메서드에서는 사용이 가능하다.

먼저 제네릭 메서드에 대해 설명하면, 제네릭 메서드를 정의할 때 <> 기호를 사용해 자신이 제네릭 메서드임을 컴파일러에게 알려야 하는데, 이는 클래스 레벨의 제네릭과는 무관하다.

다시 돌아와서 static 변수는 값 자체를 공유하기 때문에 제네릭을 사용할 수 없지만, static 메서드는 틀을 공유하기 때문에 틀 안에서 타입 파라미터가 오갈 수 있는 것이다.

참고 자료

이것이 자바다

밸덩 - java generics erasure

'자바' 카테고리의 다른 글

Java에서 String은 왜 Immutable 한가  (0) 2022.10.10
람다식 | 백기선님 LIVE-STUDY  (0) 2022.08.06
I/O | 백기선님 LIVE-STUDY  (0) 2022.08.06
Annotation | 백기선님 LIVE-STUDY  (0) 2022.08.06
Enum | 백기선님 LIVE-STUDY  (0) 2022.08.06