상세 컨텐츠

본문 제목

제네릭 정리

기록 - 프로그래밍/Java

by wjjun 2024. 1. 31. 01:03

본문

 

제네릭 사용 목적

컬렉션, 람다식, 스트림, NIO에서 사용되므로 확실하게 이해할 필요가 있습니다. API 도큐먼트를 보면 제네릭 표현이 많기 때문에 제네릭을 이해하지 못하면 API 도큐먼트를 정확히 이해할 수 없습니다. 제네릭은 클래스와 인터페이스, 메서드를 정의할 때 타입(type)을 파라미터(parameter)로 사용할 수 있도록 합니다. 타입 파라미터는 코드 작성 시 구체적인 타입으로 대체되며 다양한 코드를 생성할 수 있게 해줍니다.

 

1. 컴파일 시점에 강한 타입 체크를 할 수 있습니다. 자바 컴파일러는 코드에서 잘못 사용된 타입 때문에 발생하는 문제점을 제거하기 위해 제네릭 코드에 대해 강한 타입 체크를 합니다. 실행 시 타입 에러가 나는 것보다 컴파일 시 미리 타입을 강하게 체크해서 에러를 사전에 방지합니다. 2. 타입 변환을 제거합니다. 비제네릭 코드는 불필요한 타입 변환을 하기 때문에 프로그램 성능에 악영향을 미칩니다. 다음 코드에서는 List에 문자열 요소를 저장했지만, 요소를 찾아올 때는 반드시 String으로 타입 변환을 해야 합니다. 하지만 제네릭 코드를 사용하여 List에 저장되는 요소를 String 타입으로 국한하기 때문에 요소를 찾아올 때 타입 변환을 할 필요가 없어 성능이 향상됩니다.

List<String> list = new ArrayList<String>();
list.add("hi");
String str = list.get(0); // 타입 변환을 하지 않습니다.

 

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

제네릭 타입은 타입을 파라미터로 갖는 클래스와 인터페이스를 의미합니다. 제네릭 타입은 클래스 또는 인터페이스 이름 뒤에 "<>" 부호가 붙고, 사이에 타입 파라미터가 위치합니다.

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

 

타입 파라미터는 변수명과 동일한 규칙에 따라 작성할 수 있습니다. 일반적으로 대문자 알파벳 한 글자로 표현합니다. 제네릭 타입을 실제 코드에서 사용하려면 타입 파라미터에 구체적인 타입을 지정해야 합니다. 그럼 어떤 타입 파라미터를 사용해야 하는지 확인해보겠습니다.

public class Box {
    private Object object;
    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}

BOX 클래스의 필드 타입이 Object인데 Object 타입으로 선언한 이유는 필드에 모든 종류의 객체를 저장하고 싶어서입니다. Object 클래스는 모든 자바 클래스의 최상위 부모 클래스입니다. 그래서 자식 객체는 부모 타입에 대입할 수 있는 성질때문에 모든 자바 객체는 Object 타입으로 자동 타입 변환되어 저장됩니다.

 

Object object = 자바의 모든 객체;

set() 메서드는 매개 변수 타입으로 Object를 사용하면서 매개값으로 자바의 모든 객체를 받을 수 있게 했습니다. 받은 매개값을 Object 필드에 저장합니다. 반대로 get() 메서드는 Object 필드에 저장된 객체를 Object 타입으로 리턴합니다. 만약 필드에 저장된 원래 타입의 객체를 얻기 위해서는 강제 타입 변환이 필요하게 됩니다.

Box box = new Box();
box.set("hi");
String str = (String) box.get();

 

Object 타입을 사용하면 모든 종류의 자바 객체를 저장할 수 있는 장점은 있지만 저장할 때 타입 변환, 사용할 때 타입 변환이 발생합니다.

해결방법은 제네릭입니다.

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

타입 파라미터 T를 사용해서 Object 타입을 모두 T로 대체했습니다. T는 Box 클래스로 객체를 생성할 때 구체적인 타입으로 변경합니다.

 

