자바-14(제네릭)

dragonappear·2021년 3월 28일
0

Java

목록 보기
14/22

학습할 것 (필수)

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

1. 제네릭 사용법

- 제네릭이란?

  • 데이터 타입(data type)을 일반화(generalize)하는 것을 의미한다.
  • 제네릭은 클래스나 메서드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법이다.
  • 이렇게 컴파일 시 type check를 하면 장점이 있다.
    • 클래스나 메서드 내부에서 사용되는 객체의 타입의 안정성을 높일 수 있다.
    • 반환값에 대한 타입 변환 및 타입 검사에 들어가는 노력을 줄일 수 있다.
  • JAVA 5 이전에는 여러 타입을 사용하는 대부분의 클래스나 메서드에서 인수나 반환값으로 Object 타입을 사용했었다. 하지만 이 경우에 반환된 Object 객체를 다시 원하는 타입으로 타입을 변환해야하고, 이 때 오류가 발생할 가능성도 생긴다. 하지만 Java5부터 도입된 제네릭을 사용하면 컴파일 시에 미리 타입이 정해지므로, 타입 검사나 타입 변환과 같은 번거로운 작업을 생략할 수 있게 된다.

- 제네릭을 사용해야하는 이유

  • 제네릭 타입을 사용함으로써 잘못된 타입이 사용될 수 있는 문제를 컴파일 과정에서 제거할 수 있기 때문이다.
  • 자바 컴파일러는 코드에서 잘못 사용된 타입 때문에 문제점을 제거하기 위해 제네릭 코드에 대한 강한 타입체크를 한다. (강타입)
  • 실행 시 타입 에러가 나는 것보다 컴파일시 미리 타입을 강하게 체크해서 에러를 사전에 방지하는 것이 좋다.
  • 제네릭 코드를 사용하면 타입을 국한하기 때문에 요소를 찾아올 때 타입 변환을 할 필요가 없어 프로그램 성능이 향상 되는 효과를 얻을 수 있다.

- 제네릭 사용법

제네릭의 선언 및 생성

package me.whiteship.livestudy.week14;

public class GenericSample<T> {
    T element;
    public void setElement(T element) {
        this.element = element;
    }

    public T getElement() {
        return element;
    }
}

타입 변수

  • 아무런 이름이나 지정해도 컴파일하는데 전혀 상관이 없다.
  • 현존하는 클래스를 사용해도 되고 존재하지 않는 것을 사용해도 된다.
  • 임의의 참조형 타입을 의미한다.
  • 꼭 'T'를 사용안하고 어떠한 문자를 사용해도 되지만 아래의 네이밍을 지켜주는 것이 좋다.
  • 여러 개의 타입변수는 쉼표(,)로 구분하여 명시할 수 있다.
  • 타입 변수는 클래스에서뿐만 아니라 메서드의 매개변수나 반환값으로도 사용할 수 있다.

제네릭 타입의 이름 정하기

  • E: 요소(Element, 자바 컬렉션에서 주로 사용됨)
  • K: 키
  • N: 숫자
  • T: 타입
  • V: 값
  • S,U,V: 두번째 세번째 네번째에 선언된 타입

예제

package me.whiteship.livestudy.week14;

public class GenericSample<T> {
   T element;
   public void setElement(T element) {
       this.element = element;
   }

   public T getElement() {
       return element;
   }

   public static void main(String[] args) {
       GenericSample<Integer> integerGenericSample = new GenericSample<>();
       integerGenericSample.setElement(3);

       GenericSample<String> stringGenericSample = new GenericSample<>();
       stringGenericSample.setElement("thewing");

       System.out.println(integerGenericSample.getElement());
       System.out.println(stringGenericSample.getElement());
   }
}

output:

3
thewing

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

제네릭 타입에는 여러가지가 있다.

1. 바운디트 타입 매개변수(Bounded type parameter)

  • 바운드타입은 특정 타입의 서브 타입으로 제한한다. 클래스나 인터페이스를 설계할때 가장 흔하게 사용할 정도로 많이 볼 수 있는 개념이다.

