[영상후기] [10분 테코톡] 그린론의 제네릭

박철현·2023년 5월 3일
0

영상후기

목록 보기
118/160

movie

제네릭이란?

  • 제네릭 : 컴파일 타임에 타입을 체크함으로써 코드의 안정성을 높여주는 기능

    • List<T> / T : 타입 매개변수
    • List<String> / String : 매개변수화된 타입
  • 사용 이유

    • 컴파일 타임에 강력한 타입 검사 : 형식에 맞지 않는 타입을 넣었을 때 런타임 시에 이를 검출할 수 있는 반면, 제네릭 사용 시 컴파일 시에 검사할 수 있음

      List stringList = new ArrayList<>();
      stringList.add("woo");
      stringList.add(1);
      String result = (String) stringList.get(0) + 
                      (String) stringList.get(1); // Runtime Error
      List<String> stringList = new ArrayList<>();
      stringList.add("woo");
      stringList.add(1); // Compile Error
      			```
      
    • 캐스팅(타입 변환) 제거 : 리스트에 저장되는 요소를 알 수 없어 꺼낼때 매번 타입변환 -> 제네릭 사용 시 캐스팅 제거 가능

      List stringList = new ArrayList<>();
      stringList.add("woo");
      String result = (String) stringList.get(0);
      
      List<String> stringList = new ArrayList<>();
      stringList.add("woo");
      String result = stringList.get(0);
  • 배열 vs 제네릭 타입

// 아래 코드는 문법상 허용이 되지 않음
List<Object> objectList = new ArrayList<Integer>();

// 배열 vs 제네릭 타입
// 배열 : Integer가 Object 하위 -> 배열도 하위 타입 성립(공변)
Object[] objectArray = new Integer[1];

// Integer가 Object 하위 -> 제네릭 타입에는 적용 불가(무공변)
List<Object> objectList = new ArrayList<Integer>(); // Compile Error !

변성

  • 타입 계층 관계에서 서로 다른 타입간에 어떠한 관계가 있는지를 나타냄

    • 무공변(Invariance) - <T> - 제네릭 이에 해당

      • 타입 B가 타입 A의 하위 타입일 때, Category<B>가 Category<A>의 하위 타입이 아닌 경우. 즉 아무런 관계가 없음
      • ex) Integer가 Object의 하위 타입이더라도 Category<Object>와 Category<Integer>는 아무런 관련이 없음
        • List<Object> != List<Integer>
    • 공변(Covariance) - <? extends T>

      • 타입 B가 타입 A의 하위 타입일 때, Category<B>가 Category<A>의 하위 타입인 경우
      • 배열이 해당 / Object a[] = new Integer[1] 가능
    • 반공변(Contravariance) - <? super T>

      • 타입 B가 타입 A의 하위 타입일 때 Category<B>가 Category<A>의 상위 타입인 경우

제네릭 타입과 메서드

제네릭 타입

  • 타입을 파라미터로 가지는 클래스와 인터페이스
    • 클래스 또는 인터페이스에 화살 괄호가 붙고 그 사이에 타입 매개변수가 위치하는 형태
    • Object 타입을 저장할때와 같이 모든 타입을 저장할 수 있게 된다.
---- 비제네릭 ----
class Category {
	private Object object;
    
    public void set(Object object) {
    	this.object = object;
    }
    
    public Object get() {
    	return object;
    }
}

---- 제네릭 변경 ----
class Category<T> {
	private T t;
    
    public void set(T t) {
    	this.t = t;
    }
    
    public T get() {
    	return t;
    }
}

제네릭 메서드

  • 메서드의 선언부에 제네릭 타입이 선언된 형식
  • 제네릭 타입과 다르게 타입 매개변수의 범위가 메서드내로 한정된다
    • 아래 예시에서 선언된 타입 매개변수는 서로 문자가 같은 T지만 서로 다른 타입 매개변수.
class NoodleCategory<T> {
	private T t;
    
    public void set(T t) {
    	this.t = t;
    }
    
    public T get() {
    	return t;
    }
    
    public <T> void printClassName(T t) {
    	System.out.println("클래스 필드에 정의된 타입 = " + this.t.getClass().getName());
        System.out.println("제네릭 메서드에 정의된 타입 = " + t.getClass().getName());
    }
}

NoodleCategory<Noodle> noodleCategory = new NoodleCategory<>();
noodleCategory.set(new Noodle()); // 패키지명.Noodle 출력
noodleCategory.printClassName(new Pasta()); // 패키지명.Pasta 출력

제네릭 타입 제한의 필요성

  • 현재 타입 매개변수에는 모든 종류의 타입을 저장할 수 있다
  • 해당 타입을 Coke라고 선언 시 클래스 필드에 Coke를 저장할 수가 있음
  • Noodle만 저장하고 싶으나 Coke를 저장해도 문제가 안됨 -> 제네릭 타입 제한 필요성
 NoodleCategory<Noodle> noodleCategory = new NoodleCategory<>(); 
---- Coke도 가능 -> Noodle만 하고싶은데 Coke도 되는 현상 ---
NoodleCategory<Coke> cokeNoodleCategory = new NoodleCategory<>();
  • 제한된 제네릭 타입

    • extends Noodle 을 붙여줘서 타입 제한
    • 적용 코드
    class NoodleCategory<T extends Noodle> {
    		private T t;
      
        public void set(T t) {
            this.t = t;
        }
    
        public T get() {
            return t;
        }
    }
    • 결과
      • Noodle 타입을 상속한 Ramen 타입은 가능
      • 아무런 관계가 없는 Coke는 컴파일 오류
    NoodleCategory<Noodle> noodleCategory = new NoodleCategory<>(); 
    NoodleCategory<Ramen> ramenNoodleCategory = new NoodleCategory<>(); 
    NoodleCategory<Coke> cokeNoodleCategory = new NoodleCategory<>(); // 컴파일 에러
  • 상한 경계 : 제네릭 클래스 사용 시 상속을 사용하여 제한하고, 위쪽에 한계를 둔 경우

    • Noodle 클래스를 상속받은 클래스들만 사용할 수 있음

와일드 카드

  • 코드에서 ? 문자를 와일드 카드라 하고 제네릭에서 형태는 3가지가 있음

    • <?> Unbounded Wildcards : 모든 타입 가능
    • <? extends Noodle> Upper Wildcards : 상한 경계
      • Noodle과 하위타입
    • <? super Noodle> Lower Bounded Wildcards : 하한 경계
      • Noodle과 Noodle의 상위 타입
    • extends로 공변의 기능 / super로 반공변의 기능

와일드 카드 - 제한 예시

class Category<T> {
	private T t;
    
    public void set(T t) {
    	this.t = t;
    }
    
    public T get() {
    	return t;
    }
}

class CategoryHelper {
	public void popNoodle(Category<? extends Noodle> category) {
    	Noodle noodle = category.get(); // 꺼내기 ok
        category.set(new Noodle()); // 저장은 NO!
    }
    
    public void pushNoodle(Category<? super Noodle> category) {
    	category.set(new Noodle()); // 저장 OK
        Noodle noodle = category.get(); // 꺼내기 No
    }
}
  • popNoodle : 카테고리 타입 매개변수를 Noodle로 상한 제한

    • 조회 가능 : 최상위 타입이 Noodle이기 때문에 Noodle 변수로 안전하게 꺼내는건 가능
      • Noodle이 상위 타입 이니깐
    • 저장 불가
      • 타입 매개변수에 Noodle의 하위 타입인 Pasta가 매핑된 Category가 매개변수로 들어왔을 때
      • 상위 타입인 Noodle을 넣으면 하위 타입이 상위타입에 대입될 위험이 있어 컴파일 에러
      • 하위 타입에 상위 타입을 대입할 위험이 있어 불가능
  • pushNoodle : 카테고리 매개변수를 Noodle 타입으로 하한 제한

    • 저장 가능 : 카테고리에 매개변수화 된 타입이 Noodle 이거나 부모일 수 있으니 안전하게 객체 저장 가능
      • Noodle의 상위타입들만 올 수 있음
    • 꺼내기 불가 : noodle 보다 상위 타입이 꺼내질 수 있어 이 경우 컴파일 오류 발생

언제 와일드 카드 상한 제한과 하한 제한을 사용해야 할까?

PECS(Producer - extends, consumer - super)

  • 생성을 하는 곳에서는 extends를 쓰고, 소비를 하는 곳은 super를 사용하라는 공식
    • Effective Java 에서는 PECS 라는 공식으로 설명
class NoodleCategory<E> {
	private List<E> list = new ArrayList<>();
    
    public void pushAll(Collection<? extends E> box) {
    	for(E e : box) {
        	list.add(e);
        }
   }
 }
  • 상향제한을 하면 타입을 안전하게 가져올 수 있음
    • 생성을 하는 곳에서는 extends
class NoodleCategory<E> {
	private List<E> list = new ArrayList<>();
    
    public void popAll(Collection<? super E> box) {
    	box.addAll(list);
        list.clear();
   }
 }
  • 하한 경계를 하면 안전하게 저장할 수 있음
    • 소비를 하는 곳에서는 super

제네릭 타입 소거

  • 컴파일에는 타입 검사, 런타임 시에는 타입 소거
    • 해당 정보를 알 수 없음
  • 제네릭은 자바 5에서 생김 -> 기존 코드도 돌아가게 하기 위해 타입 소거라는 개념 도입
  • 타입 소거
    • 1) 타입 매개변수의 경계가 없는 경우에는 Object로, 경계가 있는 경우 경계 타입으로 타입 파라미터 변경
class Category<T> {
	private T t;
    
    public void set(T t) {
    	this.t = t;
    }
    
    public T get() {
    	return t;
    }
}

-> 타입 소거 1 : 런타임 시 경계 타입이 없으면 Object로 변경

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

class Category<T extends Noodle> {
	protected T t;
    
    public void set(T t) {
    	this.t = t;
    }
    
    public T get() {
    	return t;
    }
}

-> 타입 소거 2 : 런타임 시 경계 타입이 있으면 해당 경계타입으로 변경

class Category {
	protected Noodle t;
    
    public void set(Noodle t) {
    	this.t = t;
    }
    
    public Noodle get() {
    	return t;
    }
}
  • 2) 타입 안정성을 유지하기 위해 필요한 경우 타입 변환 추가
  • 3) 제네릭 타입을 상속받은 클래스의 다형성을 유지하기 위해 Bridge method 생성
profile
비슷한 어려움을 겪는 누군가에게 도움이 되길

0개의 댓글

관련 채용 정보