Box<String> box = new Box<String>();
box.set("hi");
String str = box.get();

필드 타입이 String으로 변경되었고 set() 메서드도 String 타입만 매개값으로 받을 수 있게 변경되었습니다. 그리고 get() 메서드 역시 String 타입으로 리턴하도록 변경되었습니다. 

 

멀티 타입 파라미터 (class<K, V... >, interface<K, V...>)

제네릭 타입은 두 개 이상의 멀티 타입 파라미터를 사용할 수 있습니다. 이 경우 각 타입 파라미터를 콤마로 구분합니다. 다음 예제에서 Product<T, M> 제네릭 타입을 정의하고 ProductExample 클래스에서 Product<Tv, String> 객체와 Product<Car, String> 객체를 생성합니다. 그리고 getter, setter를 호출합니다.

// 제네릭 클래스
public class Product<T, M> {
    private T kind;
    private M model;
    
    public T getKind() {return this.kind;}
    public M getModel() {return this.model;}
    
    public void setKing(T king) {this.king = kind;}
    public void setModel(M model) {this.model = model;}
    
    
// 제네릭 객체 생성
public class ProductExample {
    public static void main(String[] args) {
        Product<Tv, String> p1 = new Product<Tv, String>();
        p1.setKind(new Tv();
        p1.setModel("스마트");
        Tv tv = p1.getKind();
        String tvModel = p1.getModel();
        
        Product<Car, String> p2 = new Product(Car, String>();
        p2.setKind(new Car());
        p2.setModel("디젤");
        Car car = p2.getKind();
        String carModel = p2.getModel();
   }
}

제네릭 타입 변수 선언과 객체 생성을 동시에 할 때 타입 파라미터 자리에 구체적인 타입을 지정하는 코드가 중복해서 나와 다소 복잡할 수 있습니다. 자바 7부터 제네릭 타입 파라미터의 중복기술을 줄이기 위해 <> 연산자를 제공하고 있습니다. 자바 컴파일러는 타입 파라미터 부분에 <> 연산자를 사용하면 타입 파라미터를 유츄해 자동으로 설정합니다.

Prodcut<Tv, String> product = new Product<>();

 

 

제네릭 메서드(<T, R> R method(T t))

제네릭 메서드는 매개 타입과 리턴 타입으로 타입 파라미터를 갖는 메서드를 말합니다. 제네릭 메서드를 선언하는 방법은 리턴 타입 앞에 <> 기호를 추가하고 타입 파라미터를 기술하고 리턴 타입과 매개 타입으로 타입 파라미터를 사용하면 됩니다.

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

 

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

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

 

제네릭 메서드는 두 가지 방식으로 호출할 수 있습니다. 코드에서 타입 파라미터의 구체적인 타입을 명시적으로 지정해도 되며 컴파일러가 매개값의 타입을 보고 구체적인 타입을 추정하도록 할 수 있습니다.

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

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

// 제네릭 메서드
public class Util {
    public static <T> Box<T> boxing(T t) {
        Box<T> box = new Box<T>();
        box.set(t);
        return box;
    }
}

public class BoxingMethodExample {
    public static void main(String[] args) {
        Box<Integer> box1 = Util.<Integer>boxing(100);
        int intValue = box1.get();
        
        Box<String> box2 = Util.boxing("홍길동");
        String strValue = box2.get();
    }
}

정적 제네릭 메서드로 boxing()을 정의하고 BoxingMethodExample 클래스에서 호출하는 코드

 

 

제한된 타입 파라미터 (<T extends 최상위타입>)

구체적인 타입을 제한할 필요가 있습니다. 예를 들어 숫자를 연산하는 제네릭 메서드 매개값으로는 Number 또는 하위 클래스 타입(Byte, Short, Integer, Long, Double) 인스턴스만 가져야 합니다. 이것이 제한된 타입 파라미터가 필요한 이유입니다. 제한된 타입 파라미터를 선언하려면 타입 파라미터 뒤에 extends 키워드를 붙이고 상위 타입을 명시하면 됩니다. 상위 타입은 클래스 뿐만 아니라 인터페이스도 가능합니다. 인터페이스라고 해서 implements를 사용하지 않습니다.

public <T extends 상위타입> 리턴타입 메서드(매개변수, ...) { ... }

 

타입 파라미터에 지정되는 구체적인 타입은 상위 타입이거나 상위 타입의 하위 또는 구현 클래스만 가능합니다. 주의할 점은 메서드 중괄호 { } 안에서 타입 파라미터 변수로 사용 가능한 것은 상위 타입의 멤버(필드, 메서드)로 제한됩니다. 숫자 타입만 구체적인 타입으로 갖는 제너릭 메서드 compare(). 두 개의 숫자 타입을 매개값으로 받아서 처리하여 차이를 리턴합니다.

public <T extends Number> int compare(T t1, T t2) {
    double v1 = t1.doubleValue(); // Number의 doubleValue() 메서드 사용
    double v2 = t2.doubleValue(); // Number의 doubleValue() 메서드 사용
    return Double.compare(v1, v2);
}

Doble.compare() 메서드는 첫 번째 매개값이 작으면 -1을, 같으면 0을, 크면 1을 리턴합니다.

 

public class Util {
    public static <T extedns Number> int compare(T t1, T t2) {
        double v1 = t1.doubleValue();
        double v2 = t2.doubleValue();
        return Double.compare(v1, v2);
    }
}

public clas A {
    public void static main(String[] args) {
        int result1 = Util.compare(10, 20) // int -> Integer -> 자동 Boxing
        sout(result1);
        
        int result2 = Util.compare(4.5, 3); // 4.5 -> double -> Double 자동 Boxing
        sout(result2);
    }
}

 

와일드카드 타입(<?>, <? extends ...>, <? super ...>)

코드에서 ?는 와일드코드로 사용됩니다. 제네릭 타입을 매개값이나 리턴 타입으로 사용할 때 구체적인 타입 대신 와일드 카드를 다음 세 가지 형태로 사용할 수 있습니다.

 

제네릭타입<?> : Unbounded Wildcards(제한 없음)

타입 파라미터를 대처하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있습니다.

 

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

타입 파라미터를 대처하는 구체적인 타입으로 상위 타입이나 하위 타입만 올 수 있습니다.

 

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

타입 파라미터를 대치하는 구체적인 타입으로 하위 타입이나 상위 타입이 올 수 있습니다.

 

public class Course<T> {
    private String name;
    private T[] students;
    
    public Course(String name, int capacity) {
        this.name = name;
        students = (T[]) (new Object[capacity]); // 타입 파라미터로 배열을 생성하려면 new T[n] 형태로 배열을 생성할 수 없고 (T[]) (new Object[n])로 생성해야 합니다.
    }
    
    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;
            }
        }
    }
}

