[F-Lab 챌린지 4일차] TIL : 자바의신 21 장 - 제네릭

성수데브리·2023년 7월 2일
0

f-lab_java

목록 보기
3/73
post-thumbnail

학습 목표


  • 제네릭
  • 실습 코드

학습 결과 요약


  • 제네릭으로 타입 형변환 과정에서 발생할 수 있는 오류를 컴파일 타임에서 점검할 수 있다.

학습 내용


제네릭 도입 이유

예시를 통해 이해해보자.

public class CastingDTO {

    private Object object;

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

    public Object getObject() {
        return object;
    }
}
public class GenericSample {

    public static void main(String[] args) {
        GenericSample sample = new GenericSample();
        sample.checkCastingDTO();

    }

    private void checkCastingDTO() {
        CastingDTO dto1 = new CastingDTO();
        dto1.setObject(new String());

        CastingDTO dto2 = new CastingDTO();
        dto2.setObject(new StringBuffer());

        CastingDTO dto3 = new CastingDTO();
        dto3.setObject(new StringBuffer());
		}
}

setter 메서드 매개변수 타입이 Object 로 자동 형변환 덕분에 setter 를 호출할 때 매개변수로 어떤 타입이든 넘겨줄 수 있다.

다만, getter 를 사용할 땐 문제가 된다. getter 의 리턴 타입이 Object 여서 instanceof 연산자로 타입을 확인해야 한다.

private void checkDTO(CastingDTO dto) {

    Object object = dto.getObject();
    if (object instanceof String) {
        System.out.println("String type");
    } else if (object instanceof StringBuilder) {
        System.out.println("StringBuilder type");
    } else {
        System.out.println("StringBuffer type");
    }
}

이렇게 매번 타입을 점검해야 할까? 또한 개발자 실수로 타입 캐스팅을 잘못 선언할 수 있지도 않을까?
이런 단점을 보완하기 위해 Java 5 부터 Generic 이란것이 도입되었다.

Generic

  1. 제네릭은 타입 형 변환에서 발생할 수 있는 문제점을 컴파일 타임에서 점검할 수 있다.
  2. 캐스팅 코드 제거
  3. 타입을 파라미터화 함으로써 코드 재사용을 가능하게 한다.
public class GenericCastingDTO<T> implements Serializable {
    
    private T object;

    public T getObject() {
        return object;
    }

    public void setObject(T object) {
        this.object = object;
    }
}
GenericCastingDTO<String> dto = new GenericCastingDTO<>();
dto.setObject("String");

GenericCastingDTO<StringBuffer> dto1 = new GenericCastingDTO<>();
dto1.setObject(new StringBuffer());

GenericCastingDTO<StringBuilder> dto2 = new GenericCastingDTO<>();
dto2.setObject(new StringBuilder());

// 제네릭을 사용하면 형 변환이 필요없다.
StringBuilder builder = dto2.getObject();
StringBuffer buffer = dto1.getObject();
String str = dto.getObject();

매개변수 타입을 Object로 선언했을 때와 달리 제네릭을 사용하면 잘못된 타입으로 치환하더라도 컴파일 자체가 되지 않는다.

Generic Class

선언 방법

class name<T1, T1, ... T> {

}

클래스 선언부의 클래스 이름 뒤에 <> 꺾쇠 사이에 타입 파라미터를 나열하면 된다.

제네릭 클래스 호출과 초기화하기

  • 타입 파라미터 T 를 실제 타입으로 치환하면 된다. 이때 T 는 type parameter 실제 치환 타입을 Type arguments 라 한다.
  • 실제 타입으로 치환된 상태를 parameterized type 이라 한다. Box<Integer>
Box<Integer> box = new Box<>();
public class Box<T> {

    private T t;

    public T getT() {
        return t;
    }

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

복수개의 타입 파라미터

<> 꺽쇠를 Diamond 라 칭한다.

<> 안에 타입 파라미터를 콤마로 나열하면 된다.

public class Pair<K, V> {
    
    private K k;
    private V v;

    public Pair(K k, V v) {
        this.k = k;
        this.v = v;
    }

    public K getK() {
        return k;
    }

    public V getV() {
        return v;
    }
}

입력된 매개변수 타입을 보고 컴파일러가 type argument 의 타입을 추론한다.

Pair<Integer, String> value = new Pair<>(1, "value");
Pair<String, Integer> key = new Pair<>("key", 1);

**Parameterized Types**

타입 파라미터를 parameterized type 로 치환할 수도 있다.

Pair<Integer, ArrayList<Integer>> pair = new Pair<>(1, new ArrayList<Integer>());

**Raw Types**

raw types 은 제네릭 클래스를 사용할 때 type argument 를 제거하고 사용하는 것이다.

Box box = new Box<>();

언뜻보면, 일반 클래스처럼 사용하는 것 같지만 제네릭 클래스를 위와같이 사용하는 경우를 raw types 이라한다.

raw types 형태로 사용하는 경우가 등장하는데 그 이유는 아래와 같다.

Raw types show up in legacy code because lots of API classes (such as the Collections classes) were not generic prior to JDK 5.0.

raw types 형태로 사용하면 아래 코드는 문제없이 실행된다.

컴파일러가 타입 추론을 못하기 때문에 컴파일 시 타입 안정성을 보장하지 못한다.

Box rawBox2 = new Box();
rawBox2.set("S");
System.out.println(rawBox2.get());
rawBox2.set(1);
System.out.println(rawBox2.get());

Generic Methods

메서드를 제네릭하게 선언할 수 있다.

매개 변수로 사용된 객체에 값을 추가할 수 있다.

public class GenericList {

