1. 소개
제네릭 을 지원하는 클래스나 함수의 일부로 배열 을 사용할 수 있습니다. Java가 제네릭을 처리하는 방식 때문에 어려울 수 있습니다.
이 예제에서는 배열과 함께 제네릭을 사용하는 문제를 이해할 것입니다. 그런 다음 일반 배열의 예를 만듭니다.
또한 Java API가 유사한 문제를 어디에서 해결했는지 살펴보겠습니다.
2. 일반 배열 사용 시 고려 사항
배열과 제네릭의 중요한 차이점은 유형 검사를 적용하는 방법입니다. 특히 배열은 런타임에 유형 정보를 저장하고 확인합니다. 그러나 Generics는 컴파일 타임에 유형 오류를 확인 하고 런타임에 유형 정보를 가지고 있지 않습니다.
Java의 구문은 새로운 일반 배열을 생성할 수 있음을 시사합니다.
T[] elements = new T[size];
그러나 이것을 시도하면 컴파일 오류가 발생합니다.
그 이유를 이해하기 위해 다음을 고려해 보겠습니다.
public <T> T[] getArray(int size) {
T[] genericArray = new T[size]; // suppose this is allowed
return genericArray;
}
바인딩되지 않은 제네릭 유형 T 가 Object로 확인되면 런타임 시 메서드는 다음과 같습니다.
public Object[] getArray(int size) {
Object[] genericArray = new Object[size];
return genericArray;
}
그런 다음 메서드를 호출하고 결과를 String 배열 에 저장하면 :
String[] myArray = getArray(5);
코드는 잘 컴파일되지만 ClassCastException 과 함께 런타임에 실패 합니다 . 이는 String[] 참조에 Object[] 를 할당했기 때문 입니다. 특히 컴파일러에 의한 암시적 캐스트는 Object[] 를 필요한 유형 String[] 으로 변환하는 데 실패합니다 .
일반 배열을 직접 초기화할 수는 없지만 호출 코드에서 정확한 유형의 정보를 제공하면 동일한 작업을 수행할 수 있습니다.
3. 일반 배열 생성
우리의 예 에서 용량이 특정 크기로 고정된 제한된 스택 데이터 구조 MyStack 을 고려해 보겠습니다 . 또한 스택이 모든 유형에서 작동하기를 원하므로 합리적인 구현 선택은 일반 배열입니다.
먼저 E 유형의 일반 배열인 스택의 요소를 저장할 필드를 생성해 보겠습니다 .
private E[] elements;
두 번째로 생성자를 추가해 보겠습니다.
public MyStack(Class<E> clazz, int capacity) {
elements = (E[]) Array.newInstance(clazz, capacity);
}
두 개의 매개변수가 필요한 일반 배열을 초기화하기 위해 java.lang.reflect.Array#newInstance 를 사용 하는 방법에 주목하십시오 . 첫 번째 매개변수는 새 배열 내부의 객체 유형을 지정합니다. 두 번째 매개변수는 어레이에 대해 생성할 공간의 양을 지정합니다. Array#newInstance 의 결과는 Object 유형 이므로 일반 배열을 생성 하려면 E[] 로 캐스트해야 합니다 .
또한 Java의 예약어인 클래스가 아닌 유형 매개변수의 이름을 clazz 로 지정하는 규칙에 유의해야 합니다 .
4. ArrayList 고려
4.1. 배열 대신 ArrayList 사용
일반 배열 대신 일반 ArrayList 를 사용하는 것이 더 쉬운 경우가 많습니다 . ArrayList 를 사용하도록 MyStack 을 변경하는 방법을 살펴보겠습니다 .
먼저 요소를 저장할 필드를 만들어 보겠습니다.
private List<E> elements;
둘째, 스택 생성자에서 초기 용량으로 ArrayList 를 초기화할 수 있습니다 .
elements = new ArrayList<>(capacity);
리플렉션을 사용할 필요가 없기 때문에 클래스가 더 단순해집니다. 또한 스택을 생성할 때 클래스 리터럴을 전달할 필요가 없습니다. 마지막으로 ArrayList 의 초기 용량을 설정할 수 있으므로 배열과 동일한 이점을 얻을 수 있습니다.
따라서 드문 상황이나 배열이 필요한 외부 라이브러리와 인터페이스할 때만 제네릭 배열을 구성하면 됩니다.
4.2. ArrayList 구현
흥미롭게도 ArrayList 자체는 일반 배열을 사용하여 구현됩니다. 방법을 보기 위해 ArrayList 내부 를 살펴보겠습니다.
먼저 List 요소 필드를 살펴보겠습니다.
transient Object[] elementData;
공지 사항 의 ArrayList가 사용하는 개체 요소의 형태로. 제네릭 유형은 런타임까지 알려지지 않으므로 Object 는 모든 유형의 수퍼 클래스로 사용됩니다.
ArrayList의 거의 모든 작업은 강력한 형식의 배열을 외부 세계에 제공할 필요가 없기 때문에 이 일반 배열을 사용할 수 있다는 점은 주목할 가치가 있습니다. 단 하나의 메서드인 toArray !
5. 컬렉션에서 배열 만들기
5.1. LinkedList 예제
Java Collections API에서 일반 배열을 사용하는 방법을 살펴보겠습니다. 여기서 컬렉션에서 새 배열을 빌드합니다.
먼저 String 유형 인수를 사용하여 새 LinkedList 를 만들고 여기에 항목을 추가해 보겠습니다 .
List<String> items = new LinkedList();
items.add("first item");
items.add("second item");
둘째, 방금 추가한 항목의 배열을 만들어 보겠습니다.
String[] itemsAsArray = items.toArray(new String[0]);
배열을 빌드하려면 List . toArray 메서드에는 입력 배열이 필요합니다. 이 배열을 순전히 형식 정보를 가져와 올바른 형식의 반환 배열을 만드는 데 사용합니다.
위의 예 에서 결과 String 배열 을 빌드하기 위해 입력 배열로 new String[0] 을 사용했습니다 .
5.2. LinkedList.toArray 구현
LinkedList.toArray 내부 를 살펴보고 Java JDK에서 구현되는 방법을 살펴보겠습니다.
먼저 메서드 서명을 살펴보겠습니다.
public <T> T[] toArray(T[] a)
둘째, 필요할 때 새 배열이 생성되는 방법을 살펴보겠습니다.
a = (T[])java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), size);
앞 의 스택 예제에서와 같이 Array#newInstance 를 사용하여 새 배열을 빌드하는 방법에 주목 하세요. 또한 매개변수 a 가 Array#newInstance에 유형을 제공하는 데 사용되는 방식에 주목하십시오 . 마지막으로 Array#newInstance 의 결과 를 T[] 로 캐스팅하여 일반 배열을 만듭니다.
6. 스트림에서 배열 생성
자바 스트림 API는 우리가 스트림에있는 항목의 배열을 만들 수 있습니다. 올바른 유형의 배열을 생성하기 위해 주의해야 할 몇 가지 함정이 있습니다.
6.1. toArray 사용
Java 8 스트림 의 항목을 배열로 쉽게 변환할 수 있습니다 .
Object[] strings = Stream.of("A", "AAA", "B", "AAB", "C")
.filter(string -> string.startsWith("A"))
.toArray();
assertThat(strings).containsExactly("A", "AAA", "AAB");
그러나 기본 toArray 함수는 String 배열이 아닌 Object 배열을 제공 한다는 점에 유의해야 합니다 .
assertThat(strings).isNotInstanceOf(String[].class);
앞에서 보았듯이 각 배열의 정확한 유형은 다릅니다. Stream 의 형식 은 제네릭이므로 라이브러리 가 런타임에 형식을 유추 할 방법이 없습니다 .
6.2. toArray 오버로드를 사용하여 형식화된 배열 가져오기
공통 컬렉션 클래스 메서드가 리플렉션을 사용하여 특정 유형의 배열을 구성하는 경우 Java Streams 라이브러리는 기능적 접근 방식을 사용합니다. Stream 이 채울 준비가 되었을 때 올바른 크기와 유형의 배열을 생성하는 람다 또는 메서드 참조를 전달할 수 있습니다 .
String[] strings = Stream.of("A", "AAA", "B", "AAB", "C")
.filter(string -> string.startsWith("A"))
.toArray(String[]::new);
assertThat(strings).containsExactly("A", "AAA", "AAB");
assertThat(strings).isInstanceOf(String[].class);
우리가 전달하는 메서드 는 정수를 입력으로 받아 해당 크기의 새 배열을 반환 하는 IntFunction 입니다. 이 정확히 무엇의 생성자 문자열 [] , 그래서 우리는 사용할 수 않는 방법 참조 - [] : 새로운 String .
6.3. 고유한 유형 매개변수가 있는 제네릭
이제 스트림의 값을 List 또는 Optional 같은 유형 매개변수가 있는 객체로 변환한다고 가정해 보겠습니다 . 아마도 Optional<String>[] 을 입력으로 사용 하는 호출하려는 API가 있을 것 입니다.
이러한 종류의 배열을 선언하는 것은 유효합니다.
Optional<String>[] strings = null;
또한 map 메소드 를 사용하여 Stream<String>을 쉽게 가져와 Stream<Optional<String>> 으로 변환할 수 있습니다 .
Stream<Optional<String>> stream = Stream.of("A", "AAA", "B", "AAB", "C")
.filter(string -> string.startsWith("A"))
.map(Optional::of);
그러나 배열을 구성하려고 하면 컴파일러 오류가 다시 발생합니다.
// compiler error
Optional<String>[] strings = new Optional<String>[1];
운 좋게도 이 예제와 이전 예제 사이에는 차이점이 있습니다. 어디 문자열 [] 의 서브 클래스가 아닌 객체는 [] , 선택 사항 [] 사실에 동일한 런타임 타입 옵션 <문자열> [] . 즉, 이것은 유형 캐스팅으로 해결할 수 있는 문제입니다.
Stream<Optional<String>> stream = Stream.of("A", "AAA", "B", "AAB", "C")
.filter(string -> string.startsWith("A"))
.map(Optional::of);
Optional<String>[] strings = stream
.toArray(Optional[]::new);
이 코드는 컴파일되고 작동하지만 확인되지 않은 할당 경고를 제공합니다. 이 문제를 해결하려면 메서드에 SuppressWarnings 를 추가해야 합니다 .
@SuppressWarnings("unchecked")
6.4. 도우미 함수 사용
SuppressWarnings 를 코드의 여러 위치에 추가하는 것을 피하고 일반 배열이 원시 유형에서 생성되는 방식을 문서화하려는 경우 도우미 함수를 작성할 수 있습니다.
@SuppressWarnings("unchecked")
static <T, R extends T> IntFunction<R[]> genericArray(IntFunction<T[]> arrayCreator) {
return size -> (R[]) arrayCreator.apply(size);
}
이 함수는 원시 유형의 배열을 우리가 필요로 하는 특정 유형의 배열을 만들겠다고 약속하는 함수로 함수를 변환합니다.
Optional<String>[] strings = Stream.of("A", "AAA", "B", "AAB", "C")
.filter(string -> string.startsWith("A"))
.map(Optional::of)
.toArray(genericArray(Optional[]::new));
선택되지 않은 할당 경고는 여기에서 억제할 필요가 없습니다.
그러나 이 함수를 호출하여 상위 유형으로 유형 캐스트를 수행할 수 있다는 점에 유의해야 합니다. 예를 들어 스트림에 List<String> 유형의 개체가 포함된 경우 genericArray 를 잘못 호출 하여 ArrayList<String> 배열을 생성 할 수 있습니다 .
ArrayList<String>[] lists = Stream.of(singletonList("A"))
.toArray(genericArray(List[]::new));
이렇게 하면 컴파일되지만 ArrayList[] 가 List[] 의 하위 클래스가 아니므 로 ClassCastException이 발생합니다 . 그러나 컴파일러는 이에 대해 확인되지 않은 할당 경고를 생성하므로 쉽게 발견할 수 있습니다.
7. 결론
이 기사에서는 먼저 배열과 제네릭의 차이점을 살펴보고 제네릭 배열을 만드는 예를 살펴보았습니다. 그런 다음 ArrayList를 사용하는 것이 일반 배열을 사용하는 것보다 더 쉬운 방법을 보여주었습니다 . 또한 Collections API에서 일반 배열의 사용을 살펴보았습니다.
마지막으로 Streams API에서 배열을 생성하는 방법과 유형 매개변수를 사용하는 유형의 배열 생성을 처리하는 방법을 살펴보았습니다.