개발/Java, Kotlin

[WhiteShip][14주차] 제네릭

Kangjieun11 2022. 11. 13. 02:34
728x90

 

 

 

 

WhiteShip Java Live Study 의 커리큘럼을 따라 개인적으로 공부한 내용입니다.

 

 

 

 


Codestates BE 42 : Study, Commercise 01 Java

 

Thanks to

" Codestates_SEB_BE_42 : Commercise "

강지은, 김례화, 전진우

22.11.12

 

 

 


 

 

14주차 : 제네릭 

 

 

목차

  • 제네릭 용어 정리
  • 제네릭
  • 제네릭 사용법
  • 제네릭 클래스와 메소드
  • 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
  • Erasure
  • + 다이아몬드 연산자 <> (자바7)

 

 


 

✅ 제네릭 용어 정리

class Box<T> {}

Box<String> b = new Box<String>();
  • Box<T> : 제네릭 클래스 - T의 Box 또는 T Box 라고 읽는다.
  • T : 타입변수 , 타입매개변수 ( T 는 타입문자 )
  • Box : 원시타입 ( raw type)
  • 제네릭 타입 호출
    • 타입 매개변수에 타입을 지정하는 것
  • 파라미터화된 타입
    • 예시에서 지정된 타입 String
    • 매개변수화 된 타입
    • parameterized type

 

✅ 제네릭 (Generics)

다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스컴파일 시 타입체크를 해주는 기능

 

  • 자바5에서 추가
  • 잘못된 타입이 사용될 수 있는 문제를 컴파일 과정에서 제거 가능
  • 타입 변환을 제거함
  • 상속이 가능하다.
  • 기본 타입은 사용불가함.

 

⏺ 제네릭의 장점 (사용하는 이유 WHY)

  • 타입안전성을 제공
    • 타입안전성이란
      1) 의도하지 않은 타입의 객체를 저장하는 문제를 막음
      2) 저장된 객체를 꺼낼 때 형변환되어 발생할 수 있는 오류를 줄임 → 타입변환이 발생하지 않게 도와줌
    • 객체의 타입을 컴파일시 체크하기 때문이다.
    • 컴파일 타임에 타입의 안정성을 보장받는것이 가능함.
  • 타입체크, 형변환을 생략할 수 있어서 코드가 간결해짐

 


✅ 제네릭 사용법

 

⏺ 타입변수 < T >

 

제네릭 타입 class<T> , interface<T>

클래스나 인터페이스 뒤에 <>부호를 붙히고, 사이에 타입파라미터가 위치한다. 

  • 일반적으로 타입변수는 대문자 알파벳 한글자로 표현하고, 반드시 T를 사용하지는 않는다. 
public class 클래스명 <T> { }
public interface 인터페이스명 <T> { }

 

명명규칙 (가독성을 위함!)

타입 의미
T Type
E Element
N Number
K Key
V Value
S, U, V … 2nd, 3rd, 4th types

 

 

⏺  타입변수에 실제 타입 지정

객체 생성시 실제 타입을 대입해 지정해준다.

ArrayList<Tv> tvList = new ArrayList<Tv> ();

public class ArrayList<E> extends AbstractList<E>{
  private transieont E[] elementData;
  public boolean add(E o){ /*생략*/ }
  public E get(int index){ /*생략*/ }
}

 

⏺ 멀티 타입

제네릭 타입이 두개 이상인 경우 컴마구분자(,) 로 나열해 사용

public class Product <T, M>{
  private T kind;
  private M model;
  
  public T getKind() {return this.kind;}
  public M getModel() {return this.model;}
  
  public void setKind(T kind){
    this.kind = kind;
  }
  public void setModel(M model){
    this.model = model;
  }
}

 

 

⏺ 제네릭 타입의 다형성

참조변수에 지정해주는 제네릭 타입과  생성자에 지정해주는 제네릭 타입일치해야한다. 

ArrayList<Tv>      list1 = new ArrayList<Tv>();  // 가능
ArrayList<Product> list2 = new ArrayList<Tv>();  // 에러

class Product {}
class Tv extends Product {}

 

 

* 클래스 타입간 다형성 적용은 가능하다.

List <Tv> list1 = new ArrayList<Tv>();  // 가능
List <Tv> list2 = new LinkedList<Tv>();  // 가능

 

 

* Product의 자손 객체만 저장하고 싶다면, Product로 제네릭타입을 생성하고, 자손(Tv...) 객체를 저장하면 된다. 