Course<?> 수강생은 모든 타입(Person, Worker, Student, HighStudent)가 될 수 있다

Course<? extends Student> 수강생은 Student와 HighStudent만 될 수 있다

Course<? super Worker> 수강생은 Worker와 Person만 될 수 있다

 

public class WildCardExample {

    // 모든 과정
    public static void registerCourse( Course<?> course ) {
        sout(course.getName() + " 수강생 : " + Arrays.toString(course.getStudents()));
    }
    
    // 학생과정
    public static void registerCourseStudent( Course<? extends Student> course) {
        sout(course.getName() + " 수강생 : " + Arrays.toString(course.getStudents()));
    }
    
    // 직장인과 일반인 과정
    public static void registerCourseWorker( Course<? super Worker> course ) {
        sout(course.getName() + " 수강생 : " + Arrays.toString(course.getStudents()));
    }
    
    public static void main(String[] args) {
        Course<Person> personCourse = new Course<Person>("일반인 과정", 5);
            personCourse.add(new Person("일반인");
            personCourse.add(new Worker("직장인");
            personCourse.add(new Student("학생");
            personCourse.add(new HighStudent("고등학생");
        Course<Worker> workerCourse = new Course<Worker>("직장인 과정", 5);
            workerCourse.add(new Worker("직장인");
        Course<Student> studentCourse = new Course<Student>("학생과정", 5);
            studentCourse.add(new Student("직장인");
            studentCourse.add(new HighStudent("고등학생");
        Course<Student> highStudentCourse = new Course<HighStudent>("고등학생과정", 5);
            highStudentCourse.add(new HighStudent("고등학생");
            
        // 모든 과정 등록 가능
        registerCourse(personCourse);
        registerCourse(workerCourse);
        registerCourse(studentCourse);
        registerCourse(highStudentCourse);
        
        // 학생 과정만 등록 가능
        registerCourseStudent(studentCourse);
        registerCourseStudent(highStudentCourse);
        
        // 직장인과 일반인 과정만 등록 가능
        registerCourseWorker(personCourse);
        registerCourseWorker(workerCourse);
    }
}

 

 

제네릭 타입의 상속과 구현

제네릭 타입도 다른 타입처럼 부모 클래스가 될 수 있습니다. Product<T, M> 제네릭 타입을 상속해서 ChildProduct<T, M> 타입을 정의합니다.

public class ChildProduct<T, M> extends Product<T, M> { ... }

자식 제네릭 타입은 추가적으로 타입 파라미터를 가질 수 있습니다. 다음은 세 가지 타입 파라미터를 가진 자식 제네릭 타입을 선언한 것입니다.

 

public class ChildProduct<T, M, C> extends Product<T, M> { ... }

// 부모 제네릭 클래스
public class Product<T, M> {
    private T king;
    private M model;
    
    public T getKind() { return this.kind; }
    public M getModel() { return this.model; }
    
    public void setKing(T kind) { this.kind = kind; }
    public void setModel(M model) { this.model = model; }
}

class Tv{}


// 자식 제네릭 클래스
public class ChildProduct<T, M, C> extends Product<T, M> {
    private C company;
    public C getCompany() { return this.company; }
    public void setCompany(C company) {this.company = company; }
}


// 제네릭 인터페이스
public interface Storage<T> {
   public void add(T item, int index);
   public T get(int index);
}


// 제네릭 구현클래스
public class StorageImpl<T> implements Storage<T> {
    private T[] array;
    
    public StorageImpl(int capacity) {
        this.array = (T[]) (new Object[capacity]); // 타입 파라미터로 배열을 생성하려면 new T[n] 형태로 생성이 불가하고 (T[]) (new Object[n])으로 생성해야 합니다.
    }
    
    @Override
    public void add(T item, int index) {
        array[index] = item;
    }
    
    @Override
    public T get(int index) {
        return array[index];
    }
}

 

아래는ChildProductAndStorageExample은 ChildProduct<T, M, C>와 StorageImpl<T> 클래스의 사용방법을 보여줍니다.

public class ChildProductAndStorageExample {
    public static void main (String[] args) {
        ChildProduct<Tv, String, String> product = new ChildProduct<>();
        product.setKind(new Tv());
        product.setModel("smartTV");
        product.setCompany("Samsung");
        
        Storage<Tv> storage = new StorageImpl<Tv>(100);
        storage.add(new Tv(), 0);
        Tv tv = storage.get(0);
    }
}

'기록 - 프로그래밍 > Java' 카테고리의 다른 글

함수 인터페이스 (Consumer, Supplier, Function)  (1) 2024.02.03
람다식 사용법  (0) 2024.02.02
스레드 상태제어  (1) 2024.01.30
스레드 생성 방법 (Runnable)  (0) 2024.01.29
날짜 파싱 메서드 (Formatting)  (1) 2024.01.28

관련글 더보기

댓글 영역