CodeGym /Java Blog /무작위의 /Java Generics: 실제로 각괄호를 사용하는 방법
John Squirrels
레벨 41
San Francisco

Java Generics: 실제로 각괄호를 사용하는 방법

무작위의 그룹에 게시되었습니다

소개

JSE 5.0부터 제네릭이 Java 언어의 무기고에 추가되었습니다.

자바에서 제네릭이란 무엇입니까?

제네릭은 제네릭 프로그래밍을 구현하기 위한 Java의 특수 메커니즘으로, 알고리즘 설명을 변경하지 않고 다양한 데이터 유형으로 작업할 수 있도록 데이터 및 알고리즘을 설명하는 방법입니다. Oracle 웹 사이트에는 제네릭에 대한 별도의 자습서인 " Lesson "이 있습니다. 제네릭을 이해하려면 먼저 제네릭이 왜 필요한지, 무엇을 제공하는지 파악해야 합니다. 튜토리얼의 " 제네릭을 사용하는 이유 " 섹션에서는 두 가지 목적이 컴파일 타임에 더 강력한 유형 검사와 명시적 캐스트의 필요성 제거라고 말합니다. 우리가 사랑하는 TutorialspointJava의 제네릭: 실제로 각괄호를 사용하는 방법 - 1 온라인 자바 컴파일러 에서 몇 가지 테스트를 준비합시다 . 다음 코드가 있다고 가정합니다.

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List list = new ArrayList();
		list.add("Hello");
		String text = list.get(0) + ", world!";
		System.out.print(text);
	}
}
이 코드는 완벽하게 잘 실행될 것입니다. 하지만 상사가 우리에게 와서 "Hello, world! "라고 말하면 어떨까요? 과도하게 사용되는 문구이며 "Hello"만 반환해야 합니까? ", world!" 를 연결하는 코드를 제거하겠습니다. 이것은 충분히 무해한 것 같습니다, 그렇죠? 그러나 실제로 컴파일 시간에 오류가 발생합니다.

error: incompatible types: Object cannot be converted to String
문제는 목록에 개체가 저장되어 있다는 것입니다. String 은 Object 의 후손입니다 (모든 Java 클래스는 암시적으로 Object를 상속하므로 ). 즉, 명시적 캐스트가 필요하지만 추가하지는 않았습니다. 연결 작업 중에 객체를 사용하여 정적 String.valueOf(obj) 메서드가 호출됩니다. 결국 Object 클래스의 toString 메서드를 호출합니다 . 즉, List에는 Object가 포함되어 있습니다 . 즉, 특정 유형( Object 아님 )이 필요할 때마다 유형 변환을 직접 수행해야 합니다.

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List list = new ArrayList();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println("-" + (String)str);
		}
	}
}
그러나 이 경우 List는 객체를 가져오기 때문에 String 뿐만 아니라 Integer 도 저장할 수 있습니다 . 그러나 최악의 점은 컴파일러가 여기서 잘못된 것을 보지 않는다는 것입니다. 이제 AT RUN TIME("런타임 오류"라고 함) 오류가 발생합니다. 오류는 다음과 같습니다.

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
이것이 그다지 좋지 않다는 데 동의해야 합니다. 그리고 이 모든 것은 컴파일러가 프로그래머의 의도를 항상 정확하게 추측할 수 있는 인공 지능이 아니기 때문입니다. Java SE 5는 제네릭을 도입하여 컴파일러에게 우리의 의도, 즉 사용할 유형에 대해 알릴 수 있도록 했습니다. 우리는 컴파일러에게 우리가 원하는 것을 말함으로써 코드를 수정합니다:

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = new ArrayList<>();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println("-" + str);
		}
	}
}
보시다시피 더 이상 String 으로 캐스트할 필요가 없습니다 . 또한 유형 인수를 둘러싼 꺾쇠 괄호가 있습니다. 이제 컴파일러는 목록에 123을 추가하는 행을 제거할 때까지 클래스를 컴파일하도록 허용하지 않습니다. 이는 Integer이기 때문 입니다 . 그리고 그것은 우리에게 그렇게 말할 것입니다. 많은 사람들이 제네릭을 "구문 설탕"이라고 부릅니다. 제네릭이 컴파일된 후에는 실제로 동일한 유형 변환이 되기 때문에 맞습니다. 컴파일된 클래스의 바이트코드를 살펴보겠습니다. 하나는 명시적 캐스트를 사용하고 하나는 제네릭을 사용합니다. Java의 제네릭: 실제로 각괄호를 사용하는 방법 - 2컴파일 후 모든 제네릭은 지워집니다. 이를 " 유형 삭제". 유형 삭제 및 제네릭은 이전 버전의 JDK와 호환되도록 설계되었으며 동시에 컴파일러가 새 버전의 Java에서 유형 정의를 지원할 수 있도록 합니다.