    public <T> void add(List<T> list, T value) {
        list.add(value);
    }
}
  public static void main(String[] args) {
      
      GenericList genericList = new GenericList();
      
      ArrayList<Pair<String, Integer>> arrayList = new ArrayList<>();

      genericList.add(arrayList, new Pair<>("key1", 1));
      genericList.add(arrayList, new Pair<>("key2", 1));
  }

**Bounded Type Parameters**

타입 파라미터 T 에 어떤 타입이든 치환할 수 있다.

Bounded Type Parameters 란 type argument 에 타입의 범위를 지정하는 것이다.

Box<Integer> box = new Box<>();
Box<String> box = new Box<>();
Box<Double> box = new Box<>();
Box<Long> box = new Box<>();
...
public class Box<T> {

    private T t;

    public T getT() {
        return t;
    }

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

방법은 타입 파라미터에 extends 타입 을 지정하면 된다.

public <U extends Number> void inspect(U u) {
    System.out.println("T: " + t.getClass().getName());
    System.out.println("U: " + u.getClass().getName());
}

U u 매개변수는 Number 의 하위 클래스만 올 수 있다.

아래처럼 문자열을 매개변수로 넘기면 컴파일 에러로 잡을 수 있다.

public static void main(String[] args) {

      Box<Integer> box = new Box<>();
      box.inspect("234");

  }

타입 범위를 지정할 때 여러 타입을 나열할 수 있다. & 기호로 나열하면 된다.

<T extends B1 & B2 & B3>

Type Inference

타입 추론이란 컴파일러가 제네릭 클래스나 제네릭 메서드가 사용될 때 타입을 추론하여 결정하는 것을 말한다.

Wildcards

제네릭에서 wildcard 는 ? 키워드로 표시하는데 unknown type 을 의미한다.

와일드 카드를 사용하면 좋은 경우는

  • Object 클래스에 구현된 기능을 사용하는 메서드를 만들 때
  • 타입 파라미터에 치환되는 실제 타입과는 관련없는 메서드를 만들 때

printList 메서드 목적은 toString을 호출해 출력하는 것이다.

문제는 Object 타입의 원소만 처리 가능하므로 List, List등 다양한 타입의 List를 매개변수로 받을 수 없다.

public static void printList(List<Object> list) {

    for (Object elem : list) {
        System.out.println("elem = " + elem);
    }
}
public static void main(String[] args) {

    List<Car> cars = List.of(
            new Car("BMW"),
            new Car("Porsche")
    );
		// 컴파일 에러 발생함.
    printList(cars);
}

와일드카드를 사용하면 다양한 타입을 받을 수 있다.

public static void printList(List<?> list) {

    for (Object elem : list) {
        System.out.println("elem = " + elem);
    }
}

Type Erasure (좀 더 공부해보자)

type erasure란 컴파일러가 제네릭 파라미터를 실제 타입 또는 bridge 메서드로 교체하는 과정을 뜻한다.

type erasure 를 사용하면 컴파일러가 추가 클래스 생성이 필요하지 않고, 런타임 오버헤드가 발생하지 않음을 보장한다.

  • 타입 파라미터 교체
    • 제네릭의 bounded type 은 bound type 으로 교체

    • unbounded type 은 Object 로 교체된다

Type erasure 예시 코드

package org.flab.chapter21;

/**
 * @author gutenlee
 * @since 2023/07/02
 */
public class Node<T> {

    public T data;

    public Node(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}
package org.flab.chapter21;

/**
 * @author gutenlee
 * @since 2023/07/02
 */
public class MyNode extends Node<Integer> {

    public MyNode(Integer data) {
        super(data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }

}

myNode 변수를 부모 타입으로 형변환 한뒤 setData() 매개변수로 String 값을 전달하면

String 을 Integer 로 형변환할 수 없다는 ClassCastException 발생한다.


public static void main(String[] args) {
    MyNode myNode = new MyNode(3);
    Node n = myNode;
    n.setData("Hello"); // -> Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer
    Integer x = myNode.data;
}

type erasure 사용하는 이유

  • 제네릭 도입 이전의 코드와 호환성을 위해
  • 메모리 오버헤드를 줄일 수 있다 → 아직 잘 모르겠음!

0개의 댓글