[Java] 제네릭(Generic)
본문 바로가기
Java

[Java] 제네릭(Generic)

by IYK2h 2023. 8. 7.
728x90

제네릭(Generic)

목차

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

제네릭을 왜 사용할까

제네릭은 다양한 타입의 객체를 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크(compile-time type check)를 해주는 기능이다.

추가로, 자료형만 다른 중복되는 소스를 하나로 묶어 소스 코드의 재사용성 높이기 위해서이다.

- 제네릭스는 JDK 1.5에서 처음 도입되었다.

장점

  • 컴파일 시 타입 체크를 해줌으로써 타입 안정성이 높다.
  • 형변환을 생략할 수 있어서 코드가 간결해진다.
List list = new ArrayList();
list.add("안녕하세요");
//타입 캐스팅
String s = (String) list.get(0);
List<String> list = new ArrayList<String>();
list.add("안녕하세요");
// 캐스팅 없음
String s = list.get(0);

제네릭 사용법

제네릭은 클래스, 인터페이스 그리고 메소드에 사용할 수 있다. 이때 중요한 건 매개변수로 타입을 전달할 수 있다.

제네릭을 사용하지 않고 작성한 모든 자료형의 객체에서 사용할 수 있는 class

public class GenericSample {

    Object object;

    public Object getobject() {
        return object;
    }

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

메서드가 Object를 받거나 반환하므로 primitive 타입이 아니라면 원하는 대로 자유롭게 전달할 수 있다. 그러나 컴파일 타임에 클래스가 어떻게 사용되는지 확인할 방법이 없다.

제네릭을 사용하여 작성한 모든 자료형의 객체를 담아 사용할 수 있는 class.

public class GenericSample<T> {

    private T t;

    public T get() {
        return t;
    }

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

public class Main {
    public static void main(String[] args) {
      GenericSample<Integer> genericSample = new GenericSample<Integer>;
      genericSample.set(123);
      // genericSample.set("123") Integer 제네릭에 스트링 값을 넣어 컴팡일 에러
      genericSample.get(); // 123
    }
}

GenericSample에서 T를 "타입 변수(type variable)"라고 한다.

제네릭 타입은 프리미티브 타입(Primitive type)을 타입 매개변수로 받을 수 없다.

이유는 유형 매개변수를 사용하여 일반 클래스 또는 메소드를 정의하면 런타임 중에 Object 유형으로 대체된다. 예를 들어 List <T>는 런타임 시 List <Object>로 처리된다. 그래서 제네릭 타입은 레퍼런스 타입(Reference type)을 타입 매개변수로 사용해야 한다.

다이아몬드

Java SE 7부터 컴파일러가 선언을 살펴본 후 타입을 추론할 수 있다면 일반 클래스의 생성자를 호출하는 데 필요한 타입 인자를 빈 타입 인자 <>로 바꿀 수 있다.

public class Main {
    public static void main(String[] args) {
      GenericSample<Integer> genericSample = new GenericSample<>;
      genericSample.set(123);
      genericSample.get(); // 123
    }
}

여러 개의 타입 파라미터

interface Pair <K, V> {
    public K getKey();
    public V getValue();
}

public class GenericPairSample<K, V> implements Pair<K, V> {

    private K key;
    private V value;

    public GenericPairSample(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {return key;}
    public V getValue() {return value;}

    public static void main(String[] args) {
        //선언에서 K 및 V 의 타입을 유추 할 수 있으므로 다이아몬드 표기법을 사용하여 코드를 단축할 수 있다.
        Pair <String, Integer> pair1 = new GenericPairSample <> ( "Zero", 8);
        Pair <String, String> pair2 = new GenericPairSample <> ( "Zero", "Cola");
    }
}

raw 타입

// raw Type에 parameterized type 대입
public class GenericSample<T> {

    private T t;

    public T get() {
        return t;
    }

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

    public static class Main {
        public static void main(String[] args) {
          GenericSample<Integer> genericSample = new GenericSample<>;

          // raw타입
          GenericSample rawGenericSample = new GenericSample();

          // raw Type에 parameterized type 대입
          rawGenericSample = genericSample;
        }
    }
}
// parameterized type에 raw Type 대입
public class GenericSample<T> {

    private T t;

    public T get() {
        return t;
    }

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

