자바 스터디 14주차

김성훈·2022년 11월 19일
0

백기선 자바 스터디

목록 보기
14/15
post-thumbnail

자바의 제네릭에 대해 학습하세요.


학습할 것 (필수)

  • 제네릭 사용법
  • 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
  • 제네릭 메소드 만들기
    Erasure

Generics 란

제네릭은 클래스, 인터페이스 및 메서드를 정의 할 때 자료형이 매개 변수가 되도록 한다.
메서드 선언에 사용되는 다른 파라미터들과 마찬가지로 타입 파라미터는 서로 다른 입력으로 동일한 코드를 재사용 할 수 있는 방법을 제공한다.

차이점은 일반적인 매개 변수에 대한 입력은 값이고 타입 매개 변수에 대한 입력은 자료형 이라는 것이다.

JAVA 5부터 추가됨

제네릭만의 용어

제네릭의 장점

컴파일 타임에 더 강력한 타입을 검사한다 (type-safed)

java 컴파일러는 강력한 타입 검사를 코드에 적용하고 코드가 type-safety를 위반하면 오류를 발생시킨다.
컴파일 타임 오류를 수정하는 것은 찾기 어려울 수 있는 런타임 오류를 수정하는 것보다 쉽다.

캐스트 제거

제네릭이 없는 다음 코드는 캐스팅이 필요하다.

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

제네릭을 사용하도록 작성하면 코드를 캐스팅할 필요가 없어진다.

List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);   // no cast

프로그래머가 일반 알고리즘을 구현할 수 있도록 한다.

제네릭을 사용하여 프로그래머는 다양한 유형의 컬렉션에서 작동하고 사용자 정의를 할 수 있으며 안전하고 읽기 쉬운 제네릭 알고리즘을 구현할 수 있다.

제네릭의 특징

  1. 불공변
    제네릭과 비교하여 배열은 공변이다.
    공변이란 Sub 가 Super의 하위타입일 때, Sub[]는 Super[] 의 하위 타입 이라는 뜻이다.
    예를 들어 String 은 Object의 하위 타입이기 때문에 String[] 은 Object[] 의 하위타입이다.
    따라서 밑에 코드는 컴파일 오류를 뱉지 않고, 런타임에서 예외를 던지는 문제가 생긴다.

    그에 반해, 제네릭은 불공변이다.
List<String> 은 List<Object> 의 하위타입이 되지 않는다.
따라서 아래와 같은 코드는 컴파일타임에 에러를 내기 때문에 사전에 차단할 수 있다.

  1. 타입추론
    타입추론은 메서드를 호출하는 코드에서 타입인자가 정의한대로 제대로 쓰였는지 살펴보는 컴파일러의 기능이다.

제네릭 메서드 타입추론
다음과 같은 메서드가 있다, 박스객체의 리스트와 T 타입의 객체 t를 받아서, T 타입의 박스를 만들어 박스객체 리스트에 추가한다.

public Class BoxClass {

  public static <T> void addBox(T t, List<Box<T>> boxes) {
    Box<T> box = new Box<>();
    box.set(t);
    boxes.add(box);
  }
}

원래 이 메서드를 사용하기 위해선 아래와 같이 써야 한다.

 BoxClass.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);

이를 명시적 타입인수라고 한다.
하지만 Java8부터는 타입추론으로 아래와 같이 사용할 수 있게 되었다.

 BoxClass.addBox(Integer.valueOf(10), listOfIntegerBoxes);

컴파일러는 메서드를 호출하는 함수를 보고 제네릭 메서드의 타입 매개변수 T 를 추론할 수 있는 것이다.

객체 타입추론
우리는 컬렉션을 생성할 때 아래와 같이 빈 다이아몬드 연산자를 쓴다 (<>)

List<String> strList = new ArrayList<>();

원래는 아래와 같이 써야 한다

List<String> strList = new ArrayList<String>();
  1. 소거 (Erasure)
    제네릭은 타입의 정보가 런타임에는 소거 된다.
    원소의 타입을 컴파일타임에만 검사하고 보증함으로, 런타임에 타입 정보를 알 수 없게 한다.
    이를 실체화가 되지 않는다 라고 한다

제네릭 사용

제네릭은 크게 클래스와 메서드를 만들 때 사용한다.

1. 제네릭 타입

제네릭 타입은 클래스와 인터페이스에서 타입을 매개변수로 가지는 것을 말한다.

public class 클래스명<T> {...}
public interface 인터페이스명<T> {...}