예제

package me.whiteship.livestudy.week14;

public class BoundTypeSample <T extends Number>{
    public void set(T value){}

    public static void main(String[] args) {
        BoundTypeSample<Integer> boundTypeSample = new BoundTypeSample<>();
        boundTypeSample.set("Hi"); // 오류
    }
}
  • 이와 같이 컴파일 에러가 난다.
  • BoundTypeSample 클래스의 Type 파라미터를 T로 선언하고 <T extends Number> 로 선언한다. BoundTypeSample의 타입으로 Number의 서브 타입만 허용한다는 것이다.
  • Integer는 Number의 서브타입이기 때문에 BoundTypeSample과 같은 선언이 가능하지만 set함수의 인자로 문자열을 전달하려고 했기 때문에 컴파일 에러가 발생하게 된다.

2. Wild card

  • Unbounded Wildcard는 List<?> 와 같은형태로 물음표만 가지고 정의되어지게 된다. 내부적으로 Object로 정의되어서 사용되고 모든 타입의 인자를 받을 수 있다. 타입 파라미터에 의존하지 않는 메서드만을 사용하거나 Object 메서드에서 제공하는 기능으로 충분한 경우에 사용한다.

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

  • 타입 파리미터에 의존적이지 않은 일반 클래스의 메서드를 사용하는 경우 Wild Card를 사용한다.

EX) List.clear,List.size 등등

Upper Bounded WildCard

  • Upper Bounded WildCard는 List<? extends Foo> 와 같은 형태로 사용하고 특정 클래스의 자식 클래스만을 인자로 받는다는 것이다. 임의의 Foo 클래스를 상속받는 어느 클래스가 와도 되지만 사용할 수 있는 기능은 Foo 클래스에 정의된 기능만 사용이 가능하다.

Lower Bounded WildCard

  • Lower Bounded WildCard는 List<? super Foo> 와 같은 형태로 사용하고, Upper Bounded WildCard와 다르게 특정 클래스의 부모 클래스만을 인자로 받는다는 것이다.

기타

매개변수화 타입(parameterized type)

  • 하나 이상의 타입 매개변수(type parameter)를 선언하고 있는 클래스나 인터페이스를 제네릭 클래스, 또는 제네릭 인터페이스라고 하고 이를 제네릭 타입이라고 한다. 각 제네릭 타입에서는 매개변수화 타입들을 정의한다.
List<String> list = new ArrayList<>();
  • <>안에 있는 String은 실 타입 매개변수라고 하고 List 인터페이스에 선언되어있는 List의 E를 형식 타입 매개변수라고 한다. 제네릭은 타입 소거자(Type erasure)에 의해 자신의 타입 요소 정보를 삭제한다.
public interface List<E> extends Collection<E> {
}
  • 이것을 컴파일 해보면 아래와 같이 변경이 된다.
List<String> list = new ArrayList();

예제코드:

 public static void main(String[] args) {
        List<String> list = new ArrayList<>();
       
    }

컴파일 파일

public static void main(String[] args) {
        new ArrayList();
}
  • 신기하게도 new ArrayList만 있다.

바이트코드를 확인해보자

  • ArrayList()를 생성할때 어떠한 타입 정보도 들고있지않다. new ArrayList()로 생성한 것과 동일하게 바이트코드가 생성된다.

  • 컴파일러는 컴파일 단계에서 List 컬렉션에 String 인스턴스만 저장되어야 한다는 것을 알게되었고 또 그것을 보장해주기 때문에 ArrayList로 변경하여도 런타임에 동일한 동작을 보장한다. E,List와 같은 타입들을 비구체화 타입(타입 소거자에 의해 컴파일 타임에 타입 정보가 사라지는것(런타임에 구체화 하지 않는것))이라하며, 그 반대로 구체화 타입(자신의 타입 정보를 런타임 시에 알고 지키게하는것(런타임에 구체화 하는것))이 있으며 primitives,non-generic type, raw types 또는 List<?> Map과 같이 Unbounded wildcard Type이 있다.