ArrayList<Product> list = new ArrayList<Product>();  
list.add(new Product());
list.add(new Tv());

 

 

⏺ Iterator<E>

iterator에도 제네릭이 적용되어 있다.

제네릭이 도입되면서 이터레이터(반복자)를 적용해 해당 타입의 next()값을 가져올 수 있는것임 !

public interface Iterator<E>{
  boolean hasNext();
  E next();
  void remove();
}

 

 


 

✅ 제네릭 클래스와 메소드

 

⏺  제네릭 클래스

interface Plant { ... }
class Flower implements Plant { ... }
class Rose extends Flower implements Plant { ... }

// 특정 타입만 지정가능하도록 제한 
class Basket<T extends Flower & Plant> {
    private T item;
	
		public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }

		// 에러 : static 메서드 안에서 T 사용 불가능
		static int compare(T t1, T t2){...} 
}

public static void main(String[] args) {

		// 인스턴스화 
		Basket<Flower> flowerBasket = new Basket<>();
		Basket<Rose> roseBasket = new Basket<>();
}

 

클래스의 타입 매개변수는 클래스가 인스턴스화 될때 타입이 확정된다.

 

 

💥 주의할점 💥 

 

1)  static은 모든 객체에 대해 동일하게 동작하기 때문에,

static멤버(필드,메서드)에 제네릭 클래스의 타입변수 T를 사용할 수 없다.

(T는 인스턴스 변수로 간주)

class Box <T>{
  static T item ; // 에러
}
// static을 쓰면, Box<tool>.item 과 Box<Chicken>.item은 다른것이 되면 안된다는 의미임
// 공구와 치킨은 다른것이라 모순이 됨

 

2) 제네릭 타입 배열을 생성할 수 없다. 

  • 제네릭 배열 타입의 참조변수를 선언하는 것은 가능
  • 배열 생성하는것은 불가능하다. (new연산자 때문에)
    • new는 컴파일 시점에 T가 무엇인지 정확하게 알아야하는데,
      배열을 생성하면, T는 런타입에 결정되기 때문에 생성할 수 없는것!
class Box <T>{
	T[] items;
    
    T[] toArray(){
    	T[] tmp = new T[item.length]; // 에러 : 제네릭 배열은 생성 불가
        return tmp
    }
}

 

3) 같은 이유로 instanceOf 역시 피연산자로 T 사용 불가능하다.

 

 

 

⏺  제네릭 메소드? 

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

 

  • 타입 매개변수 선언은 반환타입 앞에 작성된다.
    • 해당 메서드 내에서만 선언한 타입 매개변수 사용 가능
  • 제네릭 메서드는 제네릭 클래스가 아닌 클래스에도 정의 될 수 있다.
  • 제네릭 메서드의 타입 매개변수는 제네릭 클래스의 타입 매개변수와 다른것이다.
    • 클래스의 타입매개변수는 클래스가 인스턴스화 될때 타입이 확정된다.
    • 반면 제네릭 메서드의 타입 지정은 메서드가 호출될 때 이루어진다.

 

// 1 : 여기에서 선언한 타입 매개변수 T와 
class Basket<T> {                        
		...
		// 2 : 여기에서 선언한 타입 매개변수 T는 서로 다른 것입니다.
		public <T> void add(T element) { 
	
				...
		}

		// static 메서드에도 선언 가능
		static <T> int setPrice(T element) {
				...
		}

		// 메서드 정의 시점에서 사용 불가능한 경우 
		public <T> void print(T item) {
        System.out.println(item.length()); // 불가 : 정의하는 시점에서 item이 String 인지 알 수 없으므로 
				System.out.println(item.equals("Kim coding")); // 가능 : Object 의 메서드는 활용 가능 
    }
}

 

 

 


 

 

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

 

Bounded Type Parameters  (제한된 제네릭 클래스)

타입 매개변수 T 에 지정할 수 있는 타입의 종류를 제한하는 방법

// bounded type parameters 미사용 
public static <T> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e > elem)  // compiler error
            ++count;
    return count;
}

