자바

java generic class

알쓸개잡 2023. 9. 24.

java generic은 java 프로그래밍에서 매우 중요한 개념 중 하나로 generic을 사용하여 타입 안정성을 높이고 재사용성을 개선할 수 있다. 이번 포스팅에서는 java generic의 개념과 활용 방법에 대해서 기록한다.

 

Generic 코드 사용의 이점

  • java 컴파일시 더 강력한 유형 검사를 수행한다. java 컴파일러는 일반 코드에 엄격한 타입 검사를 적용하고 코드가 타입 안전성을 위반하는 경우 컴파일 오류를 발생시켜 런타임 이전에 문제를 찾을 수 있다.
  • 타입을 명확히 지정하여 형변환을 제거할 수 있다.
//uses unchecked or unsafe operations. 경고가 발생한다.
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

위 코드는 List generic 컬렉션에 타입을 지정하지 않아 값을 가져올 때 형변환을 해야 하는 코드다.

generic을 사용하면 불필요한 형변환을 피할 수 있다.

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

 

Generic class 형식

class name<T1, T2, ..., Tn> {
...
}

타입 파라미터 섹션은 '<>' 로 구분되며 클래스 이름 뒤에 온다. 타입 매개변수 T1, T2, ..., Tn을 지정한다.

/**
 * 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; }
}

Box 클래스에 타입 매개변수 <T>를 정의 하여 generic 클래스임을 선언하였다. 타입 파라미터 T는 인스턴스 생성시 지정된 타입으로 치환된다. 타입 파라미터 T는 클래스 타입, 인터페이스 타입, 배열 타입등과 같이 primitive이 아닌 모든 타입이 될 수 있다.

 

타입 파라미터 이름 규칙

일반적으로 타입 파라미터 이름은 단일 대문자로 사용한다.

가장 일반적으로 사용되는 타입 파라미터 이름은 다음과 같다.

  • E - Element (자바 컬렉션 프레임워크에서 광범위하게 사용한다.)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S, U, R 등 - 2nd, 3rd, 4th 타입

 

인스턴스화

Box<Integer> intergerBox = new Box<Integer>();
== Box<Integer> integerBox = new Box<>();        //타입 추론
List<String> stringList = new ArrayList<String>();
== List<String> stringList = new ArrayList<>();  //타입 추론

generic 타입 클래스의 <T> 부분에 구체적인 타입을 명시하여 인스턴스를 생성할 수 있다. List<String> 은 String 타입 인스턴스를 element로 갖는 List를 의미한다.

타입 파라미터(type parameter)와 타입 인자(type argument)
Box<T> 에서 T는 타입 파라미터이고 Box<Integer> integerBox 에서 Integer는 타입 인자라고 한다.

 

Raw Types

raw type은 타입 인자가 지정되지 않은 generic 클래스 혹은 generic 인터페이스이다.

Box rawBox = new Box();

Box 타입은 Box<T> generic 타입의 raw 타입 이라고 한다. raw 타입이 표시되는 이유는 JDK 5.0 이전에는 많은 API 클래스(Collections 클래스)가 generic 타입이 아니었기 때문이다. 이전 java 버전과의 호환성을 위해 매개변수화된 타입을 raw 타입에 할당 할 수 있다.

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;

반대로 raw 타입을 매개변수화된 타입에 할당하면 경고가 발생한다.

Box rawBox = new Box();
Box<Integer> integerBox = rawBox; //warning!! (unchecked conversion)

경고가 발생하는 이유는 rawBox raw 타입 인스턴스의 정확한 타입을 알 수 없기 때문이다.

매개변수화된 타입을 raw 타입에 할당 후 raw 타입을 사용하는 경우에도 경고가 발생한다.

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8);  // warning: unchecked invocation to set(T)

raw 타입이 generic의 타입 검사를 우회하여 안전하지 않은 코드의 감지를 런타임으로 지연시키게 되면서 경고가 발생하는 것이다. 이러한 상황으로 인해서 raw 타입을 사용하지 않아야 한다.

 

Generic Method

Generic 메서드는 자체 타입 파라미터를 사용하는 메소드다. 타입 파라미터의 범위는 메서드 내에서만 허용된다.

public class GenericMethod {
	public static <T, R> void printStatic(T t, R r) {
		System.out.println("T " + t + ", R " + r);
	}

	public <T, R> void printNonStatic(T t, R r) {
		System.out.println("T " + t + ", R " + r);
	}
}

=============================================================

GenericMethod.<Integer, String>printStatic(1, "static");
//전달되는 argument 타입에 따라서 타입 추론이 이루어져 <Integer, String>은 생략 가능하다.
GenericMethod.printStatic(1, "static");
GenericMethod genericMethod = new GenericMethod();
//전달되는 argument 타입에 따라서 타입 추론이 이루어져 <Integer, String>은 생략 가능하다.
genericMethod.<Integer, String>printNonStatic(1, "static");
genericMethod.printNonStatic(1, "static");

-------------------------------------------------------------
결과
T 1, R static
T 1, R static
T 1, R static
T 1, R static

 

Bounded 타입 파라미터

Generic 클래스에 지정할 타입을 제한하고자 할 때 사용한다.

타입 파라미터에 extends (클래스) 혹은 implements (인터페이스) 키워드를 지정하여 정의할 수 있다.

public class NumberBoundedGeneric<T extends Number> {
	private final T t;
	public NumberBoundedGeneric(T t) {
		this.t = t;
	}

	public T getNumber() {
		return t;
	}
}
==========================================================
NumberBoundedGeneric<Long> numberBoundedGeneric = new NumberBoundedGeneric<>(100L);
System.out.println(numberBoundedGeneric.getNumber());

NumberBoundedGeneric<Integer> numberBoundedGeneric2 = new NumberBoundedGeneric<>(100);
System.out.println(numberBoundedGeneric2.getNumber());

NumberBoundedGeneric Generic 클래스는 타입 인자로 Number 타입을 상속하는 타입으로만 지정할 수 있다.

Bounded 타입 파라미터는 Generic 메소드에도 적용할 수 있다.

public <T extends Number, R extends CharSequence> void printNonStatic(T t, R r) {
    System.out.println("T " + t + ", R " + r);
}

 여러 클래스 혹은 인터페이스에 대해서 Bound를 지정할 수 있다.

<T extends B1 & B2 & B3>

타입 T는 B1, B2, B3를 모두 상속 받는 타입이어야 한다.

Class A {}
interface B {}
interface C {}

class Generic <T extends A & B & C> {}

Class 와 interface를 모두 bound 하는 타입의 경우에는 클래스 타입이 제일 먼저 선언되어야 한다.

class Generic <T extends B & A & C> {} //compile error
public static <T> int countGreaterThan(T[] array, T elem) {
    int count = 0;
    for(T e : array) {
        if (e > elem) { //compile error
            ++count;
        }
    }

    return count;
}

Generic 타입 T는 short, int, double, long과 같은 primitive type이 지정될 수 없다. if (e > elem) 연산은 primitive 타입에서만 가능하기 때문에 compile 오류가 발생한다. 이런 경우 generic 클래스인 Comparable<T> 타입을 bound하여 사용할 수 있다.

public interface Comparable<T> {
    public int compareTo(T o);
}

public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;
}

 

Generic, Inheritance, and Subtypes

두 타입이 서로 is a 관계일 때 한 타입의 인스턴스를 다른 타입의 인스턴스로 할당할 수 있다. 예를 들어 Object는 Integer의 상위 타입 중 하나이므로 Integer 인스턴스를 Object 인스턴스에 할당할 수 있다.

Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger;   // OK

Integer 타입은 Number 타입과 is a 관계이므로 아래와 같은 코드도 문제 없다.

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

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

Generic의 타입 파라미터의 타입도 위와 마찬가지로 동작한다.

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

하지만 Generic 타입 자체는 위와 같이 동작하지 않는다.

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

boxTest 메소드의 파라미터로 Box<Number> 타입이 선언되었다. boxTest에 파라미터로 전달 할 수 있는 타입에 Box<Integer>, Box<Long>, Box<Double>과 같은 타입은 전달 할 수 없다. Box<Number> 타입과 Box<Long>, Box<Double>은 엄연히 서로 다른 타입일 뿐더러 is a 관계가 성립하지 않기 때문이다.

출처&nbsp;https://dev.java/learn/generics/intro/

 

Generic 클래스 Subtyping

Generic 클래스 혹은 Generic 인터페이스를 확장하거나 구현하여 subtyping을 할 수 있다. 클래스 또는 인터페이스의 타입 매개변수와 다른 클래스 또는 인터페이스의 타입 매개변수 관계는 extends, implements 절에 의해서 결정된다.

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList<E> 타입은 AbstractList<E>를 extends 하고 List<E> 인터페이스를 implements 한다. 눈여겨 볼 부분은 타입 파라미터 E가 모두 동일하다는 것이다. 타입 argument를 변경하지 않는한 타입 간에는 하위 타입 관계가 유지된다.

출처&nbsp;https://dev.java/learn/generics/intro/

package com.example.test.generic;

import java.util.ArrayList;

public class AppendLogList<E> extends ArrayList<E> {

	private final String append;
	public AppendLogList(String append) {
		this.append = append;
	}

	@Override
	public boolean add(E e) {
		System.out.println(e + ", " + append);
		return super.add(e);
	}
}
void appendLogAddOne(ArrayList<Integer> arrayList) {
    arrayList.add(1);
}
AppendLogList<Integer> appendLogList = new AppendLogList<>("appended!!!");
appendLogAddOne(appendLogList);
========================================
결과
1, appended!!!

AppendLogList<Integer> 타입과 ArrayList<Integer>는 서로 상속 관계이므로 appendLogAddOne 메소드에 AppendLogList<Integer> 타입을 전달 할 수 있다.

AppendLogList<String> appendLogList = new AppendLogList<>("appended!!");
appendLogAddOne(appendLogList);

위와 같이 호출하면 AppendLogList<String>과 ArrayList<Integer>는 서로 전혀 다른 타입이 되어 아래와 같은 오류가 발생한다.

error: incompatible types: AppendLogList<String> cannot be converted to ArrayList<Integer>

 

Wildcards

Upper Bounded Wildcards

예를 들어 List<Integer>, List<Double>, List<Number> 에서 작동하는 메서드를 작성 하고 싶다고 할 때 Upper Bounded Wildcards를 사용하여 작성할 수 있다.

Upper Bounded Wildcard는 다음과 같은 형식으로 사용할 수 있다.

public static void process(List<? extends Number> list) {}

extends 혹은 implements 앞에 wildcard 문자('?') 를 사용하여 표현한다.

List<Number> 타입은 Number 타입의 List만 사용할 수 있지만 List<? extends Number> 타입은 Number 타입과 Number의 subtype을 사용할 수 있다.

public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list)
        s += n.doubleValue();
    return s;
}

위 코드에서 Number에 정의된 모든 메서드를 Number 인스턴스 n 에 사용할 수 있다.

 

Unbounded Wildcards

Unbounded wildcard 유형은 wildcard 문자('?')를 사용하여 지정한다. (e.g. List<?>)

Unbounded wildcard를 사용하기 위한 두 가지 시나리오가 있다.

  • Object 클래스에서 제공하는 기능을 사용하여 구현할 수 있는 메서드를 작성하는 경우
  • 코드가 타입 매개변수에 의존하지 않는 Generic 클래스의 메서드를 사용하는 경우.
    • List.size(), List.clear()와 같은.
    • Class<?>가 자주 사용되는 이유는 Class<T>의 메서드 대부분이 T에 의존하지 않기 때문임.
public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}
========================================================
List<Integer> intList = List.of(1, 2, 3, 4, 5);
printList(intList);
========================================================
위와 같이 사용하면 List<Integer> 인스턴스를 printList에 전달하면 
List<Object>와 List<Integer>는 서로 타입이 맞지 않기 때문에 compile error가 발생한다.

위 코드에서 printList 메서드의 목적은 인자로 전달된 List 객체의 각 element를 출력하기 위함이다. 하지만 List<Object> 타입이기 때문에 List<Integer>, List<String> 타입의 인스턴스를 전달 할 수 없다. 또한 printList 메서드는 각 element를 출력만 하기 때문에 Generic 타입 T의 영향을 받지 않는다. System.out.println(..)은 내부적으로 인자로 전달된 인스턴스의 toString()을 호출하기 때문에 Object 클래스에서 제공하는 기능을 사용한다. 이러한 이유로 아래와 같이 Unbounded wildcards를 사용하는 것이 좋다.

public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}

여기서 알 수 있는 것은 List<타입>은 List<?>의 subtype이 된다는 것이다.

List<Object>와 List<?>는 동일하지 않다. List<Object> 은 List에 Object 혹은 Object의 subtype을 원소로 넣을 수 있지만 List<?>는 null만 넣을 수 있다.

 

Lower Bounded Wildcards

Upper Bounded Wildcards와 반대로 super가 사용된다.

Lower Bounded Wildcard는 다음과 같은 형식으로 사용할 수 있다.

public static void process(List<? super Integer> list) {}

위 메서드는 List<Integer>, List<Number>, List<Object> 타입의 인자를 전달 받을 수 있다.

 

 

Wildcards and Subtyping

Generic 클래스나 Generic 인터페이스는 단순히 타입 파라미터간에 관계가 있다고 해서 Generic 클래스 간의 연관관계가 있는 것은 아니다. 하지만 Wildcard(<?>)를 사용하여 Generic 클래스 간의 연관관계를 만들 수 있다.

class Number {}
class Integer extends Number {}

List<Integer> listInteger = new ArrayList();
List<Number> listNumbner = listInteger;   //type이 맞지 않아 compile error

List<? extends Integer> listInteger = new ArrayList<>();
//List<? extends Integer>는 List<? extends Number>의 subtype이 되어 아래 구문은 정상이다.
List<? extends Number> listNumber = listInteger;

아래 다이어그램은 Upper & Lower wildcard를 모두 사용하여 선언된 여러 List 클래스 간의 관계를 보여준다.

출처 https://dev.java/learn/generics/wildcards/

 

Wildcard 캡처 and Helper 메서드

어떤 경우에는 컴파일러가 wildcard의 유형을 유추하기도 한다. 예를 들어 List<?>로 정의될 수 있지만, 컴파일러는 표현식을 평가할 때 코드에서 특정 유형을 유추한다. 이러한 경우를 wildcard 캡처라고 한다.

import java.util.List;

public class WildcardError {

    void foo(List<?> i) {
    	//여기서 i 인스턴스에 set되는 타입을 알 수 없으므로 컴파일 오류가 발생한다.
        //List<?> 타입 i 인스턴스에서 i.get(0)를 호출하면 
        //어떤 타입이 리턴되는지 컴파일러는 알 수 없다.
        i.set(0, i.get(0));
    }
}

위와 같은 경우에는 wildcard를 캡처하는 헬퍼 메서드를 작성하여 문제를 해결 할 수 있다.

일반적으로 helper 대상이 되는 <메서드이름>Helper()를 메서드명으로 사용한다.

public class WildcardFixed {

    void foo(List<?> i) {
        fooHelper(i);
    }

    //타입 추론을 통해서 wildcard 캡처가 수행된다.
    private <T> void fooHelper(List<T> l) {
        l.set(0, l.get(0));
    }

}

 

Wildcard 사용 가이드 라인

Generic을 사용할 때 혼란스러운 것 중 하나가 언제 Upper Bound Wildcard를 사용할지 Lower Bound Wildcard를 사용할지 결정하는 것이다. 이를 위해 Generic 인스턴스 변수를 아래 두 가지 기능 중 하나를 제공하는 것으로 생각하면 도움이 된다.

  • 'in' 변수. 'in' 변수는 코드에 데이터를 제공한다. 예를 들어 copy(src, dest) 에서 src 인수는 복사할 데이터를 제공하므로 in 변수.
  • 'out'변수. 'out' 변수는 다른 곳에서 사용할 데이터를 저장한다. copy(src, dest)에서 dest 인수는 데이터를 받으므로 out 변수.

 

wildcard를 사용할지 여부와 어떤 wildcard 유형을 사용할지 결정할 때 'in', 'out' 원칙을 적용할 수 있다. 

  • 'in'변수는 extends 키워드를 사용하여 upper bound wildcard로 정의 된다.
  • 'out'변수는 super 키워드를 사용하여 lower bound wildcard로 정의 된다.
  • Object 클래스에 정의된 메서드를 사용하여 'in' 변수에 액세스 할 수 있는 경우 unbound wildcard를 사용 한다.
  • 코드가 'in', 'out' 변수로 모두 액세스해야 하는 경우에는 wildcard를 사용하지 않는다.

위 가이드라인은 메서드의 return 타입에는 적용되지 않는다. wildcard를 return 타입으로 사용하는 것은 피하는 것이 좋다.

class NaturalNumber {

    private int i;

    public NaturalNumber(int i) { this.i = i; }
    // ...
}

class EvenNumber extends NaturalNumber {

    public EvenNumber(int i) { super(i); }
    // ...
}


List<? extends NaturalNumber> evenList = new ArrayList<>();
//Required: capture of ? extends NaturalNumber compile error
evenList.add(new NaturalNumber(35));

인스턴스 evenList는 extends 키워드를 사용한 upper bound 'in' 변수 이므로 evenList에 add를 할 수 없다고 생각하면 쉬울 것 같다.

evenList 인스턴스에 요소를 추가하기 위해서는 super 키워드를 사용한 lower bound 'out' 변수로 사용하거나 List<NaturalNumber>로 사용해야 한다.

List<? super EvenNumber> evenList = new ArrayList<>();
evenList.add(new EvenNumber(35));

참고 사이트

https://dev.java/learn/generics/intro/ 

 

Introducing Generics - Dev.java

Generics enable types (classes and interfaces) to be parameters when defining classes, interfaces and methods. Much like the more familiar formal parameters used in method declarations, type parameters provide a way for you to re-use the same code with dif

dev.java

https://dev.java/learn/generics/wildcards/

 

Wildcards - Dev.java

In generic code, the question mark (?), called the wildcard, represents an unknown type. The following section discuss wildcards in more detail, including upper bounded wildcards, lower bounded wildcards, and wildcard capture.

dev.java

 

댓글

💲 추천 글