본문 바로가기
JAVA

제네릭

by 융디's 2024. 4. 26.
728x90
제네릭

제네릭

@2024.04.15

제네릭(Genenic)

💡
Java5에서 추가된 문법으로,
프로그래밍 언어에서
타입을 파라미터로 사용할 수 있게 하는 기능이다.
  • 컴파일 시점에 타입을 체크하며, 타입 안전성이 보장되고 코드의 가독성이 향상
  • 주로 컬렉션 라이브러리에 많이 사용하며, 객체의 타입을 컴파일 시에 체크하는 기능
  • 제네릭이 없었을 때는 매번 형 변환이 필요
    • 캐스팅을 줄임으로써, 코드의 오류 가능성을 줄이고 유지 보수성 향상
    List list = new ArrayList();
    list.add("text");
    list.add(5);
    
    //list.get(0)일시Object 값으로 반환
    //부모가 자식을 가리킬순있어도 자식은 부모를 가리킬순 없음 => 형변환 필요!
    String str = (String)list.get(0);
    int num = (Integert)list.get(1);
    
    //제네릭을 사용하면 매번 형변환 할 필요 없음
    // 문자열만 저장하는 리스트 
    //-> 컴파일시 타입 체크가 이루어져 잘못된 타입 객체가 저장되는것을 방지
    List<String> strList = new ArrayList<>(); 
    String s = strList.get(0);
  • 일반적으로 쓰이는 제네릭
    • <T> → Type
    • <E> → Element
    • <K> → Key
    • <V> → Value
    • <N> → Number

제네릭 타입

💡
자바 프로그래밍에서 데이터 타입을 일반화하는 방법으로,
이를 통해 클래스, 인터페이스, 메서드가 다양한 타입의 객체를 다룰 수 있도록 해준다.
  • 코드의 재사용성을 높이고, 타입의 안전성을 강화

제네릭 클래스/인터페이스

  • 클래스/인터페이스 이름 뒤에 꺾쇠 안에 타입 파라미터를 선언
    • class Box<T>
    • inferface Box<T>
  • 타입 파라미터는 클래스/인터페이스 내부에서 변수 타입, 메서드 리턴 타입, 파라미터 타입으로 사용
  • 클래스/인터페이스를 사용할 때는 구체적인 타입을 지정하여 객체화
    • Box<Integer> box = new Box<>();
    • class Exam implements Box<String>
public class Pair<K> {
	private K first;
	
	public K getFirst(){
		return first;
	}
}

제네릭 메서드

💡
다양한 타입의 객체를 동일한 방식으로 처리하는 메서드
  • 클래스 또는 인터페이스가 제네릭이 아니더라도 사용 가능
  • 매개변수 타입, 반환 타입제네릭 타입으로 사용 가능
    //예시 1. 컴파일러 타입 추론
    public <T> void Method(T data){
    }
    Integer[] intArray = {1, 2, 3, 4, 5};
    //컴파일러는 전달된 배열의 타입을 기반으로 T의 구체적인 타입을 추론
    //예를 들어, Integer[] 배열을 전달하면 T는 Integer로 추론
    Method(intArray); 
    ------------------------------------------------------------------------------------
    //주의 할 내용. 컴파일러 타입 추론 불가능
    public static void printWord(List<T> worldList){
    }
    // 메소드 수정 
    public static <T> void printWord(List<T> worldList){
    
      }
    String word = "";
    printWord(word); // 컴파일 에러 
    // 제네릭 메소드가 아닌 제네릭 클래스나 메소드 외부에서 선언된 제네릭 타입을 사용하는 상황이기 때문에 
    wordList.add(word); // 컴파일 에러 -> 메소드 수정시 에러 발생 안함  
    ------------------------------------------------------------------------------------
    // 예시 3. Integer 처리 메서드 
    public static <T> void addList(List<Integer> list, Integer element){
        list.add(element);
    }
  • 특징
    • 타입의 안전성
      • 컴파일 시점에 타입 체크하여 타입 안전성 보장
      • 런타임에 발생할 수 있는 ClassCastException 예외 방지
    • 재사용성
      • 하나의 메서드로 다양한 타입의 객체를 처리 가능
    • 타입 추론
      • 메서드 호출 시점에 컴파일러가 인자의 타입을 바탕으로 적절한 타입을 추론

      사용자는 타입을 명시적으로 제공하지 않아도 된다.

멀티 타입 파라미터

💡
클래스나 인터페이스는 둘 이상의 타입 파라미터를 가질 수 있다.
class Pair<K, V>{
	private K first;
	private V second;
	
	public K getFirst(){
		return first;
	}
	public V getSecond(){
		return second;
	}
}

Pair<Integer,String> pair = new Pair<>();

제한된(bounded) 타입 파라미터

