영권's

14주차 과제: 제네릭 본문

스터디/백기선 라이브 스터디(자바)

14주차 과제: 제네릭

ykkkk 2021. 6. 1. 16:41

목표

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

학습할 것 (필수)

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

제네릭이란?

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

왜 제네릭을 사용해야 하는가?

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

 

제네릭 사용법

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

  • 제네릭 타입은 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다.
  • 제네릭 타입은 클래스 또는 인터페이스 뒤에 "<>"가 붙고, 사이에 타입 파라미터가 위치한다.
public class 클래스명<T>{...}
public interface 인터페이스명<T>{...}
// 타입 파라미터 T

 

타입 파라미터

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

 

타입 파라미터 네이밍 컨벤션

  • E - 요소(Element) (자바 컬렉션에서 광범위하게 사용됨)
  • K - 키(Key)
  • N - 숫자(Number)
  • T - 타입(Type)
  • V - 값(Value)
  • S,U,V etc - 2번째, 3번째, 4번째 타입들...

 

제네릭 클래스 선언 예

/**
 * Generic version of the Box class.
 * @param <T> the type of the value being boxed
 */
public class Box<T> {
    // T stands for "Type"
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box();
        integerBox.set(1);
        System.out.println("integerBox.get() : " + integerBox.get());

        Box<String> Stringbox = new Box();
        Stringbox.set("test");
        System.out.println("Stringbox.get() : " + Stringbox.get());
    }
}

결과

제네릭 타입 파라미터에 어떤 것을 사용하느냐에 따라 결과가 달라진다.

 

바이트 코드

바이트코드를 보면 타입 파라미터 T가 Object로 변환 되어있는 것을 알 수 있다.

 

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

  • 바운드 타입
    • 특정 타입의 서브 타입으로 제한한다.

타입 파라미터에 지정되는 구체적인 타입을 제한할 필요가 종종 있다.

예를 들어 숫자를 연산하는 제네릭 메서드는 매개값으로 Number 타입 또는 하위 클래스 타입의 인스턴스만 가져야 한다.

이것이 제한된 타입 파라미터가 필요한 이유이다.

제한된 타입 파라미터를 선언하려면 타입 파라미터 뒤에 extends 키워드를 붙이고 상위 타입을 명시하면 된다.

상위 타입은 클래스 뿐만아니라 인터페이스도 가능하며 인터페이스라고 해서 implements를 사용하지는 않는다.

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

public static void main(String... args) {
    Box<Integer> box = new Box<>();
    box.set("Hi"); // compile error
}
  • Box 클래스의 타입 매개변수 T를 선언하면서 <T extends Number>로 선언하였다.
  • 이는 Box의 타입으로 Number의 서브타입만 허용한다는 의미이다.
  • 위의 코드에서 Integer는 Number의 서브타입이기 때문에 Box<Integer>와 같은 선언이 가능하지만 set 함수의 인자로 문자열을 전달하려고 했기 때문에 컴파일 에러가 발생한다.

참고로 다중 바운드 타입이라는 것도 존재하는데 Java가 클래스 다중 상속이 안된다는 것을 기억하면 규칙을 이해하는 데 도움이 될 것이다. 다중 바운드 타입은 클래스 하나와 인터페이스 여러 개를 선언할 수 있다.

class A {}
interface B {}
interface C {}

class D<T extends A & B & C> { // 클래스인 A가 가장 먼저 와야한다.
  
}
// 다중 바운드 타입

class D를 선언하면서 타입 T class A, interface B, interface C의 서브타입으로 선언하였다. 클래스는 한 개만 허용되고 인터페이스는 여러 개를 선언할 수 있다.

 

 

  • 언바운드 와일드카드 타입(Unbounded wildcard type)
    • List<?>와 같은 타입을 언바운드 와일드카드 타입이라고 한다. 
    • 타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있다.
  • Unbounded : 한이 없는, 무한한
    • 어떤 타입이 오든 관계가 없다는 것이다. 언바운드 와일드카드 타입이 사용될 수 있는 시나리오는 다음과 같다.
  1. Object 클래스에서 제공되는 기능을 사용하여 구현할 수 있는 메서드를 작성하는 경우
  2. 타입 파라미터에 의존적이지 않은 일반 클래스의 메소드를 사용하는 경우, 예를 들면 List.clear, List.size, Class

