슬기로운 개발생활

온라인 자바 스터디 #14 - 제네릭(Generic)

by coco3o
반응형

목표

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

학습할 것 (필수)

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

 


제네릭 사용법

Java 제네릭(Generic)이란 ?

Java 5부터 제네릭타입이 추가되었다.

 

Java에서 제네릭(Generic)은

Data type을 특정한 type 하나로 정하지 않고 사용할 때마다 바뀔 수 있게 범용적이고 포괄적으로 지정한다 라는 의미이다. 

 

제네릭 타입은 < >을 가지는 클래스와 인터페이스를 말한다.

 

 

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

제네릭 타입을 사용함으로써 잘못된 타입이 사용될 수 있는 문제를 컴파일 과정에서 제거할 수 있다.

 

실행 시 타입 에러가 나는것보다는 컴파일 시에 에러를 사전에 방지하는 것이 좋다. 

 

또 제네릭 코드를 사용하면 타입을 국한하기 때문에 요소를 찾아올 때 타입변환을 할 필요가 없어 프로그램 성능이 향상된다.

 

ArrayList list = new ArrayList(); //제네릭을 사용하지 않을경우
list.add("test");
String temp = (String) list.get(0); //타입변환이 필요함
        
ArrayList<String> list2 = new ArrayList(); //제네릭을 사용할 경우
list2.add("test");
temp = list2.get(0); //타입변환이 필요없음

 

제네릭 장점

1. 타입 안정성 제공

-  컴파일 타임에 타입 체크를 하기 때문에 런타임에서 ClassCastException과 같은 UncheckedException을 보장받음

 

2. 타입체크와 형변환 생략 가능

-  코드가 간결해짐

 

제네릭 특징

모든 객체에 대해 동일하게 동작해야하는 static멤버에 타입 변수 T를 사용할 수 없다.

-  T는 인스턴스변수로 간주되기 때문이다.

-  static 멤버는 인스턴스변수를 참조할 수 없다.

 

제네릭 타입의 배열을 생성하는 것도 허용되지 않는다.

-  제네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, new T[10] 과 같이 배열을 생성하는 것은 안된다.

이유는 new 연산자 때문이다.  이 연산자는 컴파일 시점에 타입 T가 무엇인지 명확히 알아야 하기 때문이다.

* 꼭 제네릭 배열을 생성해야 할 필요가 있을 땐 new 연산자 대신  'Reflection API'의 new Instance()와 같이 동적으로 객체를 생성하는 메소드로 생성하거나, Object 배열을 생성해서 형변환 하는 방법 등 사용 * 

 

제네릭 사용법

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

제네릭 타입은 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다. 

 

제네릭 타입은 클래스 또는 인터페이스 이름 뒤에 < > 부호가 붙고 사이에 타입 파라미터가 위치한다.

 

타입 파라미터는 정해진 규칙은 없지만 일반적으로 대문자 알파벳 한글자로 표현한다.

 

자주 사용하는 타입인자

타입인자 설명
<T> Type
<E> Element
<K> Key
<N> Number
<V> Value
<R> Result

 

제네릭 클래스

class ExGeneric<T> {
	private T t;
    
    public void setT(T t) { 
    	this.t = t;
    }
    
    public T getT() {
    	return t;
    }
}

위와 같이 클래스를 설계할 때 구체적인 타입을 명시하지 않고 타입 파라미터로 넣어두었다가

 

실제 설계한 클래스가 사용될 때  ExGeneric<String> exGeneric = new ExGeneric< >( );   이런식으로 

 

구체적인 타입을 지정하면서 사용하면 타입 변환을 최소화 시킬 수 있다.

 

 

제네릭 인터페이스

interface ExInterfaceGeneric<T> { 
	T example();
}

class ExGeneric implements ExInterfaceGeneric<String> {
	
    @Override
    public String example() { 
    	return null;
    }
}

인터페이스도 위와 같이 클래스처럼 제네릭으로 설정해두고 활용할 수 있다.

 

 

멀티 타입 파라미터 사용

public class GenericTest<A, B, C> {
    private A first;
    private B second;
    private C third;

    public void setFirst(A first) {
        this.first = first;
    }

    public void setSecond(B second) {
        this.second = second;
    }

    public void setThird(C third) {
        this.third = third;
    }

    public void show() {
        System.out.println("A의 타입은 : " + first.getClass().getTypeName());
        System.out.println("B의 타입은 : " + second.getClass().getTypeName());
        System.out.println("C의 타입은 : " + third.getClass().getTypeName());
    }

    public static void main(String[] args) {
        GenericTest<String, Integer, Double> genericTest = new GenericTest<>();
        genericTest.setFirst("안녕하세요.");
        genericTest.setSecond(10);
        genericTest.setThird(10.0);
        
        genericTest.show();
    }
}

//	결과
A의 타입은 : java.lang.String
B의 타입은 : java.lang.Integer
C의 타입은 : java.lang.Double

Process finished with exit code 0