💡
특정 타입 또는 그 타입의 하위 클래스들에 대해서만 메서드나 클래스를 사용할 수 있도록 제한을 두는 방법
  • 예시
    • Number 클래스의 서브 클래스만 타입 파라미터로 가질 수 있는 Num 클래스 정의
// Number를 상속받는 자식들만 쓸 수 있다.
//Integer, Double, Float, Long 만 가능
class Num <T extends Number>

//T타입이 Comparable 인터페이스를 구현해야한다
<T extends Comparable<T>>

//TNumber를 상속받고, 동시에 Comparable 인터페이스를 구현 해야한다.
<T extends Number & Comparable<T>>

상한(extends) & 하한(super)

💡
상한(extends)은 제네릭 타입이 특정 클래스의 하위 타입만 허용하도록 제한하는 것

하한(super)은 제네릭 타입이 특정 클래스의 상위 타입만 허용하도록 제한하는 것
  • 차이점
    • 상한(extends)
      • 주로 읽기 작업에 사용
      • 상한 타입의 메서드와 필드를 안전하게 사용 가능
    public class NumberProcessor {
        // 상한을 사용하여 제네릭 메소드 정의
        public static <T extends Number> double sum(List<T> numbers) {
            double total = 0.0;
            for (Number number : numbers) {
                total += number.doubleValue();
            }
            return total;
        }
        public static void main(String[] args) {
            List<Integer> integerList = Arrays.asList(1, 2, 3);
            List<Double> doubleList = Arrays.asList(1.5, 2.5, 3.5);
    // Integer와 Double 모두 Number의 하위 클래스이므로 메소드 사용 가능
            System.out.println("Sum of integers: " + sum(integerList));
            System.out.println("Sum of doubles: " + sum(doubleList));
        }
    }  
    • 하한(super)
      • 주로 쓰기 작업에 사용
      • 객체를 저장하는데 적합
    public class DataWriter {
        // 하한을 사용하여 제네릭 메소드 정의
        // ?는 와일드 카드 
        public static void addNumbers(List<? super Integer> list) {
            for (int i = 1; i <= 5; i++) {
                list.add(i); // Integer 추가 가능
            }
        }
        public static void main(String[] args) {
            List<Number> numberList = new ArrayList<>();
            List<Object> objectList = new ArrayList<>();
            addNumbers(numberList);
            addNumbers(objectList);
            System.out.println("Number List: " + numberList);
            System.out.println("Object List: " + objectList);
        }
    }

와일드 카드

💡
제네릭 프로그램에서 사용되는 일종의 표현 방법으로,
특정 타입이 아닌
모든 타입을 의미
  • ? 기호를 사용하여 표현
  • 제네릭 타입을 사용하는 메서드나 클래스가 다양한 타입과 호환될 수 있도록 도와준다.
  • <T>처럼 타입을 딱 정한 것과 달리 어떠한 타입이 올 수 있음을 의미
    • <T>→ 주로 읽기와 쓰기, 타입별 연산을 수행할 때 유리
    • <?> → 주로 읽기 작업에 적합
    import java.util.ArrayList;
    import java.util.List;
    
    public class Main {
    
        // 와일드카드 타입의 리스트에 요소를 추가하는 메서드
        public static void addWild(List<?> list){
            // 컴파일 오류 발생! 와일드카드 타입의 리스트에는 어떤 타입의 요소도 추가할 수 없음
           list.add(1);
        }
    
        // 제네릭 메서드: 리스트에 요소를 추가하는 메서드
        public static <T> void addList(List<T> list, T element){
            list.add(element);
        }
    
        public static void main(String[] args) {
            // Integer 타입의 요소를 가진 리스트 초기화
            List<Integer> initList = new ArrayList<>();
            // String 타입의 요소를 가진 리스트 초기화
            List<String> strList = new ArrayList<>();
    
            // 와일드카드 타입의 리스트에 요소 추가 시도
            addWild(initList);  // 컴파일러는 List<?>로 처리
            addWild(strList);   // 컴파일러는 List<?>로 처리
    
            // 제네릭 메서드 호출 시 매개변수 타입을 명시
            addList(initList, 1);
            // Main.<String>addList(strList, "hello");
            addList(strList, "hello");
    
            // 결과 출력
            System.out.println(initList); // [1]
            System.out.println(strList);  // [Hello]
        }
    }
    
  • 종류
    • Unbounded Wildcard(제한 없는 와일드카드)
      • ? 형식
      • 모든 타입이든 다 허용
    // 어떤 타입이든 될 수 있는 객체 리스트
    List<?>
    • Bounded Wildcare(제한 있는 와일드카드) : 2가지 형태 존재
      1. Upper Bounded Wildcard(상한 제한 와일드카드)
        • ? extend Type
        • 지정된 타입 또는 그 하위 타입만 허용
        // Number 또는 Number의 하위 클래스 타입의 객체를 포함하는 리스트
        List<? extends Number>
      1. Lower Bounded Wildcard(하한 제한 와일드카드)
        • ? super Type
        • 지정된 타입 또는 그 상위 타입만 허용
        // Integer 또는 Integer의 상위 클래스 타입의 객체를 포함하는 리스트 
        List<? super Integer>

