본문 바로가기
Team

[CS 스터디] 자바 제네릭

by seungh2 2023. 9. 14.

제네릭 Generic

  • 데이터 타입을 일반화하는 것
  • 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정한다.

→ 컴파일 시에 type check를 한다.

→ 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.


Java 5 이전

  • 여러 타입을 사용하는 대부분의 클래스나 메소드에서 인수나 반환값으로 Object 타입을 사용했었다.
  • Object로 반환된 타입에 대해 다시 원하는 타입으로 타입을 변환해야 한다. → 오류 발생 가능성 ↑
  • (컬렉션에서 객체를 검색하거나 제거할 때마다 개발자들이 명시적으로 형변환을 해줘야 했다.)
import java.util.ArrayList;

public class Main {
    static class Temp{
        Object field;

        public Temp(Object field) {
            this.field = field;
        }
        public Object getField() {
            return field;
        }
    }
    public static void main(String[] args) {
        Temp temp = new Temp("String");
        // 형변환 필요
        String str = (String) temp.getField();
    }
}

제네릭 사용

public class Main {
    public static class Temp<T>{
        T field;

        public Temp(T field) {
            this.field = field;
        }
        public T getField() {
            return field;
        }
    }
    public static void main(String[] args) {
        Temp<String> temp = new Temp<>("String");
        // 형변환 필요 없음
        String str = temp.getField();
    }
}

Generic 특징

타입 안정성을 높인다.

  • 의도하지 않은 타입의 객체를 저장하는 것을 막는다.
  • 저장된 객체를 꺼낼 때에도 원래의 타입과 다른 타입으로 형변환되는 오류를 줄인다.
import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> number = new ArrayList<>();
        number.add(1);
        number.add(2);
//        number.add("1");    // Integer만 들어갈 수 있음
		// 형변환 필요 없음
		int num = number.get(0);

        ArrayList<String> str = new ArrayList<>();
        str.add("a");
        str.add("b");
        // 형변환 필요 없음
        String s = str.get(0);
    }
}
  • 의도하지 않은 타입( = String)의 객체를 저장하는 것을 막는다.
  • number에서 꺼낸 타입은 Integer, str에서 꺼낸 타입은 String

제네릭을 사용하는 이유

타입 안정성

  • 제네릭을 사용하면 컴파일 시간에 타입 체크를 수행하여, 잘못된 타입의 객체가 사용되는 것을 방지할 수 있다.
  • 런타임에 ClassCastException 발생하는 것을 줄일 수 있다.

코드 재사용 

  • 하나의 클래스나 메서드를 다양한 타입에 대해 동작하도록 만들 수 있다.

코드 간결성

  • 형변환 코드를 작성하지 않아도 된다.

제네릭은 불공변

  • List<String>은 List<Object>의 하위 타입이 아니다.
import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
//        ArrayList<Object> number = new ArrayList<String>();	 안됨
    }
}

* 배열은 공변

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        Object[] obj = new String[5];
        obj[0] = 1; // 런타임에 ArrayStoreException 발생
    }
}

타입추론

  • Java 7부터 제네릭의 타입추론이 가능해졌다.
  • 컴파일러가 알맞은 타입을 자동으로 추론할 수 있다. 
import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
    	// Java 7 이전에는 둘 다 Integer를 써줘야 했음
        ArrayList<Integer> number = new ArrayList<Integer>();
        // 컴파일러가 코드 문맥을 통해
        // 선언할 때, String으로 했으니까 생성할 타입 인자를 String으로 추론 가능
        ArrayList<String> str = new ArrayList<>();
    }
}

  • 제네릭 타입 : Temp<T>
  • 정규 타입 매개변수 : T
  • raw 타입 : Temp
  • 매개변수화 타입 : Temp<String>
  • 실제 타입 매개변수 : String
정규 타입 매개변수 알파벳은 아무거나 사용해도 되지만 관용적으로 사용하는 알파벳들이 있다.
T : Type의 T
E : Element의 E, List 같은 컬렉션의 요소에 사용
K : Key의 K, Map의 key
V : Value의 V, Map의 value

 