// bounded type parameters 사용시
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;
}
  • 특정 타입만 받도록 지정 가능 ( == 바운디드 타입 매개변수 == 경계 타입 매개변수)
    • extends 키워드를 사용하여 특정 타입의 자손들만 대입가능하게 설정 가능
    • 인터페이스나 클래스 관계없이 extends 를 사용
    • 여러 개를 사용시 & 사용
      • 여러 타입을 제한하고 싶을때 & 사용시 클래스가 인터페이스보다 앞쪽에 위치해야한다.
  • 예시에서 위의 경우는 매개변수로 받는 elem 이 비교가 가능한대상인지 확정되지 않아 컴파일 에러가 발생하게 된다.
    • 컴파일 타임에 에러가 난다는것 역시 중요한 점이다. : 타입을 지정하여 컴파일 타임에 타입의 안정성을 보장
  • 이를 아래의 경우로 변경하여 경계타입매개변수를 <T extends Comparable<T>> 와 같이 지정하여 비교가 가능한 타입만 받도록 제한하여 해결할 수 있다.

 

💻 정수, 실수 관계없이 숫자만 사용하도록 제한

public class GenericClass<T extends Number> {
    private T value;

    public GenericClass(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

→  String은 Number를 상속받은 객체가 아니라서 컴파일 에러 발생

 

 

⏺ 와일드 카드 ( ? )

어떤 타입도 받을 수 있게 사용하는 것.

 

  • 제네릭 타입에 다형성을 적용하는 방법이다
  • 의미하는 바를 명확히 하기 위한 목적이 있다. 

 

상한제한(Upper Bound), 하한제한(Lower Bound), 제한 없음(Unbounded Wildcard) 존재

<? extends T> // 상한 제한, T와 그 자손들 만 가능
<? super T>   // 하한 제한 ,T와 그 조상들만 가능
<T>           // 제한 없음   == <? extends Object>
  • 와일드카드는 상한/하한 둘 다 지정할 수 없다.

 

 

1️⃣ 상한 제한 (Upper Bound Wildcard)

  • 상한 와일드카드를 사용해 변수에 대한 제한을 완화할 수 있다.

    Example
    List<Integer> , List<Double> 및 List<Number> 에서 작동하는 메서드를 작성하려고 한다고 가정


    List<Number>
     는 List<? extends Number>보다 더 제한적이다
    왜?  전자가 Number 유형과 일치여부를 체크
            후자는
     Number 유형 또는 그 하위 유형들과  일치여부 체크

sumOfList : 목록에 있는 숫자의 합계를 반환

public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list)
        s += n.doubleValue();
    return s;
}

<? extends Number> : Number과 Number의 하위 클래스인, Integer, Double모두 가능하다.

 

 

 

List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));

 

List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld));

 

 

2️⃣ 하한 제한 (Lower Bound Wildcard)

특정 유형 또는 해당 유형의 super유형 으로 제한한다.

 

example 
Integer
 개체를 목록에 넣는 메서드를 작성한다고 가정

 

유연성을 최대화하려면 메서드가 List<Integer> , List<Number>  List<Object>에서 작동하게 해야함.

즉,  Integer  을 보유할 수 있는 타입은 모두 작동할 수 있어야한다.

List<Integer> 라는 용어 는 List<? super Integer> 보다 더 제한적이다
왜?  전자는 Integer 유형과 일치여부를 체크,
        후자는 Integer또는 Integer의 상위 유형들과  일치여부를 체크

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

 

 

 

3️⃣ 제한 없음(Unbounded Wildcard) 

 

unbounded wildcard가 유용한 경우는 2가지이다.

- Object 클래스 에서 제공하는 기능을 사용하여 구현할 수 있는 메서드를 작성하는 경우 .

- 코드가 형식 매개변수에 의존하지 않는 제네릭 클래스의 메서드를 사용하는 경우. 
ex : List.size,  List.clear  

- Class<?>는   :::  Class<T> 에 있는 대부분의 메서드가 T 에 의존하지 않을 때 자주 사용된다.

 

 

printList 메소드 :   any type의 List를 출력하는 것목표라고 하자.

 

근데 아래 코드는 Object 인스턴스만 출력할 수 있다. (목표 달성 실패!)

→  List<Integer>, List<String>, List<Double>은 불가능

 

WHY ? ::   List<Object>의 subtype이 아니기 때문이다.이런 경우에 List<?>를 써야한다. 

public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}
public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}
List<Integer> li = Arrays.asList(1, 2, 3);
List<String>  ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);

 

* List<Object> 와 List<?> 는 같지 않다는 점에 유의할 것.

 

++ List 와 List<Object> 의 차이 

List<Object>는 List의 다른 매개변수 타입과 와 호환되지 않는다 (Object만 허용한다.)

그러나 List(raw)는 모든 매개변수 List와 호환된다.

 