제네릭 선언에 사용하는 타입의 범위도 지정할수있다.

  • <> 에 어떤 타입도 상관 없다고 했지만, wildcard로 사용하는 타입을 제한할 수는 있다
package me.whiteship.livestudy.week14;

public class WildcardGeneric <E>{
    E wildcard;

    public E getWildcard() {
        return wildcard;
    }

    public void setWildcard(E wildcard) {
        this.wildcard = wildcard;
    }
}
package me.whiteship.livestudy.week14;

public class Car {

    protected  String name;

    public Car(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String toString(){
        return "Car name = " + name;
    }
}
package me.whiteship.livestudy.week14;

public class Bus extends Car{
    public Bus(String name) {
        super(name);
    }

    @Override
    public String toString(){
        return "Bus name= " + name;
    }
}
package me.whiteship.livestudy.week14;

public class CarWildcardSample {
    public void callBoundedWildcardMethod(){
        WildcardGeneric<Car> wildcard = new WildcardGeneric<Car>();
        wildcard.setWildcard(new Car("Mustang"));
        boundedWildcardMethod(wildcard);
    }

    public void boundedWildcardMethod(WildcardGeneric<? extends Car> c){
        Car value = c.getWildcard();
        System.out.println(value);
    }


    public static void main(String[] args) {
        CarWildcardSample carWildcardSample = new CarWildcardSample();
        carWildcardSample.callBoundedWildcardMethod();
    }
}
  • 앞서 사용했던 "?"라는 wildcard는 어떤 타입이 오더라도 상관이 없었다. boundedWildcardMethod()에는 "?" 대신 <? extends Car> c이 들어있다. 이렇게 정의한 것은 제네릭 타입으로 Car를 상속받은 모든 클래스를 사용할 수 있다는 의미이다. 따라서 boundedWildcardMethod() 의 매개변수에는 다른 타입을 제네릭 타입으로 선언한 객체가 넘어올 수없다.

output:

Car name = Mustang

Process finished with exit code 0

메서드를 제네릭하게 선언

package me.whiteship.livestudy.week14;

public class GenericWildcardSample {
    public <T> void genericMethod(WildcardGeneric<T> c, T addValue){
        c.setWildcard(addValue);
        T value = c.getWildcard();
        System.out.println(value);
    }

    public void callGenericMethod(){
        WildcardGeneric<String> wildcard = new WildcardGeneric<String>();
        genericMethod(wildcard,"Data");
    }

    public static void main(String[] args) {
        GenericWildcardSample genericWildcardSample = new GenericWildcardSample();
        genericWildcardSample.callGenericMethod();
    }
}

output:

Data
  • ? 를 사용하는 CarWildcardSample처럼 타입을 두리뭉실하게 하는것보다 이철머 명시적으로 메서드 선언시 타입을 지정해주면 보다 견고한 코드를 작성할수있다.

3. 제네릭 메소드 만들기

  • 제네릭 메서드란 메서드의 선언부에 타입 변수를 사용한 메서드를 의미한다.
  • 이때 타입 변수의 선언은 메서드 선언부에서 반환 타입 바로 앞에 위치한다.

public static<T> void sort(,,,){
}

아래 예제의 제네릭 클래스에서 정의된 타입 변수 T와 제네릭 메서드에서 사용된 타입 변수 T는 별개이라는 것을 알아야한다.

public class Collections{
	public static<T> void sort(List<T> list, Comparator<? super T> c){
	list.sort(c);
}
}

4. Erasure

제네릭의 타입소거(Generics Type Erasure)

  • Erasure란 원소 타입을 컴파일 타임에서만 검사하고 런타임에는 해당 타입 정보를 알수가없다. 즉 컴파일 상태에만 제약 조건을 적용하고, 런타임에는 타입에 대한 정보를 소거하는 프로세스이다.
List<String> list = new ArrayList<Integer>(); // 컴파일 에러
list/add("thewing"); // typle이 일치하지 않아 addd가 안된다.
  • 이와 같은 상황에서 컴파일 오류를 확인할수있다. java 컴파일러는 타입소거를 아래와 같이 적용한다.

