14주차. 자바의 제네릭에 대해 학습하세요.

Jiwon·2022년 11월 20일

학습할 것

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

제네릭

제네릭은 클래스, 인터페이스 및 메서드를 정의할 떄 유형 (클래스 및 인터페이스)이 매개 변수가 되도록 한다. 메서드 선언에 사용되는 보다 친숙한 형식 매개 변수와 마찬가지로 형식 매개 변수는 다른 입력으로 동일한 코드를 재사용 할 수 있는 방법을 제공한다.

차이점은
매개 변수에 대한 입력은 : 값
유형 매개 변수에 대한 입력은 : 유형
이라는 것.

제네릭을 사용하는 코드는 제네릭을 사용하지 않는 코드에 비해 많은 이점을 가진다.

컴파일 타임에 더 강력한 타입 검사가 가능하다.
Java 컴파일러는 제네릭을 사용하지 않는 일반 코드에 강력한 타입 검사를 적용하고 코드가 안정성을 위반할 경우 오류를 발생시킨다. 컴파일 오류는 런타임 오류를 수정하는 것 보다 쉽기 때문에 좋은 이점을 가진다.

제네릭을 사용하면 타입 캐스팅을 제거할 수 있다.

  • 제네릭 사용하지 않은 코드
List list = new ArrayList();
list.add("hello");
String s = (String)list.get(0);
  • 제네릭 사용 코드
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);	//no cast

타입 캐스팅을 하지 않아도 정상적으로 사용 가능하다.
: 프로그래머가 제네릭을 사용하여 다양한 유형의 컬렉션에서 작동하고 사용자 정의가 가능하며, 유형에 안전하고 읽기 쉬운 제네릭 알고리즘을 구현할 수 있다.


제네릭 사용법

public class Box {
	private Object object;
    
    public void set(Object object) {
    	this.object = object;
    }
    public Object get() {
    	return object;
    }
}

Box의 필드 object는 타입이 모든 클래스의 상위 타입인 Object이기 때문에 모든 클래스의 정보를 가질 수 있다. 하지만 해당 필드를 get/set으로 잘못 꺼내서 쓰거나 저장할 경우 런타임 시에 타입이 맞지 않는 오류가 발생할 수 있다.

public clas Exam01 {
	public static void main(String[] args) {
    	Box box = new Box();
        box.set(10)p;	//정수
        String s = (String)box.get()	//실수로 String 을 get
    }
}

ClassCastException이 발생한 것을 알 수 있다.

위의 Box 클래스를 제네릭 클래스로 정의하면

public class Box<T> {
	// T stands for "Type"
    private T t;
    
    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

제네릭 클래스는 다음과 같은 형식으로 정의할 수 있다.

클래스 이름 <T1, T2, ..., Tn> {/*...*/}

<>로 구분된 매개 변수는 클래스 이름 뒤에 온다. 해당 괄호에는 지정할 타입 피라미터를 넣을 수 있다. 또한 인터페이스에서도 동일하게 적용이 가능하다.

public class Exam02{
	public static void main(String[] args){
    	Box<Integer> box = new Box<>();
        
        box.set(10);
        Integer integer = box.get();
    }
}

타입 매개변수 명명 규칙

단일 대문자로 작성한다. 해당 규칙을 적용하면 일반 클래스 혹은 인터페이스의 이름의 차이를 구분하여 직관적으로 사용할 수 있다.

  • E element
  • K key
  • T type
  • V value
  • S,U,V

제네릭 타입 선언 및 인스턴스화

제네릭 클래스인 Box에 구체적인 타입을 선언하기 위해서는 <>를 활용해야 한다.

Box<Integer> box;

다른 변수선언과 비슷한 모양을 가지고 있고, 단순히 Integer 타입을 가지는 Box에 대한 참조 변수를 선언한 것이다. 이 클래스를 인스턴스화 하기 위해서는 동일하기 new 키워드를 사용한다.

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

Java7 부터는 클래스의 생성자를 호출하는데 필요한 타입 인수를 빈 유형으로 바꿀 수 있다. 해당 <>을 다이아몬드라고 부른다.

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

생략하여도 적절히 타입 추론되어 인스턴스화 되는 것을 알 수 있다.


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

바운디드 타입

만약 type 매개변수에 제한을 두고 싶다면 다음과 같이 사용 가능

public class Box<T> {
	private T t;
    
    public void set(T t) {this.t = t;}
    public T get() { return t; }
    