출처 : https://stackoverflow.com/questions/7885920/java-difference-between-list-and-listobject

 

  • 어떤 메서드에 파라미터로 List 나 List<Object> 가 명시된 상황에서의 차이
  • List 는 명확히 제네릭이 아님
    • raw type 이며 제네릭 이전의 사용과의 호환성으로 만들어졌음
    • 따라서 List<String>, List<Integer> 과 같은 모든 타입을 수용가능하다.
  • List<Object>는 Object 타입을 허용한다는 의사를 컴파일러에게 명확히 전달하는 꼴
    • List<Object> 가 아니고는 수용이 불가능하다. 
      • List 와 List<Object> 타입은 전혀 다른 파라미터화된 타입을 가지며 각각 하나의 독립된 타입이므로 서로를 관계가 없다.
      • 타입 매개변수가 일치하고 두 클래스가 상속관계에 있다면 상속이 가능하다.
        • Box → SteelBox
List<String> a = new ArrayList<String>();

List<Object> b = a; // compile error

List c = a; // fine

 

 

 

 제네릭 타입의 형변환

  • 제네릭타입과 원시타입간의 형변환은 가능하다. (단, 경고가 발생한다.)
  • 대입된 타입(파라미터화 된 타입)이 다른 제네릭 타입간의 형변환
    • 대입된 타입이 Object 일지라도 대입된 타입이 다르면 형변환이 불가능하다.
    Box<Object> objBox = null;
    Box<String> strBox = null;
    
    objBox = (Box<Object>) strBox; // 에러
    strBox = (Box<String>) objBox; // 에러
    
    Box<? extends Object> wBox = new Box<String>(); // OK
    • Box 를 Box 으로 직접 형변환하는것은 불가능
    • 와일드카드가 포함된 제네릭 타입으로 형변환 하면 가능 (단, 확인되지않은 타입으로 형변환 한다는 경고 발생)
    • 마찬가지로 와일드카드가 사용된 제네릭 타입끼리도 형변환이 가능하다.

 

 

 

💻 와일드 카드 예시

// 와일드 카드 사용 이전 코드 
// 과일박스를 넣으면 주스를 만들어 반환하는 Juicer
class Juicer {
		... 
		// 타입매개변수 대신 특정 타입을 지정해준 상황 
		static Juice makeJuice ( FruitBox<Fruit> box) {
				String temp = "";
				for (Fruit f : box.getList()) {
						temp += f + "";
				}
				return new Juice(temp);
		}
}
  • 우선 Juicer는 제네릭 클래스가 아니다. 또한 makeJuice()는 static 메서드이다.
  • FruitBox 로 고정시 FruitBox 과 같은 다른 타입을 매개변수로 대입할 수 없다.
    • 타입이 맞지 않으므로
  • 메서드 오버로딩을 통해 만들면 되지 않나 ?
    • 제네릭 타입이 다른것만으로는 오버로딩이 성립하지않는다
      • 제네릭 타입은 컴파일러가 컴파일 할때만 사용하고 제거해버린다.
  • 이같은 상황에서 와일드 카드를 도입하여 사용한다.
    • 위의 예시에서 와일드 카드 도입시
static Juice makeJuice ( FruitBox<? extends Fruit> box) { }
  • FruitBox< 모든 종류 Fruit 및 그 이하 클래스> 를 수용 가능하게된다.

 

 

누군가 말한 요약.. 아직 이해 불가능

제네릭  : 지금은 이 타입을 모르지만, 이 타입이 정해지면 그 타입 특성에 맞게 사용하겠다.
와일드 카드 : 지금도 이 타입을 모르고, 앞으로도 모를 것이다.  

 

 

 


 

 

✅ Erasure  (삭제, 소거)

제네릭을 사용하면 그 타입은 컴파일 후에 지워지게 된다. 

 

즉 컴파일 타임에만, 타입에 대한 제약 조건을 적용하고,

컴파일러가 타입변수를 실제 유형으로 바꿔버리기 때문에  런타임에는 타입에 대한 정보를 소거해 해당 타입 정보를 알 수 없다.

 

List<String> myList = new ArrayList<String>(); // 코딩 내용
ArrayList myList = new ArrayList(); // 디컴파일 결과

변수명은 동일하지만 제네릭 타입 파라미터는 제거되었다. 