Upper Bounded Wildcard(상위 클래스 제한) ex) 제네릭타입<? extends 상위타입>

  • 타입 파라미터를 대치하는 구체적인 타입으로 상위타입이나 그 하위타입만 올 수 있다.
  • Upper Bounded Wildcard는 List<? extends Foo>와 같은 형태로 사용하고 특정 클래스의 자식 클래스만을 인자로 받는다는 것이다. 임의의 Foo클래스를 상속받는 어느 클래스가 와도 되지만 사용할 수 있는 기능은 Foo클래스에 정의된 기능만 사용이 가능하다.

Lower Bounded Wildcard(하위 클래스 제한) ex) 제네릭타입<? super 하위타입>

  • 타입 파라미터를 대치하는 구체적인 타입으로 하위 타입이나 그 상위타입이 올 수 있다.
  • Lower Bounded Wildcard는 List<? super Foo>와 같은 형태로 사용하고, Upper Bounded Wildcard와 다르게 특정 클래스의 부모 클래스만을 인자로 받는것이다.

예)

package generics;

public class Course<T> {
    private String name;
    private T[] students;

    public Course(String name, int capacity) {
        this.name = name;

        // 타입 파라미터로 배열을 생성하려면 new T[n] 형태로 배열을 생성할 수 없고
        // (T[])new Object[n]으로 생성해야 한다.
        students = (T[]) new Object[capacity];
    }

    public String getName() {
        return name;
    }

    public T[] getStudents() {
        return students;
    }

    // 배열에 비어있는 부분을 찾아서 수강생을 추가하는 메서드
    public void add(T t) {
        for (int i = 0; i < students.length; i++) {
            if (students[i] == null) {
                students[i] = t;
                break;
            }
        }
    }
}

수강생이 될 수 있는 타입은 4가지 클래스라고 하자.

  • Coursc<?>
    • 수강생은 모든 타입이 될 수 있다.
  • Course<? extends Student>
    • 수강생은 Student와 HighStudent만 될 수 있다.
  • Course<? super Worker>
    • 수강생을 Worker와 Person만 될 수 있다.

 

제네릭의 서브타이핑(Subtyping in generics)

객체지향 관점에서 아래의 코드는 is-a 관계이므로 컴파일 에러 없이 정상 동작할 것이다.

public void someMethod(Number n) { /* ... */ }

someMethod(new Integer(10));   // OK
someMethod(new Double(10.1));   // OK

 

마찬가지로 다음과 같은 제네릭 코드도 정상 동작할 것이다.

Box<Number> box = new Box<Number>();
box.add(new Integer(10));   // OK
box.add(new Double(10.1));  // OK

 

Box 클래스의 매개변수화 타입으로 Number를 선언하였고 Integer Double Number의 서브타입이기 때문에 문제가 없다. 하지만 아래와 같은 코드는 어떨까?

public void boxTest(Box<Number> n) { /* ... */ }

boxTest(new Box<Double>());
boxTest(new Box<Integer>());

얼핏 봐서는 문제가 없는 것처럼 보이지만 컴파일 에러가 발생한다. 

Box<Double>이나 Box<Integer> Box<Number>의 서브타입이 아니기 때문이다. 이걸 한눈에 이해하기 쉽게 표현한 다이어그램이 있다.

출처 : https://medium.com/@joongwon/java-java%EC%9D%98-generics-604b562530b3

 

  • 매개변수화 타입은 불변(invariant)이기 때문에 Box<Number>는 Box<Integer>의 서브타입도, 슈퍼 타입도 아니다. 오로지 Box<Number>에서는 Number 타입만 허용하고 Box<Integer>는 Integer 타입만 허용하기 때문에 둘은 다른 존재다.
  • 제네릭 클래스나 인터페이스를 상속관계로 정의하고 싶다면 다음과 같이 클래스 or 인터페이스의 상속관계를 정의해야 한다.

출처: https://medium.com/@joongwon/java-java%EC%9D%98-generics-604b562530b3

우리만의 List 인터페이스를 정의한다고 상상했을 때 다음과 같이 구성할 수 있다.

interface PayloadList<E,P> extends List<E> {
  void setPayload(int index, P val);
}

PayloadList List<E>를 상속받으면서 추가적인 타입 P를 선언하였다. 

PayloadList는 다음과 같은 형태를 가질 수 있는데 아래의 형태 모두 List<String>의 서브타입이다.

출처 : https://medium.com/@joongwon/java-java%EC%9D%98-generics-604b562530b3

 

매개변수화 타입(Parameterized type)

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

 

예제

package generics;

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

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

컴파일된 파일 new ArrayList() 만 있다.

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

비 구체화 타입(non-reifiable type)

  • 타입 소거자에 의해 컴파일 타임에 타입 정보가 사라지는 것(런타임에 구체화 하지 않는 것)

구체화 타입(reifiable type)

  • 자신의 타입 정보를 런타임 시에 알고 지키게 하는 것(런타임에 구체화 하는 것)

 

제네릭 메소드 만들기

  • 제네릭 메서드는 매개 타입과 리턴 타입으로 타입 파라미터를 갖는 메서드를 말한다.
  • 제네릴 메서드를 선언하는 방법은 리턴 타입 앞에 <> 기호를 추가하고 타입 파라미터를 기술한 다음, 리턴 타입과 매개 타입으로 타입 파라미터를 사용하면 된다.
public <타입파라미터...> 리턴타입 메서드명(매개변수 ...){ ... }

다음 boxing() 제네릭 메서드는 <> 기호안에 타입 파라미터 T를 기술한 뒤, 매개 변수 타입으로 T를 사용했고, 리턴 타입으로 제네릭 타입 Box<T>를 사용했다.

public <T> Box<T> boxing(T t) { ... }

 

제네릭 메서드는 두 가지 방식으로 호출 할 수 있다.

코드에서 타입 파라미터의 구체적인 타입을 명시적으로 지정해도 되고, 컴파일러가 매개값의 타입을 보고 구체적인 타입을 추정하도록 할 수도 있다.

리턴타입 변수 = <구체적인 타입> 메서드명(매개값); // 명시적으로 구체적 타입을 지정
리턴타입 변수 = 매서드명(매개값); // 매개값을 보고 구체적 타입을 추정

// ex)
Box<Integer> box = <Integer>boxing(100); // 타입 파라미터를 명시적으로 Integer로 지정
Box<Integer> box = boxing(100);	// 타입 파라미터를 Integer로 추정

 

다음 예제는 Util 클래스에 정적 제네릭 메서드로 compare()를 정의하고 CompareMethodExample 클래스에서 호출했다.

타입 파라미터는 K와V로 선언 되었는데, 제네릭 타입Pair가 K와 V를 가지고 있기 때문이다.

compare() 메서드는 두 개의 Pair를 매개값으로 받아 K와 V값이 동일한지 검사하고 boolean 값을 리턴한다.

package generics;

public class Pair<K,V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;}

    public K getKey() {return key;}

    public void setKey(K key) { this.key = key; }

    public V getValue() {return value;}

    public void setValue(V value) {this.value = value;}
}
package generics;

public class Util {
    public static <K,V> boolean compare(Pair<K,V> p1, Pair<K,V> p2){
        boolean keyCompare = p1.getKey().equals(p2.getKey());
        boolean valueCompare = p1.getValue().equals(p2.getValue());
        return  keyCompare && valueCompare;
    }
}
package generics;

public class CompareMethodExample {
    public static void main(String[] args) {
        Pair<Integer,String> p1 = new Pair<Integer,String>(1,"사과");
        Pair<Integer,String> p2 = new Pair<Integer,String>(1,"사과");

        // 메서드의 제네릭 타입의 구체적인 타입을 명시적으로 지정
        boolean result1 = Util.<Integer,String>compare(p1,p2);
        if (result1){
            System.out.println("논리적으로 동등한 객체입니다.");
        }else{
            System.out.println("논리적으로 동등하지 않은 객체입니다.");
        }

        Pair<String,String> p3 = new Pair<String,String>("user1","사과");
        Pair<String,String> p4 = new Pair<String,String>("user2","사과");

        // 메서드의 제네릭 타입의 구체적인 타입을 추정
        boolean result2 = Util.compare(p3,p4);
        if (result2){
            System.out.println("논리적으로 동등한 객체입니다.");
        }else{
            System.out.println("논리적으로 동등하지 않은 객체입니다.");
        }

    }
}

실행 결과

첫번째는 메서드의 제네릭 타입을 명시적으로 Integer와 String으로 주었고 두번째는 컴파일러가 구체적인 타입을 추정하였다.

 

클래스 타입 파라미터와 메서드 타입 파라미터가 같으면 어떤것을 따라갈지 궁금한 점이 생겨 실험해봤다.

public class Box<T> {
    // T stands for "Type"
    private T t;

    public void set(T t) { this.t = t; }
    public <T> T get(T t) { return t; }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box();
        integerBox.set(1);
        System.out.println("integerBox.get() : " + integerBox.<String>get("test"));

    }
}

결과

get 메서드의 내용을 return (T) this.t 로 변경하더라도 에러가 나는 것을 알 수 있었고

이런 코드가 있을 때 메서드 get의 리턴 타입 T는 클래스 타입 파라미터가 아닌 메서드 타입 파라미터를 따라갔다.

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

같은 타입 파라미터를 사용한다면 메서드안에서는 메서드에서 사용된 타입 파라미터로 인식하는것 같다.

Erasure

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

  • 컴파일러는 지네릭 타입을 이용해 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다.
  • 그리고 지네릭 타입을 제거한다.
  • 즉, 컴파일된 파일(*.class)에는 지네릭 타입에 대한 정보가 없는 것이다.
  • 이렇게 하는 주된 이유는 제네릭이 도입되기 이전의 소스 코드와의 호환성을 유지하기 위해서이다.
  • JDK1.5부터 제네릭이 도입되었지만, 아직도 원시 타입을 사용해서 코드를 작성하는 것을 허용한다.
  • 그러나 앞으로 가능하면 원시 타입을 사용하지 않는것이 좋다 언젠가는 새로운 기능을 위해 하위 호환성르 포기해야 할 때가 올 것이다.

 

제네릭 타입의 기본적인 제거 과정

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

제네릭 타입이 <T extends Fruit>라면 T는 Fruit로 치환된다.

<T>인 경우는 T는 Object로 치환된다. 그리고 클래스 옆의 선언은 제거된다.

//변경 전
class Box<T extends Fruit>{
	void add(T t){
    	...
    }
}
// 변경 후
class Box{
	void add(Fruit t){ ... }
}

 

 

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

  • 지네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가한다.
T get(int i){
	return list.get(i);
}
Fruit get(int i){
	return (Fruit)list.get(i);
}

 

와일드 카드가 포함되어 있는 경우 다음과 같이 적절한 타입으로의 형변환이 추가된다.

static Juice makeJuice(FruitBox<? extends Fruit> box){
	String tmp = "";
	for(Fruit f : box.getList()) temp += f + " ";
	return new Juice(temp);
}

 

static Juice makeJuice(FruitBox box){
	String tmp = "";
	Iterator it = box.getList().iterator();
	while(it.hasNext()){
		tmp += (Fruit)it.next() + " " ;
	}
	return new Juice(temp);
}

 

 

 


출처 : 

이것이 자바다

 

자바의 정석3판

 

https://sujl95.tistory.com/73

 

14주차 과제: 제네릭

14주차 과제: 제네릭 시작하기 전에 제네릭이 무엇이고 제네릭을 왜 사용해야하는지 알아보자 제네릭이란? 데이터 타입(data type)을 일반화(generalize)하는것을 의미한다 제네릭은 클래스나 메소드

sujl95.tistory.com

https://docs.oracle.com/javase/tutorial/java/generics/types.html

 

Generic Types (The Java™ Tutorials > Learning the Java Language > Generics (Updated))

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

https://medium.com/@joongwon/java-java%EC%9D%98-generics-604b562530b3

'스터디 > 백기선 라이브 스터디(자바)' 카테고리의 다른 글

15주차 과제: 람다식  (0) 2021.06.05
13주차 과제: I/O  (0) 2021.05.31
12주차 과제: 애노테이션  (0) 2021.03.05
11주차 과제: Enum  (0) 2021.03.05
10주차 과제: 멀티쓰레드 프로그래밍  (0) 2021.02.25
Comments