[JAVA] 제네릭 제한된 타입 파라미터와 와일드 카드 타입, 제네릭 타입의 상속과 구현

dejeong·2024년 9월 19일
1

JAVA

목록 보기
15/24
post-thumbnail

Object 타입을 사용한 Box 클래스는 모든 종류의 객체를 저장할 수 있다. Object 클래스가 모든 자바 클래스의 최상위 부모이기 때문에, 모든 객체는 자동으로 Object 타입으로 변환되어 저장될 수 있으나, 이를 읽어올 때는 강제 타입 변환이 필요하다.

Box box = new Box();
box.set("hello");                 // String 타입을 Object 타입으로 자동 변환하여 저장
String str = (String) box.get();   // Object 타입을 String 타입으로 강제 변환하여 가져옴

이 방식은 다양한 객체를 저장할 수 있지만, 강제 타입 변환을 해야 하는 불편함이 있다. 잘못된 타입으로 변환할 경우, ClassCastException이 발생할 위험도 있다.

제네릭을 사용하면 타입 안정성을 확보하면서도 타입 변환을 할 필요가 없다.

public class Box<T> {
    private T t;

    public T get() {
        return t;
    }

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

Box 클래스를 사용할 때, 타입 파라미터 T를 구체적인 타입으로 지정

Box<String> box = new Box<String>();

이 경우, TString으로 대체되며, Box 클래스 내부는 자동으로 다음과 같이 재구성

public class Box<String> {
    private String t;

    public String get() {
        return t;
    }

    public void set(String t) {
        this.t = t;
    }
}

set() 메소드는 String 타입만 허용하고, get() 메소드는 String 타입을 반환

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

Util : 프로젝트에서 자주 사용하는 값을 정적인 값으로 선언하여 외부에서 호출하여 사용하는 방식이 자주 이용된다. (static 변수 = 정적 변수 = 클래스 변수)

public class Util {
    public static final double PI = 3.14159;
}

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

타입 파라미터에 구체적인 타입을 제한하는 기능, 타입을 상속받은 자식들만 올 수 있다. 메소드는 매개값으로 Number타입 혹은 Byte, Short, Integer, Long, Double타입의 인스턴스만 가져야 한다.

Number라는 추상 클래스를 상속받고 있다. → 숫자 정수형을 사용할 수 있게 된다.

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

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

최상위 타입에 Number을 지정해주고 타입 파라미터 를 지정해주면 Number라는 타입을 가지고 있는 클래스들만 매개변수로 올 수 있다(T t1, T t2). 타입을 상속받고 있는 클래스만 오도록 제한을 해주는 것이다. Number 클래스에서 제공해주는 메서드를 사용할 수 있다.

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

compare 메서드와 를 사용하여 입력 받는 값((T t1, T t2))을 제한받고, 입력 받은 값을 비교하는 코드 작성, 비교를 위해 타입을 제한하는 것

타입에 안정성을 가질 수 있도록 만들어준다.


ctrl + alt + b로 상속받고 있는 것들을 확인할 수 있다.

public class Util {
    // -1, 0, 1
    public static <T extends Number> int compare(T t1, T t2){
        // 실수형
        double value1 = t1.doubleValue(); // 매개변수로 입력받은 t1을 double 타입으로 변환해준 것. compare을 사용하기 위해
        double value2 = t2.doubleValue();

        return Double.compare(value1, value2);
    }
}
public class BoundedTypeExample {
    public static void main(String[] args) {
        int result = Util.compare(1, 2);
        System.out.println(result);

        int result2 = Util.compare(4.5, 1.2);
        System.out.println(result2);
        
        int result3 = Util.compare(3, 3);
        System.out.println(result3);
    }
}
-1
1 
0

문자열 비교시 에러 발생된다.

compare 메서드

제네럴에서는 T(타입)를 가장 많이 사용하고, 엘리먼트의 약자인 E도 사용한다.(주로 약어를 사용)


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

타입을 제한, 매개 변수 혹은 리턴 타입으로도 사용할 수 있다.

  • 제네릭타입<?> : Unbounded Wildcards (제한 없음)
    • 타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있다.
  • 제네릭 타입<? extends 상위타입> : Upper Bounded Wildcards (상위 클래스 제한)
    • 타입 파라미터를 대치하는 구체적인 타입으로 상위 타입이나 하위 타입만 올 수 있다.
  • 제네릭 타입<? super 하위타입> : Lower Bounded Wildcards (하위 클래스 제한)
    • 타입 파라미터를 대치하는 구체적인 타입으로 하위 타입이나 상위 타입이 올 수 있다.
    package chap10.wildcard;
    
