본문 바로가기
Java Tutorials

#17 Lesson: Generics 2

by xogns93 2024. 7. 29.

Type Inference

타입 유추는 메서드 호출에 적용할 수 있는 타입 아규먼트(또는 아규먼트)를 결정하기 위해 해당 메소드 호출  해당 메서드 선언을 살펴보는 Java 컴파일러의 기능입니다.

추론 알고리즘은 타입 아규먼트의 타입과 (가능한 경우)리턴되는 타입을 결정합니다.

마지막으로 추론 알고리즘은 모든 아규먼트와 함께 작동하는 특정 타입을 찾으려고 시도합니다.

 

이 마지막 요점을 설명하기 위해 다음 예제에서 타입 추론은 pick 메서드에 전달되는 두 번째 아규먼트가 Serializable 유형임을 확인합니다.

static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());

 

Type Inference and Generic Methods

제네릭 메서드는 꺾쇠 괄호 사이에 타입을 지정하지 않고 일반 메서드처럼 일반 메서드를 호출할 수 있는 타입 유추를 도입했습니다.

List<String> list = new ArrayList<>();
list.add("korea");
list.add("usa");
list.add("england");
list.add("japan");
String[] strs = new String[list.size()];
String[] ret = list.toArray(strs);
// String[] ret = list.<String>toArray(strs);

 

Box 클래스가 필요한 다음 예제 BoxDemo를 고려하십시오.

public class BoxDemo {

  public static <U> void addBox(U u, 
      java.util.List<Box<U>> boxes) {
    Box<U> box = new Box<>();
    box.set(u);
    boxes.add(box);
  }

  public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
    int counter = 0;
    for (Box<U> box: boxes) {
      U boxContents = box.get();
      System.out.println("Box #" + counter + " contains [" +
             boxContents.toString() + "]");
      counter++;
    }
  }

  public static void main(String[] args) {
    java.util.ArrayList<Box<Integer>> listOfIntegerBoxes =
      new java.util.ArrayList<>();
    BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
    BoxDemo.outputBoxes(listOfIntegerBoxes);
  }
}

 

이 예제의 출력은 다음과 같습니다:

Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]

 

제네릭 메소드 addBox는 U라는 하나의 타입 파라미터를 정의합니다.

일반적으로 Java 컴파일러는 제네릭 메소드 호출의 타입 파라미터를 유추할 수 있습니다. 따라서 대부분의 경우 이를 지정할 필요가 없습니다. 예를 들어 제네릭 메서드 addBox를 호출하려면 다음과 같이 타입 감시를 ​​사용(아래처럼, addBox 메서드 이름 앞에 <Integer>)하여 타입 파라미터를 지정할 수 있습니다.

BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);

 

또는 타입 감시를 생략하면 Java 컴파일러가 타입 파라미터가 Integer라고 자동으로 추론합니다(메소드의 아규먼트에서 추론).

BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);

 

 

Type Inference and Instantiation of Generic Classes

컴파일러가 컨텍스트에서 타입 아규먼트를 유추할 수 있는 한 제네릭 클래스의 생성자를 호출하는 데 필요한 타입 아규먼트를 빈 타입 파라미터 세트 (<>)로 바꿀 수 있습니다. 이 꺾쇠 괄호 쌍은 비공식적으로 다이아몬드라고 합니다.

예를 들어 다음 변수 선언을 고려하십시오.

Map<String, List<String>> myMap = new HashMap<String, List<String>>();

 

생성자의 파라미터화된 타입을 텅빈 타입 파라미터 세트(<>)로 대체할 수 있습니다.

Map<String, List<String>> myMap = new HashMap<>();

 

제네릭 클래스 인스턴스화 중에 타입 유추를 활용하려면 다이아몬드를 사용해야 합니다. 다음 예제에서 컴파일러는 HashMap() 생성자가 Map<String, List<String>> 타입이 아닌 HashMap 원시(Raw) 타입을 참조하기 때문에 확인되지 않은[unchecked] 변환 경고를 생성합니다.

Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning

 

Type Inference and Generic Constructors of Generic and Non-Generic Classes

 

생성자는 제네릭 클래스와 제네릭이 아닌 클래스 모두에서, 제네릭(즉, 자신의 formal 타입 파라미터를 선언)  클래스 생성자로 정의할 수 있습니다. 다음 예를 고려하십시오.

class MyClass<X> {
  <T> MyClass(T t) {
    // ...
  }
}

 

MyClass 클래스의 다음 인스턴스화를 고려하십시오.

new MyClass<Integer>("")

 