꺾쇠 괄호( <> )로 구분 된 타입 매개 변수는 클래스 이름 뒤에 온다. 객체가 생성될 때 타입 파라미터를 받는 부분이다.
의미적으로 해당 클래스나 인터페이스 내에서 T 타입을 사용하겠다 라는 뜻이다.

타입 파라미터와 일반 클래스 또는 인터페이스 이름의 차이를 구분하기 위해서 정해진 규칙에 따라 타입 파라미터는 단일 대문자를 사용한다.

public class Box<E> {

    private E in;

    public void push(E element) {
        in = element;
    }

    public E pop() {
        return in;
    }
}

Box 클래스의 타입 매개변수로 <E> 를 사용한 것을 볼 수 있다.
push() 에서는 E 타입을 매개변수로 받고 있다.
pop() 에서는 E 타입을 리턴타입으로 한다.

가장 일반적으로 사용되는 타입 매개변수 이름은 다음과 같다.

  • E-엘리먼트 (Java Collections Framework에서 광범위하게 사용됨)
  • K-키
  • N-숫자
  • T-타입
  • V-값
  • S, U, V 등-2, 3, 4 종

많이쓰는 E 는 어떤 타입도 올 수 있으며, Stack 객체를 만들 떄 결정된다.
만약 Box<String> 으로 객체를 생성한다면, 위 클래스에서 E 는 모두 String 이 되어 동작하게 된다.

    public static void main(String[] args) {
      Box<String> s = new Box<>();

      s.push("hi");
      System.out.println(s.pop());
  }

2. 제네릭 메서드

제네릭 메서드는 타입 매개변수를 사용하는 메소드이다.
제네릭 타입을 선언하는 것과 비슷하지만 제네릭 메서드에서 타입 매개변수의 scope는 선언 된 메소드로 제한된다.

제네릭 메소드의 구문에는 메소드의 리턴 타입 전에 나타나는 꺾쇠 괄호 안에 타입 매개변수 목록이 포함된다.
static 제네릭 메소드의 경우 타입 매개변수 섹션이 메소드의 리턴 타입 전에 나타나야한다.

public <타입 파라미터 . . . > 리턴타입 메소드명 (매개변수, . . . ) { . . . }

public static <타입 파라미터 . . . > 리턴타입 메소드명 (매개변수, . . . ) { . . . }

예)

public class Util {
    public static <T> WitchPot<T> put(T t) {
        return new WitchPot<>(t);
    }
}

---

public class WitchPot<T> {
    private T meterial;

    public WitchPot(T meterial) {
        this.meterial = meterial;
    }

    public static void main(String[] args) {
        String frog = "개구리";
        WitchPot<String> pot = Util.<String>put(frog);

        System.out.println(pot.meterial); //개구리

    }
}

메소드 호출하기 (명시적으로 타입 파라미터 지정)

public class WitchPot<T> {
    private T meterial;

    public WitchPot(T meterial) {
        this.meterial = meterial;
    }
}

컴파일러가 제네릭 메소드의 반환 대상의 타입을 미리 검사하는 타입 추론 기능에 의해서 타입 파리미터는 생략이 가능하게 됐다.

또한 Java SE 8부터는 컴파일러의 타입 추론 개념이 확장되어서 메소드 인자에 포함된 매개변수화된 타입까지 검사한다.

public class WitchPot<T> {
    private T meterial;

    public WitchPot(T meterial) {
        this.meterial = meterial;
    }

    public static void main(String[] args) {
        String frog = "개구리";
        //반환 대상이 WitchPot<String> 인 것을 확인하고 String 으로 추론한다.
        WitchPot<String> pot = Util.put(frog);

        System.out.println(pot.meterial); //개구리

    }
}

바운디드 타입

제네릭 타입에서 타입 인자로 사용할 수 있는 타입을 제한하려는 경우가 있을 수 있다.
예를 들어 숫자에 대해 작동하는 메소드는 Number 또는 해당 하위 클래스의 인스턴스만 허용하려고 할 수 있다.
이것이 바운디드 타입의 용도라고 볼 수 있다.

바운디드 타입 파라미터를 선언하려면 타입 파라미터의 이름, extends키워드, 상위 바운드를 나열한다.

<T extends UpperBound>

여기서의 extends 키워드는 특별하게도 implements의 기능까지 포함하기 때문에 상위 바운드는 인터페이스가 될 수 있다.