  • 제네릭 타입(Example) 에서는 해당 타입 파라미터(T) 나 Object로 변경해준다. Object로 변경하는 겨우 unbounded 된 경우를 뜻하며, 이는 <E extends Comparable> 와 같이 bound를 해주지 않는 경우를 의미한다. 이 소거 규칙에 대한 바이트 코드는 제네릭을 적용할 수 있는 일반 클래스, 인터페이스, 메서드에 적용이 가능하다.

  • 타입 안전성 보존을 위해 필요시 type casting을 넣어준다.

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

public static <E> boolean containsElement(E [] elements, E element){
	for (E e : elements){
    	if(e.equals(element)){
        return true;
        }
    }
    return false;
}

실제로 이렇게 선언되어 있는 제네릭 메서드의 경우 선언 방식에 따라 컴파일러가 타입 파라미터 E를 실제 유형의 Object로 변경한다.

public static  boolean containsElement(Object [] elements, Object element){
	for (Object e : elements){
    	if(e.equals(element)){
        return true;
        }
    }
    return false;
}

만든 이유는?

  • 하위 호환성을을 지키기 위해서이다.
  • 제네릭을 사용하더라도 하위 버전에서도 동일하게 동작해야하기 때문이다.
  • Primitive 타입을 사용하지 못하는 것도 바로 기본타입은 Object 클래스를 상속받고 있지 않기 때문이다.
  • 그래서 기본 타입 자료형을 사용하기 위해서는 Wrapper 클래스를 사용해야한다.
  • Wrapper 클래스를 사용할 경우 Boxing과 Unboxing을 명시적으로 사용할 수 도 있지만 암묵적으로도 사용할 수 있으니 구현자체에는 크게 신경쓸 부분이 없다.
package me.whiteship.livestudy.week14;

import java.util.Arrays;

public class Example1<T>{
    private T[] myArray;

    public Example1(int size) {
        //  myArray = new T[size]; X(컴파일 오류)
        //  Type parameter 'T' cannot be instantiated directly
        myArray = (T[]) new Object[size];
    }

    public void addElem(int index,T t){
        myArray[index] = t;
    }

    public void printElem(){
        System.out.println(Arrays.toString(myArray));
    }

    public static void main(String[] args) {
        Example1<String> e = new Example1<>(3);
        e.addElem(0,"java");
        e.addElem(1,"jeneric");

        e.printElem();
    }
}
9번라인처럼 제네릭타입을 사용해서 배열을 생성하면 편할텐데, 10라인처럼 생성해야하는 이유는?
  • new 연산자를 사용하기 때문이다.
  • new 연산자는 동적 메모리 할당 영역인 heap 영역에 생성한 객체를 할당한다.
  • 하지만 제네릭은 컴파일 타임에 동작하는 문법이다.
  • 컴파일 타임에는 T의 타입이 어떤타입인지 알 수 없기 때문에 Object타입으로 생성한 다음 타입 캐스팅을 해주어야 사용할수있다.
**static 변수에도 제네릭 타입을 사용할 수 없다.**
 private T[] myArray;
 private static T[] arr;
  • static 키워드를 사용해서 멤버 필드를 선언하게 되면, 특정 객체에 종속되지 않고 클래스 이름으로 접근해서 사용할수있다.

  • 제네릭 타입을 사용하면, 위의 예제의 경우 Example<String>Example<Integer> 등으로 객체를 생성해서 인스턴스마다 사용하는 타입을 다르게 사용할수있어야하는데 static 으로 선언한 변수는 사용할수 없다.

하지만 재밌게도 **static 메서드**에는 제네릭을 사용할 수 있다.
  • Static 키워드를 사용하면 클래스 이름으로 접근하여 객체를 생성하지 않고 여러 인스턴스에서 공유해서 사용할 수 있다.

  • 변수같은 경우 해당 값을 사용하려면 값의 타입을 알아야하지만,