제네릭 타입 상속

  • 주의할 점
    • 타입 불일치
      • 하위 클래스에서 상위 클래스의 타입 파라미터를 재정의 시, 상위 클래스에서 정의된 메서드와 충돌이 발생하지 않도록 주의
    • 상속과 인터페이스 구현
      • 클래스가 제네릭 인터페이스를 구현하거나, 제네릭 클래스를 상속받을 때, 모든 타입 파라미터를 명시적으로 정의

제네릭 클래스 상속

💡
하위 클래스상위 클래스의 타입 파라미터를 그대로 사용하거나, 새로운 파라미터를 정의


@기본 형태

class Child <T> extends Praent<T>
  • 일반 클래스 상속과 유사하나, 제네릭 타입에 대한 좀 더 복잡한 상속 구문을 다룬다.
  • 제네릭 클래스를 상속할 때는 상속된 클래스의 제네릭 타입 매개변수를 다시 선언할 필요 존재
    // 부모 제네릭 클래스
    class Box<T> {
        private T value;
    
        public Box(T value) {
            this.value = value;
        }
    
        public T getValue() {
            return value;
        }
    }
    
    // 자식 제네릭 클래스
    class BoxChild<T> extends Box<T> {
        // 부모 클래스의 생성자 호출
        public BoxChild(T value) {
            super(value);
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            // 부모 제네릭 클래스의 인스턴스 생성
            Box<Integer> intBox = new Box<>(10);
            System.out.println("Integer value: " + intBox.getValue());
    
            // 자식 제네릭 클래스의 인스턴스 생성
            BoxChild<String> strBox = new BoxChild<>("hello");
            System.out.println("String value: " + strBox.getValue());
        }
    }

제네릭 타입 파라미터의 상속

💡
하위 클래스에서 상위 클래스의 타입 파라미터를 그대로 사용하면, 상위 클래스가 정의한 타입 파라미터에 따라 타입 안정성을 유지 가능
// 부모 클래스인 Box 클래스 정의
class Box<T> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

// 자식 클래스인 StringBox 클래스 정의
// 부모 클래스의 타입 파라미터를 상속받아서 String 타입만을 다루는 Box 클래스를 만듦
class StringBox extends Box<String> {
    public StringBox(String value) {
        super(value);
    }

    // StringBox 클래스에는 추가적인 메서드나 필드를 정의할 수 있음
    public void printUpperCase() {
        System.out.println(getValue().toUpperCase());
    }
}

public class Main {
    public static void main(String[] args) {
        // 부모 클래스인 Box를 이용하여 Integer 타입의 객체를 생성
        Box<Integer> intBox = new Box<>(10);
        System.out.println("Integer value: " + intBox.getValue());

        // 자식 클래스인 StringBox를 이용하여 String 타입의 객체를 생성
        StringBox strBox = new StringBox("hello");
        System.out.println("String value: " + strBox.getValue());
        strBox.printUpperCase(); // 추가적인 메서드 호출
    }
}

제네릭 타입 파라미터 재정의

💡
하위 클래스에서 상위 클래스의 타입 파라미터를 다시 정의하면, 다양한 타입을 처리할 수 있는 클래스 생성 가능


@기본 형태

class Child<T,E> extends Parent<U>
  • 재정의된 제네릭 타입 파라미터를 사용하면, 하위 클래스에서 부모 클래스의 제네릭 타입을 더 구체적으로 특화 가능
    // 부모 제네릭 클래스
    class Box<T> {
        private T value;
    
        public Box(T value) {
            this.value = value;
        }
    
        public T getValue() {
            return value;
        }
    }
    // NumberBox클래스는 Box 클래스를 상속하여 Box 클래스의 제네릭 타입 매개변수를 U로 재정의 
    // 자식 제네릭 클래스
    class NumberBox<U> extends Box<U> {
        // 부모 클래스의 생성자 호출
        public NumberBox(U value) {
            super(value);
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            // 부모 제네릭 클래스의 인스턴스 생성
            Box<Integer> intBox = new Box<>(10);
            System.out.println("Integer value: " + intBox.getValue());
    
            // 자식 제네릭 클래스의 인스턴스 생성
            NumberBox<Double> doubleBox = new NumberBox<>(3.14);
            System.out.println("Double value: " + doubleBox.getValue());
        }
    }
    
728x90

'JAVA' 카테고리의 다른 글

데코레이터 패턴  (1) 2024.04.27
컬렉션 프레임워크  (1) 2024.04.27
java.lang과 java.util 패키지  (0) 2024.04.26
내부 클래스  (0) 2024.04.25
자동 리소드 닫기  (0) 2024.04.25