위 코드는 파라미터화된 타입 MyClass<Integer>의 인스턴스를 작성합니다. 이 코드는 제네릭 클래스 MyClass<X>의 formal 타입 파라미터 X에 대해 Integer 타입을 명시적으로 지정합니다. 이 제네릭 클래스의 생성자에는 formal 타입 파라미터  T가 포함(즉, 제너릭 클래스 생성자)되어 있습니다. 컴파일러는 이 제네릭 클래스 생성자의 formal 타입 파라미터  T에 대해 String 타입을 유추합니다(이 생성자의 실제 타입파라미터는 String 개체이기 때문). 

 

Java SE 7 이전 릴리스의 컴파일러는 제너릭 메소드와 유사하게 제너릭 생성자의 실제 타입 파라미터를 유추할 수 있습니다. 그러나 Java SE 7 이상의 컴파일러는 다이아몬드(<>)를 사용하는 경우 인스턴스화되는 제너릭 클래스의 실제 타입 파라미터를 유추할 수 있습니다. 다음 예를 고려하십시오.

MyClass<Integer> myObject = new MyClass<>("");

 

이 예제에서 컴파일러는 제네릭 클래스 MyClass<X>의 formal 타입 파라미터 X에 대해 Integer 타입을 유추합니다. 또한 컴파일러는 이 제네릭 클래스 생성자의 formal 타입 파라미터 T에 대한 String 타입을 유추합니다.

참고: 유추 알고리즘은 호출 아규먼트, Target type 및 가능하고 명백한 예상 리턴 타입만 사용하여 타입을 유추한다는 점에 유의하는 것이 중요합니다. 추론 알고리즘은 프로그램 후반부의 결과를 사용하지 않습니다.

 

제너릭 클래스 생성자 예제 1

public class MyClass<X> {
    private X x;

    // Generic constructor
    <T> MyClass(T t) {
        System.out.println("Constructor called with argument: " + t);
    }

    // Getter and setter for X
    public X getX() {
        return x;
    }

    public void setX(X x) {
        this.x = x;
    }

    public static void main(String[] args) {
        MyClass<String> myClass1 = new MyClass<>("Hello");
        MyClass<Integer> myClass2 = new MyClass<>(123);

        myClass1.setX("Test String");
        System.out.println("myClass1 x: " + myClass1.getX());

        myClass2.setX(456);
        System.out.println("myClass2 x: " + myClass2.getX());
    }
}

 

제너릭 클래스 생성자 예제 2

public class MyClass<X> {
    private X x;

    // Generic constructor
    <T extends X> MyClass(T t) {
        this.x = t;
        System.out.println("Constructor called with argument: " + t);
    }

    // Getter for X
    public X getX() {
        return x;
    }

    // Main method to test the class
    public static void main(String[] args) {
        MyClass<Number> myClass1 = new MyClass<>(123); // Integer is a subtype of Number
        MyClass<Object> myClass2 = new MyClass<>("Hello"); // String is a subtype of Object

        System.out.println("myClass1 x: " + myClass1.getX());
        System.out.println("myClass2 x: " + myClass2.getX());
    }
}

 

Target Types

 

Java 컴파일러는 제너릭 메소드 호출의 타입 파라미터를 유추하기 위해 타겟 타입(Target Type) 지정을 이용합니다.

expression의 타겟 타입은 expression이 나타나는 위치에 따라 Java 컴파일러가 예상하는 데이터 타입입니다. 다음과 같이 선언된 Collections.emptyList 메소드를 고려하십시오.

static <T> List<T> emptyList();

 

다음 할당 statement를 고려하십시오:

List<String> listOne = Collections.emptyList();

 

위 코드는 List<String>의 인스턴스를 기대하고 있습니다. 이 데이터 타입은 타겟 타입입니다. emptyList 메서드는 List<T> 타입의 값을 반환하기 때문에 컴파일러는 타입 아규먼트 T가 String 값이어야 한다고 추론합니다. 이는 Java SE 7 및 8 모두에서 작동합니다. 또는 타입 감시(.<String>)를 사용하고 다음과 같이 T 값을 지정할 수 있습니다.

List<String> listOne = Collections.<String>emptyList();

 

그러나 이 컨텍스트에서는 타입 감시가 필요하지 않습니다.

하지만 다른 상황에서는 필요하였습니다.

다음 방법을 고려하십시오.

void processStringList(List<String> stringList) {
    // process stringList
}

 

emptyList 메서드 호출로 processStringList 메서드에게 아규먼트를 전달하는 메서드를 호출한다고 가정합니다. Java SE 7에서 다음 명령문은 컴파일되지 않습니다.

processStringList(Collections.emptyList());

 

The Java SE 7 compiler generates an error message similar to the following:

 

List<Object> cannot be converted to List<String>

 