여러 개의 타입 파라미터가 필요한 경우에는 <> 안에 쉼표로 필요한 만큼 선언하면 된다.
public class Main {
    public static class Temp<T, E>{
        T field;
        E element;
        public Temp(T field, E element) {
            this.field = field;
            this.element = element;
        }
        public T getField() {
            return field;
        }
        public E getElement(){
        	return element;
        }
    }
    public static void main(String[] args) {
        Temp<String, Integer> temp = new Temp<>("String", 1);
        // 형변환 필요 없음
        String str = temp.getField();
        Integer n = temp.getElement();
    }
}

제한된 타입 파라미터

  • 타입 파라미터를 사용할 때, ~중에 아무거나를 구현하는 방법
public class Main {
    public static class Temp<T extends Number>{
        T field;

        public Temp(T field) {
            this.field = field;
        }
        public T getField() {
            return field;
        }
    }
}

→ Number 를 구현하고 있는 객체만 T가 될 수 있다. 

→ 즉, Number를 포함한 객체 중 아무거나


와일드카드 <?>

  • 아무 타입이나 받고 싶을 때 사용하는 방법
  • 항상 <> 연산자 안에 있어야 한다.
import java.util.List;

public class Main {
    static void printList(List<?> list) {
        list.forEach(System.out::println);
    }
}

<T>는 반환값, 매개변수 타입, 메서드 코드 중에서 사용 가능하지만

<?> 는 사용 불가능 하다.

public <T> void go(T param){}		// 됨
// public void go(? param){}		// 안됨

한정적 와일드카드 타입 

  • 한정적 타입 매개변수와 비슷하게 ~ 중 아무거나를 구현할 수 있는 방법
<? extends T>		// T 타입을 포함한 T의 하위 타입 아무거나
<? super E>			// E의 상위 타입 아무거나

Type Erasure 타입 소거

  • 컴파일러가 제네릭을 확인하여 필요한 곳에 형변환을 넣어준 다음 제네릭 타입을 제거한다.
  • 컴파일된 파일(*.class)에는 제네릭 타입이 없다.

→ 제네릭이 도입되기 이전의 소스코드와의 호환성을 유지하기 위해서

→ 타입 파라미터가 바인드 되지 않은 경우에는 Object로 대체된다.

 

<> 연산자 안에 Primitive Type을 사용하지 못하는 이유

  • Type Erasure 때문
    • 제네릭 타입이 특정 타입으로 제한되어 있을 경우에는 해당 타입에 맞춰 컴파일 시 타입을 변경하고
    • 제한되어 있지 않을 경우에는 Object 타입으로 변경한다.
  • Primitive Type은 Object를 상속받고 있지 않기 때문에 사용하지 못한다.
  • 기본 타입 자료형을 사용하기 위해서는 Wrapper 클래스를 사용해야 한다.
Wrapper 클래스를 사용할 경우 Boxing, Unboxing으로 구현 자체에 크게 신경쓰지 않아도 된다.

Wrapper 클래스

  • Primitive Type 데이터를 객체로 다루기 위해 제공되는 클래스
  • Primitive Type 값을 내부에 래핑하여 객체로 만들어준다.
  • 각각 Primitive Type에 대응하는 Wrapper 클래스가 있다.

Boxing

  • Primitive Type 값을 해당하는 Wrapper 클래스의 인스턴스로 변환하는 것
  • ex) int 값을 Integer 객체로 변환하는 것

Unboxing

  • Wrapper 클래스의 인스턴스를 다시 해당하는 Primitive Type 값으로 변환하는 것
  • ex) Integer 객체의 값을 int 값으로 변환하는 것

 

Java 5부터 Autoboxing, Auto-unboxing을 지원하여 컴파일러가 자동으로 boxing, unboxing 코드를 생성해준다.

 

728x90

댓글