Home [Java] Generic
Post
Cancel

[Java] Generic

2월 2주차 스터디 발표 자료📖
2주차 기술면접 주제 중 네번 째 주제입니다!
Generic 개념에 대해 정리하고자 합니다.
참고(10분 테코톡): https://www.youtube.com/watch?v=Vv0PGUxOzq0&t=56

제네릭(Generic)


  • 클래스나 메소드에서 사용할 내부 데이터 타입을 외부에서 지정하는 기법이다.
  • 제네릭은 불공변(Invariant)을 띈다.
    X가 Y의 서브타입이여도, 서로 관련이 없어 X는 X로만 사용할 수 있다.
  • 제네릭 클래스 생성하기 (예시)
1
2
3
4
5
6
7
8
9
10
class FruitBox<T> { //T는 타입매개변수
    List<T> fruits = new ArrayList<>();

    public void add(T fruit){
        fruits.add(fruit);
    }
}

// 외부에서 데이터 타입 지정하기
FruitBox<Apple> appleBox = new FruitBox<>();

제네릭을 사용하는 이유?


1. 타입 안정성

컴파일 타임에 자료형의 오류에 대한 검증을 수행하여, 런타임에 자료형에 안전한 코드를 실행할 수 있다.

  • 원래의 의도? (예시)
    과일이라면 종류에 상관없이 과일 박스 객체를 사용할 수 있도록 하고싶으니까, 최상위 클래스 Object로 정의해서 어떤 과일 객체든 접근이 가능하도록 해야겠다!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
      class Apple {}
      class Banana {}
      class FruitBox {
          private Object fruit;
          public FruitBox (Object fruit){
              this.fruit = fruit;
          }
    
          public Object getFruit() {
              return fruit;
          }
      }
    
  • 개발자의 실수!
    AppleBox가 아닌 BananaBox에서 Banana객체를 가져와야하는데, 실수로 AppleBox에서 Apple객체를 가져와 Banana 객체로 캐스팅해버렸다.
    1
    2
    3
    4
    5
    6
    7
    
      public static void main(String[] args){
          FruitBox appleBox = new FruitBox(new Apple());
          FruitBox bananaBox = new FruitBox(new Banana());
    
          Apple apple = (Apple)appleBox.getFruit();
          Banana banana = (Banana)appleBox.getFruit(); // 실수 발생
      }
    

    그러나, 문법적으로는 오류가 없어 개발자는 코드를 실행하기 전까지 해당 오류 사실을 몰랐다.
    ➔ 즉, 자료형에 대한 검증이 컴파일 타임에 이루어지지 않았다.

  • 제네릭을 사용하면 위의 문제를 해결할 수 있다!
    자바 컴파일러는 제네릭 코드에 강한 타입체크를 하기때문에, 타입 안정성에 위배된다면 에러를 발생시킨다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
      class Apple {}
      class Banana {}
      class FruitBox<T> {
          private T fruit;
          public FruitBox (T fruit){
              this.fruit = fruit;
          }
    
          public T getFruit() {
              return fruit;
          }
      }
    

2. 캐스팅 삭제

제네릭은 컴파일 타임에 미리 타입이 정해지므로, 캐스팅 작업이 필요하지않다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 비제네릭 클래스
public static void main(String[] args){
    FruitBox appleBox = new FruitBox(new Apple());
    FruitBox bananaBox = new FruitBox(new Banana());

    Apple apple = (Apple)appleBox.getFruit();
    Banana banana = (Banana)bananaBox.getFruit();
}

// 제네릭 클래스
public static void main(String[] args){
    FruitBox<Apple> appleBox = new FruitBox<>(new Apple());
    FruitBox<Banana> bananaBox = new FruitBox<>(new Banana());
       
    Apple apple = appleBox.getFruit();
    Banana banana = bananaBox.getFruit();
}

제네릭 메소드


  • <> 안에 지정한 타입으로 매개변수의 타입을 지정한다.
  • 타입 파라미터의 범위는 메소드 블록 이내로 한정한다.
  • 제네릭 클래스가 아니여도 정의할 수 있다.
  • 제네릭 메소드 생성하기 (예시)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      class Name {
          public <T> void printClassName(T t){
              System.out.println(t.getClass().getName());
          }
      }
    
      // ...
      // 제네릭 메소드 실행하기
      public static void main(String[] args){
          Name name = new Name();
          name.printClassName(1); // java.lang.Integer
          name.printClassName(3.14); // java.lang.Double
      }
    

타입 매개변수의 제한


  • 타입 매개변수가 특정 클래스의 하위 클래스만 가능하도록 설정할 수 있을까?
    가능하다!!