    public class Person {
        private String name;
    
        // 생성자는 리턴 타입이 없어도 된다.
        public Person(String name){
            this.name = name;
        }
    
        // 메서드는 리턴 타입이 있어야 한다.
        public String getName() {
            return name;
        }
    
        // toString 재정의, 해쉬값이 아닌 이름이 출력될 수 있도록
        @Override
        public String toString() { // Object 에서 물려 받음
            // return super.toString(); // 부모의 toString 출력
            return name; // 입력 받은 이름을 바로 리턴할 수 있게 return getName 으로 해도 됨
        }
    }
    // 자식 클래스
    package chap10.wildcard;
    
    public class Worker extends Person{
        public Worker(String name){
            super(name);
        }
    }
    
    package chap10.wildcard;
    
    public class Student extends Person{
        Student(String name){
            super(name);
        }
    }
    
    package chap10.wildcard;
    
    public class HighStudent extends Student {
        HighStudent(String name){
            super(name);
        }
    }
    package chap10.wildcard;
    
    public class Course<T>{
        private String name;
        private T[] students;   // Person[] students
    
        // 생성자(리턴타입은 없으나, 접근 제한은 가능함)
        Course(String name, int capacity){
            this.name = name; // 입력 받은 이름으로 현재 필드 초기화
            this.students = (T[]) new Object[capacity]; // 배열 초기화, 가상의 값이기 때문에 Object 로 배열의 객체를 생성, 배열의 길이 지정  Object 라는 타입이기 때문에 캐스팅 해주어야 한다.course의 타입을 지정하는 당시에 배열의 타입이 지정이 될 것임.
        }
        // name 이 private 이기 때문에 외부에서 호출할 수 있에 getter 메서드 생성
        public String getName() {
            return name;
        }
    
        // students 가 private 이기 때문에 외부에서 호출할 수 있에 getter 메서드 생성
        public T[] getStudents() {
            return students;
        }
    
        void add(T t){
            for(int i = 0; i < students.length; i++){
                if(students[i] == null){
                    students[i] = t;
                    break;
                }
            }
        }
    }
    package chap10.wildcard;
    
    import java.util.Arrays;
    
    public class WildCardExample {
        public static void registerPerson(Course<?> t){
            t.getStudents(); // 목록 호출
            Arrays.toString(t.getStudents()); // 배열 각각의 요소 출력
            System.out.println(t.getName() + ": " + Arrays.toString(t.getStudents())); // 각각의 객체의 주소값(해쉬값)이 출력됨, Object 메서드에 그렇게 선언되어 있음
        }
    
        public static void registerPerson2(Course<? extends Student> t){
            System.out.println(t.getName() + ": " + Arrays.toString(t.getStudents()));
        }
    
        public static void registerPerson3(Course<? super Worker> t){
            System.out.println(t.getName() + ": " + Arrays.toString(t.getStudents()));
        }
    
        public static void main(String[] args) {
            // Course<Person> personCourse = new Course<>(); // 디폴트 생성자 호출
            Course<Person> personCourse = new Course<>("일반인 과정", 4); // 디폴트 생성자 호출
            personCourse.add(new Person("일반인"));
            personCourse.add(new Student("학생"));
            personCourse.add(new Worker("직장인"));
            personCourse.add(new HighStudent("고등학생"));
    
            Course<Worker> workerCourse = new Course<>("직장인 과정",4);
            workerCourse.add(new Worker("직장인2"));
    
            Course<Student> studentCourse = new Course<>("학생 과정", 4);
            studentCourse.add(new Student("학생3"));
            studentCourse.add(new HighStudent("고등학생3"));
    
            Course<HighStudent> highStudentCourse = new Course<>("고등학생 과정", 4);
            highStudentCourse.add(new HighStudent("고등학생4"));
    
            System.out.println("============");
            registerPerson3(workerCourse);
            registerPerson3(personCourse);
    
            System.out.println("============");
            registerPerson2(studentCourse);
            registerPerson2(highStudentCourse);
    
            System.out.println("============");
            registerPerson(personCourse);
            registerPerson(workerCourse);
            registerPerson(studentCourse);
            registerPerson(highStudentCourse);
        }
    }
    