    public static class Main {
        public static void main(String[] args) {
          // raw타입
          GenericSample rawGenericSample = new GenericSample();

          // parameterized type에 raw Type 대입
          GenericSample<Integer> genericSample = rawGenericSample;

          genericSample.set(123);
          System.out.println(genericSample.get()); // 123
        }
    }
}

parameterized type에 raw Type 대입하는 경우 컴파일 단계에서 경고를 받는다.

> javac GenericSample.java
Note: GenericSample.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
> java GenericSample
OK

@SuppressWarnings("unchecked") 어노테이션으로 경고를 무시할 수도 있다.

raw 타입은 제네릭 타입 검사를 우회(type-unsafety)하여 안전하지 않은 코드에 의해 런타임 오류로 연장될 수 있으므로 "raw 타입을 사용하지 않아야 한다."

바운디드 타입(Bounded Type Parameter)

바운디드 타입은 특정 타입의 서브 타입으로 제한한다.

클래스나 인터페이스 설계할 댸 가장 흔하게 사용되는걸 많이 볼 수 있다.

BoundTypeSample 구문에서 컴파일 에러가 발생한다.

BoundTypeSample 클래스는 <T extends Number>로 선언되어 Number의 서브 타입만 허용하게 된다.

하위 타입은 대입할 수 있다.

public class BoundTypeSample <T extends Number> {
    public void set(T value){}
    public static void main(String[] args){
        BoundTypeSample<Integer> boundTypeSample1 = new BoundTypeSample<>();
        BoundTypeSample<Double> boundTypeSample2 = new BoundTypeSample<>();
    }
}

그러나 기존 타입의 상속관계가 제네릭 타입까지 이전되지는 않는다. Number는 Integer의 상위 클래스이지만, BoundTypeSample는 BoundTypeSample의 상위 클래스가 아니다.

ex)

ArrayList<E> implements List<E>
List<E> extends Collection<E>

https://docs.oracle.com/javase/tutorial/java/generics/inheritance.html

제한된 제네릭 클래스

타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만 여전히 모든 종류의 타입을 지정할 수 있는 것에는 변함이 없다.

매개변수 타입의 종류를 제한할 수 있는 방법

public class GenericSample<T>{

    T t;

    public T get() {
        return t;
    }

    public void set(T t) {
        this.t = t;
    }
}
public class AdjectiveGeneric<T extends Adjective> extends GenericSample {

}
AdjectiveGeneric<Adjective Type> adjectiveGeneric = new AdjectiveGeneric<>();

// 컴파일 에러
//AdjectiveGeneric<Another Type> adjectiveGeneric = new AdjectiveGeneric<>();

인터페이스를 구현한다고 해도 extends를 사용해야 한다. implements 사용 X

인터페이스가 여러 개라면?

public class AdjectiveGeneric<T extends AdjectiveOne & AdjectiveTwo> extends GenericSample {
}

만약 여러 상위 바운드 중에서 클래스가 있다면 해당 클래스가 가장 앞에 와야 한다. 안 그러면 컴파일 에러가 발생한다.

public class AdjectiveGeneric<T extends Class1 & Interface1 & Interface2> extends GenericSample {
}

와일드카드

Unbounded WildCard

List<?> 와 같은 형태로 물음표만 가지고 정의된다.

내부적으로 Object로 정의되어서 사용되고 모든 타입의 인자를 받을 수 있다. 타입 파라미터에 의존하지 않는 메소드만을 사용하거나 Object 메소드에서 제공하는 기능으로 충분한 경우에 사용한다.

Unbounded : 무한한, 끝이 없는

Upper Bounded Wildcard

List<? extends Foo> 와 같은 형태로 사용하고 특정 클래스의 자식 클래스만을 인자로 받는다.

임의의 Foo 클래스를 상속받는 어느 클래스가 와도 되지만 사용할 수 있는 기능은 Foo클래스에 정의된 기능만 사용이 가능하다.

Lower Bounded Wildcard

List<? super Foo>와 같은 형태로 사용하고, 특정 클래스의 부모 클래스만을 인자로 받는다.

제네릭 메소드

메서드의 선언부에 제네릭 타입이 선언된 메서드

컬렉션 메소드인 Collections.sort() 메소드가 바로 제네릭 메서드이며, 제네릭 타입의 선언 위치는 반환 타입 바로 앞이다.

static <T> void sort(List<T> list, Comparator<? super T> c)
public class GenericMethodSample {

    public static <T> T genericMethod(T t) {
        return t;
    }

    public static void main(String[] args) {
        System.out.println(genericMethod("문자")); //문자
        System.out.println(genericMethod(12345)); //12345
    }
}

해당 메소드 내부에서 사용할 타입 파라미터가 무엇인지 미리 알려줘야 한다.

Erasure

Java 컴파일러는 타입 Erasure 프로세스로서 모든 타입 파라미터를 지우고 타입 파라미터가 바인드 된 경우 첫 번째 바인드로 대체하고 타입 파라미터가 바인드 되지 않은 경우 Object 대체한다.

예시

package study;

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

public class Sample {
    List<Integer> list = new ArrayList<>();
}

바이트 코드

// class version 55.0 (55)
// access flags 0x21
public class study/Sample {