원시 유형

제네릭에 대해 말하자면, 우리는 항상 매개변수화된 유형과 원시 유형이라는 두 가지 범주를 가지고 있습니다. 원시 유형은 꺾쇠 괄호 안에 "유형 설명"을 생략한 유형입니다. Java의 제네릭: 실제로 각괄호를 사용하는 방법 - 3매개변수화된 유형에는 "설명"이 포함되어 있습니다. Java의 제네릭: 실제로 각괄호를 사용하는 방법 - 4보시다시피 스크린샷에서 화살표로 표시된 특이한 구조를 사용했습니다. 이것은 Java SE 7에 추가된 특수 구문입니다. " 다이아몬드 " 라고 합니다 . 왜? 꺾쇠 괄호는 다이아몬드를 형성합니다: <> . 또한 다이아몬드 구문이 " 유형 유추 " 개념과 연관되어 있음을 알아야 합니다 . 결국 컴파일러는 <>를 보고오른쪽에서 값이 할당되는 변수의 유형을 찾는 할당 연산자의 왼쪽을 찾습니다. 이 부분에서 찾은 내용을 기반으로 오른쪽에 있는 값의 유형을 이해합니다. 사실 제네릭 형식이 왼쪽에 지정되고 오른쪽에는 지정되지 않은 경우 컴파일러는 다음과 같이 형식을 유추할 수 있습니다.

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = new ArrayList();
		list.add("Hello, World");
		String data = list.get(0);
		System.out.println(data);
	}
}
그러나 이것은 제네릭이 있는 새 스타일과 제네릭이 없는 이전 스타일을 혼합합니다. 그리고 이것은 매우 바람직하지 않습니다. 위의 코드를 컴파일하면 다음 메시지가 표시됩니다.

Note: HelloWorld.java uses unchecked or unsafe operations
사실 여기에 다이아몬드를 추가해야 하는 이유조차 이해할 수 없는 것 같습니다. 그러나 여기에 예가 있습니다.

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = Arrays.asList("Hello", "World");
		List<Integer> data = new ArrayList(list);
		Integer intNumber = data.get(0);
		System.out.println(data);
	}
}
ArrayList 에는 컬렉션을 인수로 사용하는 두 번째 생성자가 있다는 것을 기억할 것입니다 . 그리고 이곳은 불길한 무언가가 숨겨져 있는 곳입니다. 다이아몬드 구문이 없으면 컴파일러는 그것이 속고 있다는 것을 이해하지 못합니다. 다이아몬드 구문을 사용하면 됩니다. 따라서 규칙 #1은 항상 매개변수화된 유형과 함께 다이아몬드 구문을 사용하는 것입니다. 그렇지 않으면 원시 유형을 사용하는 위치가 누락될 위험이 있습니다. "확인되지 않았거나 안전하지 않은 작업 사용" 경고를 제거하기 위해 메서드 또는 클래스에 @SuppressWarnings("unchecked") 주석을 사용할 수 있습니다 . 그러나 왜 그것을 사용하기로 결정했는지 생각해보십시오. 규칙 1번을 기억하십시오. 형식 인수를 추가해야 할 수도 있습니다.

자바 제네릭 메서드