또는 여러 개의 상위 바운드를 가질 수 도 있다.

<T extends B1 & B2 & B3>

만약 여러 상위 바운드 중에서 클래스가 있다면 해당 클래스가 가장 앞에 와야한다. 안 그럼 컴파일 에러

<T extends Class1 & Interface1 & Interface2>
public class WitchPot<T> {
    private T meterial;

    public T get() {
        return this.meterial;
    }

    static <U extends Meterial> WitchPot<U> put(U u) {
        return new WitchPot<U>();
    }

    public static void main (String [] args) {
        //Meterial을 상속받은 객체만 받도록 제한되었기 때문에 문자열은 컴파일 에러발생
        WitchPot.put( "나는 문자열.");
    }
}

바운디드 타입은 또 상위 바운드에 해당하는 클래스의 메소드를 코드에서 사용할 수 있다.
그냥 T인 경우에는 아무런 메소드도 코드에 적지 못한것과 비교가 된다.

와일드 카드 타입

<?> 라고 불린다.
와일드카드 타입은 <?>, 한정적 와일드카드 타입인 <? extends E>, <? super E> 의 세가지 이용법이 있다

한정적 와일드카드 타입 : extends

한정적 타입 매개변수와 비슷하다.
~중 아무거나 를 표현하고 싶을 때 쓸 수 있다.

public class Chooser<T> {
    private final List<T> choiceList;

    public Chooser(Collection<T> choices) {
        this.choiceList = new ArrayList<>(choices);
    }
    
    public T choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceList.get(rnd.nextInt(choiceList.size()));
    }
}

이 클래스의 필드 choiceList 에는 Chooser 클래스를 만든 타입의 컬렉션을 넣을 수 있다.

List<Number> numberList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
Chooser<Number> chooser1 = new Chooser<>(numberList); // 가능 
Chooser<Number> chooser2 = new Chooser<>(integerList); // 불가능

이번에도 제네릭의 불공변의 특성 때문에, Integer 는 Number 의 하위 타입이지만

Collection 은 Collection 의 하위 타입이 아니기 때문에 Integer 타입 리스트는 생성자 인수로 넣을 수 없다.

이때 한정적 와일드카드 타입을 사용할 수 있다.

   public Chooser(Collection<? extends T> choices) {
        this.choiceList = new ArrayList<>(choices);
    }

<? extends T> 는 T 타입을 포함한 T 의 하위타입 아무거나 와 같은 뜻이다.
이렇게 바꾸면 불가능하던 Integer 타입 리스트도 생성자의 인수로 넣을 수 있다.

한정적 와일드카드 타입 : super

public class Box<E> {

    private final List<E> sockets;

    public Box() {
        this.sockets = new ArrayList<>();
    }

    public void putItems(E object) {
        sockets.add(object);
    }

    public List<? super E> getNewItemList(List<? super E> list) {
        list.addAll(sockets);
        return list;
    }




    public static void main(String[] args) {
        Box<String> strBox = new Box<>();
        strBox.putItems("hi");
        strBox.putItems("bye");

        List<Object> objectList = new ArrayList<>();

        System.out.println(strBox.getNewItemList(objectList));// [hi, bye]


    }
}

Box 는 제네릭 타입이고 E 라는 타입 매개변수를 가지는 리스트 socket 를 필드로 가지고 있다.
socket 에 요소를 집어넣는 putItem 이라는 메서드를 가지고 있다.
getNewItemList() 는 새로운 리스트를 줘서 socket 의 내용을 거기다가 복사해 담아서 반환하는 메서드이다.

옮겨담을 리스트는 E의 상위타입이면 옮겨 담을 수 있을 것이다.

코드의 예와 같이, List 의 내용물인 String 타입은, List 의 내용물인 Object 타입에 옮겨담을 수 있다.

따라서 옮겨담을 리스트의 타입 매개변수는 sockect 의 타입 매개변수의 상위타입이면 옮겨담을 수 있는 것이다.

<? super E> 는 이 의미를 완전히 내포한다. E 의 상위타입 아무거나 라는 뜻이다.

출처

https://docs.oracle.com/javase/tutorial/java/generics/types.html
https://alkhwa-113.tistory.com/entry/%EC%A0%9C%EB%84%A4%EB%A6%AD
https://rockintuna.tistory.com/102

profile
"한 명이 걷는 천 걸음 보다 천 명이 함께 걷는 한 걸음이 성공의 시작이고 완성이다"

0개의 댓글