  • 메서드의 경우 해당 기능을 공유해서 사용하는 것이기 때문에 제네릭 타입 변수 T를 매개변수로 사용한다고 하면 해당 값은 메서드안에서 지역 변수로 사용되기 때문에 변수와 달리 메서드는 static 으로 선언 되어 있어도 제네릭을 사용할수있다.

따라서 컴파일러는 코드의 형식 안정성을 보장하고 런타임 오류를 방지한다.

Type Erasure의 유형

1. 클래스 Erasure

클래스 수준에서 컴파일러는 클래스의 Type Parameter를 버리고 첫번째 바인딩으로 대체하거나 Type Parameter가 바인딩 되지 않은 경우 Object로 변환한다.

배열을 사용하여 Stack 구현의 예시를 보자.

package me.whiteship.livestudy.week14;

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(){
        
    }
}
  • 컴파일시 컴파일러는 바인딩되지 않은 형식 매개변수 E를 Object로 바꾸게 된다.
package me.whiteship.livestudy.week14;

public class Stack< {
    private Object[] stackContent;

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

2. type Parameter E가 바인딩 된 경우

package me.whiteship.livestudy.week14;

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(){

    }
}

컴파일러는 바인됭 된 형식 매개변수 E를 첫번쨰 바인딩 된 클래스인 Comparable로 대체한다.

package me.whiteship.livestudy.week14;

public class BoundStack{
    private Comparable[] stackContent;

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

    public void push(Comparable로 data){

    }

    public Comparablepop(){

    }
}

3. Method Type Erasure

  • Method Type Erasure의 경우 method-level type erasure가 저장되지 않고바인딩되지 않은 경우 부모 형식 Object로 변환되거나 바인딩 될때 첫번째 바인딩 될 클래스로 변환한다.

[주어진 배열의 내용을 표시하는 예제]

public static <E> void printArray(E[] array){
	for (E element: array){
    	System.out.printf("%s " , element);
    }
}
  • 컴파일시 컴파일러는 Type parameter E를 Object로 바꾼다.
public static void printArray(Object[] array){
	for (Object element: array){
    	System.out.printf("%s " , element);
    }
}

[바인딩된 method type parameter의 경우]

public static <E extends Comparable<e>> void printArray(E[] array){
	for (E element: array){
    	System.out.printf("%s " , element);
    }
}
  • Type Parameter E를 지우고 Comparable로 대체한다.
public static void printArray(Comparable[] array){
	for (Comparable로 element: array){
    	System.out.printf("%s " , element);
    }
}

제네릭 사용코드:

package me.whiteship.livestudy.week14.ex;

public class Entity<K> {
    protected K id;

    public K getId() {
        return id;
    }
}
package me.whiteship.livestudy.week14.ex;

public class Apple extends Entity<Integer> {

   public static Apple of(Integer id){
       Apple apple = new Apple();
       apple.id = id;
       return apple;
   }
}
package me.whiteship.livestudy.week14.ex;

public class Banana extends Entity<Integer> {

    public static Banana of(Integer id){
      Banana banana = new Banana();
      banana.id = id;
      return banana;
    }
}
package me.whiteship.livestudy.week14.ex;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class GenericDao<E extends Entity<K>,K> {

    private Map<K,E> dataSource =new HashMap<>();

    public void save(E e){
        dataSource.put(e.getId(),e);
    }

    public void delete(E e){
        dataSource.remove(e.getId());
    }

    public void delete(K k){
        dataSource.remove((k));
    }

    public List<E> findAll(){
        return new ArrayList<>(dataSource.values());
    }

    public E findById(K k){
        return dataSource.get(k);
    }

}
package me.whiteship.livestudy.week14.ex;

import java.util.ArrayList;
import java.util.List;

public class App {
    public static void main(String[] args) {
       GenericDao<Apple,Integer> genericDao = new GenericDao<>();

        genericDao.save(Apple.of(5));
        genericDao.save(Apple.of(7));

        List<Apple> appleList = genericDao.findAll();

        for (Apple apple : appleList) {
            System.out.println(apple.getId());
        }
        
    }
}

참고

참고링크
참고링크
제네릭주의사항참고

0개의 댓글