    package chap10.wildcard;
    
    public class CourseTypeExample {
        public static void main(String[] args) {
            Person person = new Person("일반인");
            Person person2 = new Student("학생");
            Person person3 = new Worker("직장인");
            Person person4 = new HighStudent("고등학생");
    
            Course<Person> personCourse = new Course<>("일반인 과정", 4);
            personCourse.add(person);
    
            // 위 코드를 한줄로 표현하면 personCourse.add(new Person()); Ctrl + Alt + N
    
            Course<Worker> workerCourse = new Course<>("직장인 과정", 5);
            workerCourse.add(new Worker("직장인2")); // worker와 worker의 자식만 들어갈 수 있다.
    
            Course<Student> studentCourse = new Course<>("학생 과정", 4);
            studentCourse.add(new Student("학생3"));
            studentCourse.add(new HighStudent("고등학생3"));
    
            Course<HighStudent> highStudentCourse = new Course<>("고등학생 과정", 4);
            highStudentCourse.add(new HighStudent("고등학생4"));
        }
    }

제네릭 타입의 상속과 구현

상속(inherit)

제네릭 타입도 상속받을 수 있도록 정의해야 한다. 자식 제네릭 타입은 추가적으로 타입 파라미터를 가질 수 있다. 상속 관계에서는 부모 클래스 타입이 자식 클래스를 품을 수 있다.

package chap10.inherit;

// 부모 제네릭 클래스
public class Product<T, M>{
    private T kind;
    private M model;

    // 생성자
    Product(T kind, M model) {
        this.kind = kind; // 타입 추론
        this.model = model;
    }

    public T getKind() {
        return kind;
    }

    public M getModel(){
        return model;
    }
}
package chap10.inherit;

public class ChildProduct<T, M, C> extends Product<T, M> {
    private C company;

    // 생성자
    ChildProduct(T kind, M model, C company){
        super(kind, model); // 상속 받은 것임으로 부모 호출
        this.company = company;
    }

    public C getCompany() {
        return company;
    }
}

컴파일 되는 시점에 타입이 유연하게 바뀔 수 있다.

ChildProduct<String, String, String>ChildProduct<Tv, String, String>

package chap10.inherit;

import java.util.ArrayList;

public class InheritGenericExample {
    public static void main(String[] args) {
        // 타입 = 클래스이기 때문에 Tv가 타입이 된다.
        Tv tv = new Tv();
        ChildProduct<Tv, String, String> childProduct = new ChildProduct(new Tv(), "galaxy book", "samsung"); // new Tv() 생성자를 호출해서 객체를 만들어줌, 첫번째 제네릭 타입은 Tv 라는 타입이 된다.

        // ArrayList<Tv> tvList = new ArrayList<>();

        String company = childProduct.getCompany();
        System.out.println(childProduct.getCompany());
    }
}

구현(implements)

제네릭 인터페이스를 구현한 클래스도 제네릭 타입이 된다.

package chap10.implement;

// 데이터를 들고 있는 저장소라고 생각
public interface Storage<T> {
    void add(T item, int index); // 값을 넣어줄 수도 있고
    
    T get(int index); // 값을 get 해줄 수도 있다.
}

제네릭 인터페이스인 Storage 타입을 구현한 StorageImpl 클래스도 제네릭 타입이어야한다.

package chap10.implement;

// Stogage 의 구현체
// 구현체에서도 타입 파라미터를 가져다 써야 한다.
public class StorageImpl<T> implements Storage<T> {
    private T[] array; // 필드 생성

    // 생성자
    public StorageImpl(int capacity){
        array = (T[]) new Object[capacity]; // 배열 생성
    }

    @Override
    public void add(T item, int index) {
        array[index] = item;
    }

    @Override
    public T get(int index) {
        return array[index];
    }
}
package chap10.implement;

import java.util.ArrayList;

public class ImplementExample {
    public static void main(String[] args){
        StorageImpl<String> storage = new StorageImpl<>(10);
        storage.add("첫번째",0);
        storage.add("두번째",1);
        storage.add("세번째",2);

        String result = storage.get(1);
        System.out.println(result);

        // ArrayList 사용
        ArrayList<String> storageList = new ArrayList<>();
        storageList.add(0, "문자열1");
        storageList.add(1, "문자열2");
        storageList.get(1);
    }
}
profile
룰루

0개의 댓글