타입은 두개 이상의 멀티 타입 파라미터를 사용할 수 있고, 이 경우 각 타입 파라미터를 콤마로 구분한다.

 


제네릭 주요 개념 

한정적 타입 매개변수(Bounded Type)

 

타입 파라미터들은 바운드(bound) 될 수 있다.

바운드 된다는 의미는 제한된다는 의미인데 메소드가 받을 수 있는 타입을 제한할 수 있다는 것이다.

예를들면, 어떤 타입과 그 타입의 모든 서브 클래스들을 허용하거나,

어떤 타입과 그 타입의 모든 부모클래스들을 허용하도록 메소드를 작성할 수 있다.

public class GenericTest<T extends Number> {
    public void set(T t) {}

    public static void main(String[] args) {
        GenericTest<Integer> genericTest = new GenericTest<>();

        genericTest.set("Hi!");
    }
}

 

 

위와 같이 컴파일 에러가 난다.

GenericTest 클래스의 Type 파라미터를 T로 선언하고 <T extends Number >로 선언한다.

GenericTest의 타입으로 Number의 서브 타입만 허용한다는 것이다.

 

Integer는 Number의 서브타입인데  set메소드의 인자로 문자열(String)을 전달하려고 했기 때문에 컴파일 에러가 발생하게된다.


제네릭 와일드 카드

 

코드에서 ?를 일반적으로 와일드카드라고 부른다. 사용하는 경우는 아래와 같다.

 

출처 :&nbsp;https://honbabzone.com/java/java-generic/

 

와일드카드 타입에는 총 세가지의 형태가 있으며 물음표(?) 키워드로 표현된다.

 

<?> :  타입 파라미터를 대치하는 것으로 모든 클래스나 인터페이스타입이 올 수 있다.

-  A  ~  E 모두 가능

 

<? extends 상위타입> :  객체의 하위 클래스만 올 수 있다.

- <? extends D>  :  D , E 가능

 

<? extends 하위타입> :  객체의 상위 클래스만 올 수 있다.

- <? super D>  :  D,  A 가능


제네릭 메소드 

제네릭 메소드는 매개 타입과 리턴 타입으로 타입 파라미터를 갖는 메소드를 말한다.구현을 하기 위해선 리턴 타입 앞에 < >다이아몬드 기호를 추가하고, 타입 파라미터를 기술한 다음리턴 타입과 매개 타입으로 타입 파라미터를 사용하면 된다.

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

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

 

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

1. 리턴타입 변수 = <구체적 타입> 메소드명(매개값);     //명시적으로 구체적 타입 지정
2.리턴타입 변수 = 메소드명(매개값);     //매개값을 보고 구체적 타입을 추정
1. Box<Integer> box = <Integer>boxing(100);     //타입 파라미터를 명시적으로 Integer로 지정
2. Box<Integer> box = boxing(100);     //타입 파라미터를 Integer로 추정

일반적으로 매개값을 넣어줌으로 컴파일러가 유추하게 만들어주는 두번째 방법을 사용한다.

 

실습예제

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;
    }
}
public class Util {
	//Generic Method
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        boolean keyCompare, valueCompare;

        keyCompare = p1.getKey().equals(p2.getKey());
        valueCompare = p1.getValue().equals(p2.getValue());

        return keyCompare && valueCompare;
    }
}
public class CompareMethodEx {
    public static void main(String[] args) {
    	//타입 파라미터 지정
        Pair<String, Integer> p1 = new Pair<String, Integer>("coco", 26);
        //타입 파라미터 추정
        Pair<String, Integer> p2 = new Pair<>("coco", 26);
		//GenericMethod 호출
        boolean result = Util.compare(p1, p2);
        System.out.println(result);


        Pair<String, Integer> p3 = new Pair<>("coco", 26);
        Pair<String, Integer> p4 = new Pair<>("Jack", 30);

        result = Util.compare(p3, p4);
        System.out.println(result);
    }
}

//실행 결과
true
false

Process finished with exit code 0

 

Util 안에 있는 제네릭 메소드는 정적메소드이기 때문에 인스턴스화 없이 바로 접근 가능하다.


Erasure

제네릭은 타입의 안정성을 보장하며 실행시간에 오버헤드가 발생하지 않도록 하기 위해 추가되었다.

 

컴파일러는 제네릭 타입을 이용하여 소스파일을 체크하고 필요한 곳에 형변환을 넣어준다.

그리고 제네릭 타입을 제거한다.

 

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();
}

// 변경 후
Fruit get(int i) {
    return (Fruit)list.get(i);
}

List의 get( )은 Object 타입을 반환하므로 형변환이 필요하다.


 

 

 

 

references : coding-factory.tistory.com/573 honbabzone.com/java/java-generic/ ict-nroo.tistory.com/42 watrv41.gitbook.io/devbook/java/java-live-study/14_week sujl95.tistory.com/73?category=941455

 

 

 

 

 

반응형

블로그의 정보

슬기로운 개발생활

coco3o

활동하기