  // compiled from: Sample.java

  // access flags 0x0
  // signature Ljava/util/List<Ljava/lang/Integer;>;
  // declaration: list extends java.util.List<java.lang.Integer>
  Ljava/util/List; list

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 6 L0
    ALOAD 0

    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 7 L1
    ALOAD 0
    NEW java/util/ArrayList
    DUP
    INVOKESPECIAL java/util/ArrayList.<init> ()V
    PUTFIELD study/Sample.list : Ljava/util/List;
    RETURN
   L2
    LOCALVARIABLE this Lstudy/Sample; L0 L2 0
    MAXSTACK = 3
    MAXLOCALS = 1
}

ArrayList 가 생성될 때 타입 정보가 없다. raw type으로 ArrayList를 생성해도 똑같은 바이트 코드를 볼 수 있다.

내부에서 타입 파라미터를 사용할 경우 Object 타입으로 취급하여 처리된다.

이것을 타입 소거 (type Erasure)라고 한다.

타입 소거는 제네릭 타입이 특정 타입으로 제한되어 있을 경우 해당 타입에 맞춰 컴파일 시 타입 변경이 발생하고

타입 제한이 없을 경우 Object 타입으로 변경된다.

이렇게 구현된 이유는 "하위 호환성을 지키기 위해"서 이다. 제네릭을 사용하더라도 하위 버전에서도 동일하게 동작해야 하기 때문이다.

primitive 타입을 사용하지 못하는 것도 바로 이 기본 타입은 Object 클래스를 상속받고 있지 않기 때문이다. 그래서 기본 타입 자료형을 사용하기 위해서는 Wrapper 클래스를 사용해야 한다.

한 가지 더 생각해 볼 수 있는 문제

제네릭 타입 파라미터를 사용해서 배열을 생성하는 예제

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];
    }
    ...
}

제네릭 타입을 사용해서 배열을 생성하려면 array = new T[size]; 와 같이 사용하면 편할 텐데 왜 사용하지 못하고 array = (T[]) new Object[size]; 와 같이 사용해야 할까?

그 이유는 new 연산자를 사용하기 때문이다.

  • new 연산자는 동적 메모리 할당 영역인 heap 영역에 생성한 객체를 할당한다.
  • 하지만 제네릭은 컴파일 타임에 동작하는 문법이다.
  • 컴파일 타임에는 T의 타입이 어떤 타입인지 알 수 없기 때문에 Object 타입으로 생성한 다음 타입 캐스팅을 해주어야 사용할 수 있다.

추가로, static 변수에도 제네릭 타입을 사용할 수 없다.

package study;

public class Sample<T> {
    private T myValue_1;
    // 'study.Sample.this' cannot be referenced from a static context
    // private static T myValue_2;
}

static 키워드를 사용해서 멤버 필드를 선언하게 되면, 특정 객체에 종속되지 않고 클래스 이름으로 접근해서 사용할 수 있다.
제네릭 타입을 사용하면, 위 예제의 경우 Sample과 Sample 등으로 객체를 생성해서 인스턴스마다 사용하는 타입을 다르게 사용할 수 있어야 하는데, static 으로 선언한 변수가 가능할 수가 없다. 그렇기 때문에 static 변수에는 제네릭 타입을 사용할 수 없다.

하지만 static 메소드에는 제네릭을 사용할 수 있다.

static 키워드를 사용하면 클래스 이름으로 접근하여 객체를 생성하지 않고 여러 인스턴스에서 공유해서 사용할 수 있다.
변수 같은 경우 해당 값을 사용하려면 값의 타입을 알아야 한다. 하지만, 메소드의 경우 해당 기능을 공유해서 사용하는 것이기 때문에 제네릭 타입 변수 T를 매개변수로 사용한다고 하면 해당 값은 메소드 안에서 지역 변수로 사용되기 때문에 변수와 달리 메소드는 static으로 선언되어 있어도 제네릭을 사용할 수 있다.

Reference

https://xxxelppa.tistory.com/206?category=858435

https://rockintuna.tistory.com/102#type-erasure

728x90

'Java' 카테고리의 다른 글

[Java] 람다식  (0) 2023.09.25
[Java] I/O  (0) 2023.07.22
[Java] 어노테이션(Annotation)  (0) 2023.07.04
[Java] 리플렉션(Reflection)  (0) 2023.06.30
[Java] 바이트코드 조작  (0) 2023.06.23

댓글