    public<U extends Number> void print(U u){
    	System.out.println(t.getClass());
        SYstem.out.println(u.getClass());
    }
}

print메소드의 type 매개변수를 extends 키워드를 사용하여 Number로 제한하였다. 해당 U에는 Integer, Float 등등의 클래스만 허용된다.

와일드 카드

와일드 카드를 표현하는 ?는 알 수 없는 타입을 나타낸다. 와일드 카드는 다양한 상황에서 사용할 수 있다. 매개변수, 필드 혹은 지역 변수, 때때로 반환 타입에도 사용된다.

하지만 와일드 카드는 제네릭 메소드 호출, 제네릭 클래스 인스턴스 생성 또는 슈퍼 타입의 타입 인수로는 사용되지 않는다.

  • Upper Bounded Wildcards
    변수에 대한 제한을 완화시키고 싶을 때 사용. extends 키워드 앞에 wildcard를 붙여 사용한다.
public static double sumOfList(List<? extends Number> list){
	double s = 0.0;
    for (Number n : list)
    	s += n.doubleValue();
        
    return s;
}
  • Unbounded Wildcards
    매개변수로 전달받은 list의 모든 항목을 출력하는 printList 메소드를 만들었다.
public static void printList(List<Object> list){
	for(Object elem : list)
    	System.out.println(elem + " ");
    System.out.println();
}

하지만 printList는 Object List의 목록한 매개변수 목록으로 받을 수 있기 때문에, Double, Integer 등 타입의 list를 전달하면 컴파일 오류를 발생시킨다.

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

public class Exam06 {

    public static void main(String[] args) {

        List<Double> list = new ArrayList<>();
        list.add(10.0);
        list.add(11.0);
        list.add(12.0);

        printList(list);
    }

    public static void printList (List<?> list) {
        for (Object elem : list)
            System.out.print(elem + " ");
        System.out.println ();
    }
}

printList를 위와 같이 수정하여 작성하면 모든 List에 대응이 가능하다.

  • Lower Bounded Wildcards
    Upper Bounded Wildcards와 반대로, 특정 type에서 해당 type의 super type까지 허용하는 제네릭을 만드는 경우 사용된다. wildcard 뒤에 super 키워드를 붙여서 사용한다.
import java.util.ArrayList;
import java.util.List;

public class Exam07 {

    public static void main(String[] args) {

        List<Integer> integers = new ArrayList<>();
        List<Number> numbers = new ArrayList<>();

        integers.add(10);
        numbers.add(10.0);

        printList(integers);
        printList(numbers);
    }

    public static void printList(List<? super Integer> list) {
        for (Object o : list)
            System.out.println(o.getClass());
    }
}

Integer 타입의 super type들이 허용되는 것을 알 수 있다.


제네릭 메소드 만들기

메소드의 선언부에 제네릭 타입이 선언된 메소드를 제네릭 메소드라고 한다.
위에 만들었던 printList를 제네릭 메소드로 변경하면

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

public class Exam08 {

    public static void main(String[] args) {

        List<Double> list = new ArrayList<>();
        list.add(10.0);
        list.add(11.0);
        list.add(12.0);

        printList(list);
    }

    public static <T extends Number> void printList(List<T> list) {
        for (Object elem : list)
            System.out.println(elem + " ");
        System.out.println();
    }
}

기존의 printList와 동일하게 적용된다. 매개변수의 타입이 복잡할 때 직관적으로 확인이 가능하기 때문에 유용하게 사용이 가능하다.


Erasure

Java에 제네릭이 도입되어 컴파일 타임에 엄격한 타입 검사를 지원한다.
제네릭을 구현하기 위해 Java컴파일러는 다음과 같은 type erasure를 제공한다.

  • 제네릭 타입의 모든 타입 매개변수를 해당 범위 또는 타입 매개변수가 제한되지 않은 경우 Object로 바꾼다.
  • 타입 안정성을 유지하기 위해 필요한 경우 타입 캐스트를 삽입한다.
  • 확정된 제네릭 유형에서 다형성을 유지하는 브리지 메소드를 생성한다.

type erasure는 매개 변수화된 타입에 대해 새 클래스가 생성되지 않도록 한다.
결과적으로 제네릭은 런타임 오버 헤드를 발생 시키지 않는다.

public class Node<T> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

출처
https://hyeonic.tistory.com/142

profile
과연 나는 ?

0개의 댓글