1
2
3
4
5
6
7
class FruitBox<T> {
    private List<Fruit> fruits = new ArrayList<>();

    public void add(T fruit){
        fruits.add(fruit); // 컴파일 에러 ( Fruit의 하위클래스가 아닐 수 있기때문에 )
    }
}

1. 상한 경계

  • T extends Fruit
    타입 매개변수의 클래스는 Fruit 클래스이거나 Fruit의 하위 클래스여야 한다. (상위 클래스 x) 상한 경계
    1
    2
    3
    4
    5
    6
    7
    
    class FruitBox<T extends Fruit> {
      private List<Fruit> fruits = new ArrayList<>();
    
      public void add(T fruit){
          fruits.add(fruit); // 컴파일 OK
      }
    }
    

2. 하한 경계

  • T super Fruit
    타입 매개변수의 클래스는 Fruit 클래스이거나 Fruit의 상위 클래스여야 한다. (하위 클래스 x) 하한 경계

와일드 카드 vs 제네릭 타입


  • 와일드 카드? 어떤 타입인지 모르고, 무슨 타입인지 신경쓰지 않는다. 제네릭과 달리 공변성을 띈다.
  • 제네릭 타입? 어떤 타입인지 모르지만, 타입이 정해지면 그 특성에 맞게 사용한다.

  • List로 예를 들어보자! 제네릭,와일드카드

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
      static void printList1(List<?> list){
          /**
          1. 와일드 카드는 list에 담긴 원소에 전혀 관심이 없기 때문에, 원소와 관련된 add 메소드를 사용할 수 없다.
          2. 단, null은 가능하다.
          */
    
          list.add(list.get(1)); // 컴파일 에러
      }
    
      static <T> void printList2(List<T> list){
          // 제네릭은 list에 담긴 원소에 관심을 갖기 때문에 원소와 관련된 add 메소드를 사용할 수 있다.
            
          list.add(list.get(1)); // 컴파일 성공
      }
    

공변, 반공변, 불공변??

  1. 공변성?
    • A가 B의 하위 타입일 때, List<A>가 List<B>의 하위 타입이다.
    • 제네릭은 불공변 상태이기 때문에 한정적 와일드 카드 타입을 통해 공변을 지원한다.
    • ex. Collection<? Extends E>로 변경 시, E 타입의 하위 타입을 자바 컴파일러가 인식할 수 있다.
      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
      32
      33
      34
      35
      
       public class Stack<E> {
         private final List<E> stack = new ArrayList<>();
         private int position = -1;
              
         public void push(E element) {
             position += 1;
             stack.add(element);
         }
              
         public E pop() {
             E popElement = stack.remove(position);
             position -= 1;
             return popElement;
         }
              
         public boolean isEmpty() {
             return position == -1;
         }
      
             public int stackSize() {
             return position + 1;
         }
      
         // 1. 한정적 와일드 카드 사용 전 (불공변)
         public void pushAllNotWild(Collection<E> elements) {
             position += elements.size();
             stack.addAll(elements);
         }
      
         // 2. 한정적 와일드 카드 사용 후 (공변)
         public void pushAllWild(Collection<? extends E> elements) {
             position += elements.size();
             stack.addAll(elements);
         }
       }
      
      1
      2
      3
      4
      5
      
       // Number는 Integer의 상위 타입
       Stack<Number> numberStack = new Stack<>();
       List<Integer> integers = Arrays.asList(1, 2, 3);
       numberStack.pushAllNotWild(integers); // 컴파일 에러 발생 (불공변성)
       numberStack.pushAllWild(integers) // 컴파일 OK
      

      와일드카드

  2. 반공변성?
    • 공변성의 반대 경우이다. A가 B의 하위 타입일 때, List<B>가 List<A>의 하위 타입이다.
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
32
33
34
35
36
37
38
public class Stack<E> {
    private final List<E> stack = new ArrayList<>();
    private int position = -1;
    
    public void push(E element) {
        position += 1;
        stack.add(element);
    }
    
    public E pop() {
        E popElement = stack.remove(position);
        position -= 1;
        return popElement;
    }
    
    public boolean isEmpty() {
        return position == -1;
    }

	public int stackSize() {
        return position + 1;
    }

    public void pushAllWild(Collection<? extends E> elements) {
        position += elements.size();
        stack.addAll(elements);
    }

    // 1. 한정적 와일드 카드 사용 전 (불공변)
    public void popAllNotWild(Collection<E> dst) {
        dst.addAll(stack);
    }

    // 2. 한정적 와일드 카드 사용 후 (반공변)
    public void popAllWild(Collection<? super E> dst) {
        dst.addAll(stack);
    }
}
1
2
3
4
5
6
7
Stack<Number> numberStack = new Stack<>();
List<Integer> integers = Arrays.asList(1, 2, 3);
numberStack.pushAllWild(integers);

List<Object> dst = new ArrayList<>();
numberStack.popAllNotWild(dst); // 컴파일 에러 
numberStack.popAllWild(dst); // Object가 Integer의 상위타입이라 상관없음

Number 타입의 list 원소 값을 Object 타입의 list에 넣을라 함
와일드카드

와일드 카드


1. 비경계 와일드카드(Unbounded Wildcards)

  • ? 의 형태로 사용한다. (ex. List<?>)
  • 모든 타입이 인자가 될 수 있다.
    1
    2
    3
    4
    5
    6
    
    public static void printList(List<?> list){
      for (Object ob : list) {
          System.out.println(ob + "");
      }
      System.out.println();
    }
    

비경계 와일드카드의 특징

  1. List<?>에서 Get한 원소는 Object 타입이다.
    🤔 이유는??
    비경계 와일드카드의 원소는 어떤 타입도 될 수 있으므로, 어떤 타입이든 읽을 수 있게 모든 타입의 조상 클래스인 Object로 반환한다.
    1
    2
    3
    4
    
    public static void get(List<?> list){
     Object ob = list.get(0);
     String st = list.get(0); // 컴파일 에러
    }
    
  2. List<?>에는 null만 삽입할 수 있다.
    🤔 이유는??
    비경계 와일드카드의 원소가 어떤 타입인지 알 수 없으므로 타입 안정성을 지키기 위해 null만 삽입이 가능!
1
2
3
4
5
6
7
8
public static void main(String[] args){
    List<Integer> list = new ArrayList<>();
    addDouble(list); // Integer 리스트에 Double 변수가 들어가는 모순 발생
}

private static void addDouble(List<?> list){
    list.add(3.14); // 만약 모든 타입의 값을 넣는게 허용된다면??
}

2. 상한 경계 와일드카드(Uppder Bounded Wildcards)

  • ? extends T 의 형태로 사용한다.
  • T 혹은 T의 하위 클래스만 인자로 올 수 있다.

상한 경계 와일드카드의 특징

  1. List<? extend T>에서 Get한 원소는 T이다.
    • 상한 경계 와일드카드의 원소는 T 혹은 T의 하위 클래스이다.
    • 원소들의 최고 공통 조상인 T로 읽으면, 어떤 타입이 오든 T로 읽을 수 있다.
      1
      2
      3
      4
      5
      6
      
       public static void printList(List<? extends Fruit> fruits) {
         for(Fruit fruit : fruits) {
             System.out.println(fruits + " ");
         }
         System.out.println();
       }
      
  2. List<? extends T>에는 null만 삽입할 수 있다.
    • 상한 경계 와일드카드의 원소가 어떤 타입인지 알 수 없다.
      1
      2
      3
      
       List<Apple> apples = new ArrayList<>();
       List<? extends Fruit> = apples;
       fruits.add(new Banana()); // 컴파일 에러(List<Apple>에 바나나가 들어감)
      

3. 하한 경계 와일드카드 (Lower Bounded Wildcards)

  • ? super T의 형태로 사용한다.
  • T혹은 T의 상위 클래스만 인자로 올 수 있다.

하한 경계 와일드카드의 특징

  1. T 하한 경계 와일드카드의 원소는 T의 상위 클래스 중 어떤 타입도 될 수 있다.
    • 어떤 타입이 와도 읽을 수 있도록, T들의 공통 조상인 Object로 받는다.
      1
      2
      3
      4
      5
      6
      
       public static void printList(List<? super Fruit> fruits){
         for(Object fruit:fruits){
             System.out.println(fruits + "");
         }
         System.out.println();
       }
      
  2. List<? super T>에는 T 혹은 T의 하위 클래스만 삽입할 수 있다.
    • 하한 경계 와일드카드의 원소는 T 혹은 T의 상위 클래스이다.
      1
      2
      3
      4
      
      List<? super Fruit> fruits = new ArrayList<>();
      fruits.add(new Apple());
      fruits.add(new Fruit());
      fruits.add(new Food()); // 컴파일 에러 (Fruit의 상위클래스)
      
    • 🤔 이유는??
      List<Fruit>, List<Food>, List<Object> 중 어떤 리스트가 올지 fruits 배열은 모른다.
      하지만 그 중 어떤 리스트여도, Fruit이나 Fruit의 하위 클래스를 원소로 추가할 수 있으므로
      상위 클래스의 원소를 추가할 수 없다.
This post is licensed under CC BY 4.0 by the author.