컴파일러는 타입 파라미터 T에 대한 값이 필요하므로 Object 값으로 시작합니다. 따라서 Collections.emptyList를 호출하면 processStringList 메서드와 호환되지 않는 List<Object> 타입의 값이 반환됩니다. 따라서 Java SE 7에서는 다음과 같이 타입 아규먼트의 값을 지정해야 합니다.

processStringList(Collections.<String>emptyList());

 

Java SE 8에서 더 이상 타입 감시가 필요하지 않습니다.

타겟 타입이 무엇인지에 대한 개념이 확장되어 processStringList 메소드에 대한 아규먼트와 같은 메소드 아규먼트를 포함합니다.

이 경우 processStringList에는 List<String> 타입의 아규먼트가 필요합니다. Collections.emptyList 메서드는 List<T>의 값을 반환하므로 List<String>의 타겟 타입을 사용하여 컴파일러는 타입 아규먼트 T의 값이 String인 것으로 유추합니다. 따라서 Java SE 8에서는 다음 코드가 컴파일됩니다.

processStringList(Collections.emptyList());

 

import java.util.Collections;
import java.util.List;

public class TargetTypeExample {

    public static void main(String[] args) {
        // Java SE 7 and 8: Using target type inference
        List<String> listOne = Collections.emptyList();
        System.out.println("listOne: " + listOne);

        // Using type witness in Java SE 7 and 8
        List<String> listTwo = Collections.<String>emptyList();
        System.out.println("listTwo: " + listTwo);

        // Java SE 7: Type witness is required
        processStringList(Collections.<String>emptyList()); // This works in both Java SE 7 and 8

        // Java SE 8: Target type inference allows omitting type witness
        processStringList(Collections.emptyList()); // This works in Java SE 8
    }

    static void processStringList(List<String> stringList) {
        // Process stringList
        System.out.println("Processing stringList: " + stringList);
    }
}

 

자바에서 Target Type은 주로 제네릭과 람다 expression의 문맥에서 사용되는 개념입니다. 
이 개념은 컴파일러가 expression의 타입을 결정하는 데 사용하는 정보를 나타냅니다. 
이해를 돕기 위해 두 주요 사용 사례에 대해 설명하겠습니다.


1. 제네릭과 타입 추론
자바 7 이전까지는 제네릭을 사용할 때, 생성자에 대한 타입 파라미터를 명시적으로 선언해야 했습니다. 
예를 들어, List<String> 타입의 객체를 생성할 때는 new ArrayList<String>()처럼 선언했습니다. 하지만 자바 7 이상에서는 다이아몬드 연산자(<>)를 도입하여, 컴파일러가 문맥을 바탕으로 적절한 타입을 추론할 수 있게 되었습니다.
즉, new ArrayList<>()와 같이 작성하면 컴파일러는 대입 연산자 왼쪽에 명시된 List<String>을 바탕으로 오른쪽의 ArrayList의 타입 파라미터를 String으로 추론합니다. 여기서 List<String>이 Target Type이 됩니다.


2. 람다 Expression
자바 8에서 람다 expression이 도입되면서 Target Type 개념은 더욱 중요해졌습니다.
람다 expression은 그 자체로는 타입이 명확하지 않고, 그것을 할당하는 컨텍스트(예: 변수 타입, 메소드 파라미터)를 통해 타입이 결정됩니다.
예를 들어,
Comparator<String> comp = (s1, s2) -> s1.compareTo(s2);
에서 람다 expression,
(s1, s2) -> s1.compareTo(s2)는 Comparator<String>이라는 Target Type에 의해 결정됩니다.
컴파일러는 이 타입 정보를 사용하여 람다 expression의 파라미터 타입, 리턴 타입, 그리고 적용 가능한 인터페이스를 추론합니다.


요약
Target Type은 컴파일러가 expression의 유형을 추론하는 데 필요한 문맥적 정보를 제공합니다. 
제네릭에서는 다이아몬드 연산자(<>를 사용하여 타입을 자동으로 추론할 수 있게 했고,
람다 expression에서는 할당되는 컨텍스트를 통해 람다의 시그니처를 결정합니다. 이러한 메커니즘은 코드의 간결성과 가독성을 향상시키는 데 크게 기여합니다.
 

'Java Tutorials' 카테고리의 다른 글

#19 Lesson: Generics 4  (1) 2024.08.01
#18 Lesson: Generics 3  (1) 2024.07.29
#16 Lesson: Generics 1  (1) 2024.07.28
#15 Lesson: Numbers and Strings [Autoboxing and Unboxing]  (1) 2024.07.28
#14 Lesson: Numbers and Strings [Strings]  (1) 2024.07.26