[Java] 제너릭

제너릭

래퍼 클래스(Wrapper 클래스)

래퍼 클래스는 기본 타입(int, boolean 등)을 객체로 사용할 수 있도록하는 클래스입니다.

기본 클래스래퍼 클래스
booleanBoolean
byteByte
charCharacter
shortShort
intInteger
longLong
floatFloat
doubleDouble

박싱(boxing)과 언박싱(unboxing)

기본 타입의 값을 래퍼 클래스 객체로 변환하는 것을 박싱, 래퍼 클래스 객체에서 기본 값을 추출하는 것을 언박싱이라고 합니다.

public class Main {
    public static void main(String[] args) {
        int num1 = 10;
        Integer num2 = new Integer(num1); // 박싱
        int num3 = num2.intValue(); // 언박싱
    }
}

자바에서는 명시적으로 박싱, 언박싱을 할 수 있고 자동으로 박싱, 언박싱을 할 수도 있습니다.

public class Main {
    public static void main(String[] args) {
        // 명시적 박싱, 언박싱
        int value = 10;
        Integer num1 = (Integer) value;
        int num2 = (int) num1;

        // 자동 박싱, 언박싱
        Integer num3 = 10;
        int num4 = num1;
    }
}

제너릭(Generic)

제네릭은 타입을 미리 정해놓지 않고 외부에서 필요한 타입을 지정할 수 있는 기능입니다.

선언 방법은 “<>” 괄호와 괄호 안에 타입 파라미터명을 작성하면 됩니다. 타입 파라미터명은 어떠한 문자를 사용해도 되며, 여러 개일 경우 쉼표로 구분하여 작성하면 됩니다. 보통 타입 파리미터명은 아래와 같이 작성합니다.

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

클래스 및 인터페이스에서 선언 시 이름 뒤에 선언하면 됩니다.

접근제어자 class 클래스명 <T> {...}
접근제어자 interface 인터페이스명 <T> {...}
public class Box <T> {
    private T content;

    public void setContent(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }
}

제네릭 클래스를 생성할 때는 실제 타입을 명시해야 합니다. 타입을 명시할 때 주의할 점은 기본 타입은 사용할 수 없습니다.

public class Box <T> {
    private T content;

    public void setContent(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }
}

public class Main {
    public static void main(String[] args) {
        // Box<int> box = new Box<>(); // 에러 발생
        Box<Integer> box = new Box<>();

        box.setContent(10);
        int content = box.getContent();
    }
}

메서드에서 선언 시 리턴타입 앞에 선언합니다. 선언된 타입 파라미터는 리턴타입, 매개변수에서 사용됩니다.

접근제어자 <T> 리턴타입 메서드명(매개변수, ...) {...}
public class Box {
    public Object content;

    public <T> void setContent(T content) {
        this.content = content;
    }
}

public class Main {
    public static void main(String[] args) {
        Box box = new Box();

        box.setContent(new Integer(10));
        System.out.println(box.content); // 10
        box.setContent("안녕");
        System.out.println(box.content); // 안녕
    }
}

제네릭을 사용하면 타입에 대한 유연성과 안정성을 확보할 수 있고, 타입 변환 및 타입 검사에 들어가는 노력을 줄일 수 있습니다.

예로 Box 클래스의 content 필드가 있습니다.

public class Box {
    public Object content;
}

public class Main {
    public static void main(String[] args) {
        Box box = new Box();

        box.content = new Integer(10);
        System.out.println(box.content); // 10

        box.content = new String("안녕");
        System.out.println(box.content); // 안녕
    }
}

content 필드는 Object 타입으로 선언되어 모든 타입의 값을 대입할 수 있습니다. 대입만 한다면 불편한 점은 없지만 content의 값을 추출하거나 특정 메서드를 사용한다면, Object 타입에 어떠한 객체가 대입되었는지 불확실하기 때문에 강제 타입 변환 후 사용해야 하는 번거로움이 있습니다.

public class Main {
    public static void main(String[] args) {
        Box box = new Box();

        box.content = new String("안녕");
//        String content2 = box.content; // 에러 발생
        String content2 = (String) box.content; // String 타입으로 값을 대입하기 위해선 강제 타입 변환 필요
        String[] strings = ((String) box.content).split(""); // String 클래스에 정의된 메서드를 사용하기 위해선 강제 타입 변환 필요
    }
}

제네릭을 사용하면 강제 타입 변환하는 노력을 줄일 수 있습니다.

public class Box <T> {
    public T content;
}

public class Main {
    public static void main(String[] args) {
        Box<String> box = new Box();

        box.content = new String("안녕");
        String content = box.content;
        String[] strings = box.content.split("");
    }
}

와일드카드

와일드카드(<?>)란 모든 타입으로 대체할 수 있는 타입입니다.

public class Main {
    public static void printList(List<?> list) {
        for (Object element : list) {
            System.out.println(element);
        }
    }

    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3, 4, 5);
        List<String> stringList = List.of("사과", "바나나", "오렌지");

        printList(intList);
        printList(stringList);
    }
}
// 실행 결과
1
2
3
4
5
사과
바나나
오렌지

와일드카드의 상한과 하한

타입 파라미터의 구체적인 타입을 제한할 필요가 있을 때 제한된 타입 파라미터(Bounded Type Parameter)를 사용합니다.

상한 제한된 와일드카드(Upper Bounded Wildcards)

상한 제한된 와일드카드를 사용하면 대입할 수 있는 타입을 특정 타입 또는 특정 하위 타입으로 제한할 수 있습니다.

public <? extends 특정타입> 리턴타입 메서드명(매개변수, ...) {...}

예로 어떤 리스트의 요소를 출력하는 메서드를 만드는데, 이 메서드의 매개변수 타입을 Number 또는 하위 클래스(Byte, Short, Integer, Long, Double)로 제한한다면 아래와 같이 작성하면 됩니다.

import java.util.List;

public class Main {
    public static void printList(List<? extends Number> list) {
        for (Number n : list) {
            System.out.println(n);
        }
    }

    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3);
        List<Double> doubleList = List.of(1.0, 2.0, 3.0);
        List<String> stringList = List.of("사과", "바나나", "오렌지");

        printList(intList);
        printList(doubleList);
        printList(stringList); // String은 Number를 상속하지 않기 때문에 에러 발생
    }
}

하한 제한된 와일드카드(Lower Bounded Wildcards)

하한 제환된 와일드 카드는 필요한 타입이 특정 타입과 같거나 특정 타입의 상위 타입일 때 사용합니다.

public <? super 특정타입> 리턴타입 메서드명(매개변수, ...) {...}
public void processList(List<? super Integer> list) {

    list.add(new Integer(20));
    list.add(new Float(2.0f)); // Float 타입은 Integer 타입 또는 Integer 상위 타입이 아니여서 에러 발생

}


© 2023.06. by 고개

Powered by 고개