→   타입 파라미터는 컴파일러에 의해 해석되는 부분이고 자바 가상 머신에서는 해석이 되지 않기 때문이다. 
그래서 제네릭은 런타임에 체크하는 것이 아니라 컴파일 시에 정합성을 체크하게 된다.

 

→  자바 가상머신은 제네릭을 고려하지 않고 실행된다.  (제네릭이 제거된 기본 클래스형으로만 처리)

 

→  가상머신상에서 제네릭 코드를 제거하는 이유는

제네릭을 해석하기 위해 추가적인 자원 소모를 없애고,  빠르고 명확하게 동작하도록 하기 위해서 사용한다.

 

 

 

⏺ 제네릭 타입 제거

1) T를  실제 타입 파라미터 / Object로 바꾼다.

2) 타입 안전성 보존을 위해 필요시 type casting을 넣는다.

- 확장된 제네릭 타입에서 다형성 보존을 위해  3) bridge method를 생성한다.

 

 

 

1-1 ) 컴파일러가 <E>는 unbouned이기 때문에 E를 Object로 바꿔준다.

public class Stack<E> {
    private E[] stackContent;

    public Stack(int capacity) {
        this.stackContent = (E[]) new Object[capacity];
    }

    public void push(E data) {
        // ..
    }

    public E pop() {
        // ..
    }
}
public class Stack {
    private Object[] stackContent;

    public Stack(int capacity) {
        this.stackContent = (Object[]) new Object[capacity];
    }

    public void push(Object data) {
        // ..
    }

    public Object pop() {
        // ..
    }
}

 

 1-2) 만약 bound 되었다면

제네릭 타입의 경계(bound)를 제거한다.

첫번째로 바운드된 클래스 (예제 코드에선 Comparable이다.) 로 바꾼다. 

public class BoundStack<E extends Comparable<E>> {
    private E[] stackContent;

    public BoundStack(int capacity) {
        this.stackContent = (E[]) new Object[capacity];
    }

    public void push(E data) {
        // ..
    }

    public E pop() {
        // ..
    }
}
public class BoundStack {
    private Comparable [] stackContent;

    public BoundStack(int capacity) {
        this.stackContent = (Comparable[]) new Object[capacity];
    }

    public void push(Comparable data) {
        // ..
    }

    public Comparable pop() {
        // ..
    }
}

 

2)  제네릭 타입을 제거한 후 타입이 일치하지 않으면 형변환을 추가한다.

  • List의 get() 의 경우 Object 타입을 반환하므로 형변환이 필요하다.
  • 따라서 형변환을 추가해준다.
  • 와일드 카드 가 포함된 경우 역시 적절한 형변환과정을 추가한다.
T get(int i) {
    return list.get(i); // list 의 get() 은 Object 형을 반환
}

----------------------------------

Fruit get(int i) {
    return (Fruit) list.get(i); 
}

 

 

3) 브릿지 메서드

 

  • 매개변수화된 클래스를 확장하거나, 메소드 서명이 약간 다르거나,
    모호할 수 있는 매개변수화된 인터페이스를 구현하는 클래스 또는 인터페이스를 컴파일하는 동안
    Java 컴파일러에 의해 생성된 합성 메소드
  • 메서드의 매개변수나 리턴타입을 소거하기 위해서 만들어지는 메서드 
public class SomeList extends ArrayList<String> {
  @Override
  public boolean add(String e) {
      ...
  }
}
void Test() {
  ArrayList<String> list = new SomeList();
  list.add("string");
}
  • ArrayList의 타입소거된 add() 메서드를 호출할 경우 SomeList에는 add(Object e) 메서드가 존재하지 않기 때문에 이를 위해 브릿지 메서드를 생성한다.

 

 

+ 다이아몬드 연산자 (자바 7)

* 객체 선언부에 <> 를 기술하면 변수 선언 시에 사용한 제네릭 타입이 그대로 적용 됨

Map<String, List<String>> myMap = new HashMap<>();

 

* 다이아몬드 연산자는 오직 객체를 생성하는 부분에서만 사용할 수 있다.

아래는 컴파일러가 제네릭의 타입을 추정할 때 변수에 선언해 놓은 파라미터를 바탕으로 추정하기 때문에 컴파일 에러 발생

 

Map<> myMap = new HashMap<String, List<String>>(); // 컴파일 에러 발생

 

* (자바 10) 변수를 선언 시 특정 클래스 지정안하고, 타입추론 가능하게 var 사용가능

var myMap = new HashMap<String, List<String>>();

 

 

 


 

references