제네릭을 사용하면 매개 변수 유형과 반환 유형이 매개 변수화된 메서드를 만들 수 있습니다. Oracle 자습서: " Generic Methods "에서 이 기능에 대해 별도의 섹션을 제공합니다. 이 자습서에서 설명하는 구문을 기억하는 것이 중요합니다.
  • 여기에는 꺾쇠 괄호 안에 유형 매개변수 목록이 포함됩니다.
  • 형식 매개변수 목록은 메서드의 반환 형식 앞에 옵니다.
예를 살펴보겠습니다.

import java.util.*;
public class HelloWorld {
	
    public static class Util {
        public static <T> T getValue(Object obj, Class<T> clazz) {
            return (T) obj;
        }
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList("Author", "Book");
		for (Object element : list) {
		    String data = Util.getValue(element, String.class);
		    System.out.println(data);
		    System.out.println(Util.<String>getValue(element));
		}
    }
}
Util 클래스 를 보면 두 가지 일반 메서드가 있음을 알 수 있습니다. 유형 유추의 가능성 덕분에 유형을 컴파일러에 직접 표시하거나 직접 지정할 수 있습니다. 두 옵션 모두 예제에 나와 있습니다. 그건 그렇고, 구문에 대해 생각해 보면 많은 의미가 있습니다. 제네릭 메소드를 선언할 때 메소드 이전에 유형 매개변수를 지정합니다. 메소드 이후에 유형 매개변수를 선언하면 JVM이 사용할 유형을 파악할 수 없기 때문입니다. 따라서 먼저 T 유형 매개변수를 사용할 것이라고 선언 한 다음 이 유형을 반환할 것이라고 말합니다. 당연히 Util.<Integer>getValue(element, String.class)는 오류와 함께 실패합니다.호환되지 않는 유형: Class<String> 은 Class<Integer> 로 변환할 수 없습니다 . 제네릭 메서드를 사용할 때는 항상 유형 삭제를 기억해야 합니다. 예를 살펴보겠습니다.

import java.util.*;
public class HelloWorld {
	
    public static class Util {
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList(2, 3);
		for (Object element : list) {
		    System.out.println(Util.<Integer>getValue(element) + 1);
		}
    }
}
이것은 잘 실행될 것입니다. 그러나 컴파일러가 호출되는 메서드의 반환 유형이 Integer 임을 이해하는 경우에만 가능합니다 . 콘솔 출력 문을 다음 줄로 바꿉니다.

System.out.println(Util.getValue(element) + 1);
오류가 발생합니다.

bad operand types for binary operator '+', first type: Object, second type: int.
즉, 유형 삭제가 발생했습니다. 컴파일러는 아무도 유형을 지정하지 않았음을 확인하므로 유형이 Object 로 표시되고 메서드가 오류와 함께 실패합니다.

제네릭 클래스

메서드만 매개변수화할 수 있는 것은 아닙니다. 수업도 가능합니다. Oracle 튜토리얼의 "Generic Types" 섹션 에서 이에 대해 다룹니다. 예를 들어 보겠습니다.

public static class SomeType<T> {
	public <E> void test(Collection<E> collection) {
		for (E element : collection) {
			System.out.println(element);
		}
	}
	public void test(List<Integer> collection) {
		for (Integer element : collection) {
			System.out.println(element);
		}
	}
}
여기에서는 모든 것이 간단합니다. 제네릭 클래스를 사용하는 경우 클래스 이름 뒤에 type 매개 변수가 표시됩니다. 이제 기본 메서드 에서 이 클래스의 인스턴스를 생성해 보겠습니다 .

public static void main(String []args) {
	SomeType<String> st = new SomeType<>();
	List<String> list = Arrays.asList("test");
	st.test(list);
}
이 코드는 잘 실행될 것입니다. 컴파일러는 List of numbers와 Collection of Strings 가 있음을 확인합니다 . 그러나 유형 매개변수를 제거하고 다음을 수행하면 어떻게 됩니까?

SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
오류가 발생합니다.

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
다시 말하지만 이것은 유형 삭제입니다. 클래스가 더 이상 유형 매개변수를 사용하지 않기 때문에 컴파일러는 우리가 List 를 전달했기 때문에 List <Integer> 가 있는 메서드가 가장 적합하다고 결정합니다. 그리고 우리는 오류로 실패합니다. 따라서 규칙 #2가 있습니다. 일반 클래스가 있는 경우 항상 유형 매개변수를 지정하십시오.

제한

제네릭 메서드 및 클래스에 지정된 유형을 제한할 수 있습니다. 예를 들어, 컨테이너가 유형 인수로 숫자 만 허용하기를 원한다고 가정합니다 . 이 기능은 Oracle 자습서의 제한된 유형 매개변수 섹션 에 설명되어 있습니다 . 예를 살펴보겠습니다.

import java.util.*;
public class HelloWorld {
	
    public static class NumberContainer<T extends Number> {
        private T number;
    
        public NumberContainer(T number) { this.number = number; }
    
        public void print() {
            System.out.println(number);
        }
    }

    public static void main(String []args) {
		NumberContainer number1 = new NumberContainer(2L);
		NumberContainer number2 = new NumberContainer(1);
		NumberContainer number3 = new NumberContainer("f");
    }
}
보시다시피 type 매개변수를 Number 클래스/인터페이스 또는 그 자손으로 제한했습니다. 클래스뿐만 아니라 인터페이스도 지정할 수 있습니다. 예를 들어:

public static class NumberContainer<T extends Number & Comparable> {
제네릭은 와일드카드 도 지원합니다 . 제네릭은 세 가지 유형으로 나뉩니다. 와일드카드 사용은 Get-Put 원칙을 준수해야 합니다 . 다음과 같이 표현할 수 있습니다.
  • 구조에서 값을 가져올 때만 확장 와일드카드를 사용하십시오 .
  • 값을 구조에 넣을 때만 슈퍼 와일드카드를 사용하십시오 .
  • 그리고 구조체에서 가져오거나 구조체에 넣기를 원할 때 와일드카드를 사용하지 마세요.
이 원칙은 PECS(Producer Extends Consumer Super) 원칙이라고도 합니다. 다음은 Java의 Collections.copy 메서드 에 대한 소스 코드의 작은 예입니다 . Java의 제네릭: 실제로 각괄호를 사용하는 방법 - 5작동하지 않는 작은 예는 다음과 같습니다.

public static class TestClass {
	public static void print(List<? extends String> list) {
		list.add("Hello, World!");
		System.out.println(list.get(0));
	}
}

public static void main(String []args) {
	List<String> list = new ArrayList<>();
	TestClass.print(list);
}
그러나 extends 를 super 로 바꾸면 모든 것이 정상입니다. 내용을 표시하기 전에 값으로 목록을 채우므로 이는 소비자 입니다 . 따라서 우리는 슈퍼를 사용합니다.

계승

제네릭에는 상속이라는 또 다른 흥미로운 기능이 있습니다. 제네릭에 대해 상속이 작동하는 방식은 Oracle 자습서의 " Generics, Inheritance, and Subtypes "에 설명되어 있습니다. 중요한 것은 다음을 기억하고 인식하는 것입니다. 우리는 이것을 할 수 없습니다:

List<CharSequence> list1 = new ArrayList<String>();
상속은 제네릭과 다르게 작동하기 때문에 Java의 제네릭: 실제로 각괄호를 사용하는 방법 - 6다음은 오류와 함께 실패하는 또 다른 좋은 예입니다.

List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
다시 말하지만 여기에서는 모든 것이 간단합니다. List<String> 은 List<Object> 의 자손이 아니지만 String Object 의 자손입니다 . 배운 내용을 보강하려면 Java 과정에서 비디오 강의를 시청하는 것이 좋습니다.

결론

그래서 우리는 제네릭에 관한 기억을 되살렸습니다. 그들의 기능을 최대한 활용하는 경우가 거의 없다면 일부 세부 사항이 모호해집니다. 이 짧은 리뷰가 여러분의 기억을 되살리는 데 도움이 되었기를 바랍니다. 더 나은 결과를 얻으려면 다음 자료를 숙지하시기 바랍니다.
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION