Как создать универсальный массив в Java? - PullRequest
994 голосов
/ 09 февраля 2009

Из-за реализации обобщений Java вы не можете иметь такой код:

public class GenSet<E> {
    private E a[];

    public GenSet() {
        a = new E[INITIAL_ARRAY_LENGTH]; // error: generic array creation
    }
}

Как я могу реализовать это, поддерживая безопасность типов?

Я видел решение на форумах Java, которое выглядит следующим образом:

import java.lang.reflect.Array;

class Stack<T> {
    public Stack(Class<T> clazz, int capacity) {
        array = (T[])Array.newInstance(clazz, capacity);
    }

    private final T[] array;
}

Но я действительно не понимаю, что происходит.

Ответы [ 29 ]

651 голосов
/ 10 февраля 2009

Я должен задать вопрос в ответ: ваш GenSet "проверен" или "не проверен"? Что это значит?

  • Проверено : Строгая печать . GenSet явно знает, какой тип объектов он содержит (то есть его конструктор был явно вызван с аргументом Class<E>, и методы будут генерировать исключение, когда им передаются аргументы, не относящиеся к типу E. См. Collections.checkedCollection.

    -> в этом случае вы должны написать:

    public class GenSet<E> {
    
        private E[] a;
    
        public GenSet(Class<E> c, int s) {
            // Use Array native method to create array
            // of a type only known at run time
            @SuppressWarnings("unchecked")
            final E[] a = (E[]) Array.newInstance(c, s);
            this.a = a;
        }
    
        E get(int i) {
            return a[i];
        }
    }
    
  • Не проверено : слабая печать . Проверка типов на самом деле не выполняется ни для одного из объектов, переданных в качестве аргумента.

    -> в этом случае вы должны написать

    public class GenSet<E> {
    
        private Object[] a;
    
        public GenSet(int s) {
            a = new Object[s];
        }
    
        E get(int i) {
            @SuppressWarnings("unchecked")
            final E e = (E) a[i];
            return e;
        }
    }
    

    Обратите внимание, что тип компонента массива должен быть стирание параметра типа:

    public class GenSet<E extends Foo> { // E has an upper bound of Foo
    
        private Foo[] a; // E erases to Foo, so use Foo[]
    
        public GenSet(int s) {
            a = new Foo[s];
        }
    
        ...
    }
    

Все это является следствием известной и преднамеренной слабости обобщений в Java: он был реализован с использованием стирания, поэтому «универсальные» классы не знают, с каким аргументом типа они были созданы во время выполнения, и поэтому не могут обеспечить безопасность типов, если не реализован какой-либо явный механизм (проверка типов).

184 голосов
/ 28 мая 2010

Вы можете сделать это:

E[] arr = (E[])new Object[INITIAL_ARRAY_LENGTH];

Это один из предложенных способов реализации универсальной коллекции в Effective Java; Элемент 26 . Нет ошибок типа, нет необходимости повторно приводить массив. Однако это вызывает предупреждение, потому что это потенциально опасно, и его следует использовать с осторожностью. Как подробно описано в комментариях, этот Object[] теперь маскируется под наш E[] тип и может вызывать неожиданные ошибки или ClassCastException s, если используется небезопасно.

Как правило, такое поведение безопасно, если массив приведения используется внутренне (например, для поддержки структуры данных), а не возвращается или не подвергается воздействию клиентского кода. Если вам нужно вернуть массив универсального типа в другой код, упомянутый вами класс отражения Array - верный путь.


Стоит отметить, что везде, где это возможно, вам будет гораздо приятнее работать с List s, а не с массивами, если вы используете обобщенные элементы. Конечно, иногда у вас нет выбора, но каркас коллекций гораздо надежнее.

60 голосов
/ 19 ноября 2010

Вот как использовать обобщенные элементы для получения массива именно того типа, который вы ищете, при сохранении безопасности типов (в отличие от других ответов, которые либо вернут вам массив Object, либо приведут к предупреждениям при компиляции время):

import java.lang.reflect.Array;  

public class GenSet<E> {  
    private E[] a;  

    public GenSet(Class<E[]> clazz, int length) {  
        a = clazz.cast(Array.newInstance(clazz.getComponentType(), length));  
    }  

    public static void main(String[] args) {  
        GenSet<String> foo = new GenSet<String>(String[].class, 1);  
        String[] bar = foo.a;  
        foo.a[0] = "xyzzy";  
        String baz = foo.a[0];  
    }  
}

Это компилируется без предупреждений, и, как вы можете видеть в main, для любого типа вы объявляете экземпляр GenSet как, вы можете присвоить a массиву этого типа, и вы можете назначить элемент от a до переменной этого типа, что означает, что массив и значения в массиве имеют правильный тип.

Он работает с использованием литералов класса в качестве токенов типа времени выполнения, как описано в Java Tutorials . Литералы класса обрабатываются компилятором как экземпляры java.lang.Class. Чтобы использовать один, просто следуйте за именем класса с .class. Так, String.class действует как Class объект, представляющий класс String. Это также работает для интерфейсов, перечислений, любых размерных массивов (например, String[].class), примитивов (например, int.class) и ключевого слова void (то есть void.class).

Class сам по себе является общим (объявлен как Class<T>, где T обозначает тип, который представляет объект Class), что означает, что тип String.class равен Class<String>.

Таким образом, всякий раз, когда вы вызываете конструктор для GenSet, вы передаете литерал класса для первого аргумента, представляющего массив объявленного типа экземпляра GenSet (например, String[].class для GenSet<String>). Обратите внимание, что вы не сможете получить массив примитивов, поскольку примитивы нельзя использовать для переменных типа.

Внутри конструктора вызов метода cast возвращает переданный Object аргумент, приведенный к классу, представленному объектом Class, для которого был вызван метод. Вызов статического метода newInstance в java.lang.reflect.Array возвращает в виде Object массив типа, представленного объектом Class, переданным в качестве первого аргумента, и длиной, указанной в int, переданной в качестве второго аргумента. Вызов метода getComponentType возвращает объект Class, представляющий тип компонента массива, представленного объектом Class, для которого был вызван метод (например, String.class для String[].class, null, если Class объект не представляет массив).

Это последнее предложение не совсем точно. Вызов String[].class.getComponentType() возвращает объект Class, представляющий класс String, но его типом является Class<?>, а не Class<String>, поэтому вы не можете сделать что-то вроде следующего.

String foo = String[].class.getComponentType().cast("bar"); // won't compile

То же самое относится к каждому методу в Class, который возвращает объект Class.

Что касается комментария Йоахима Сауэра к этого ответа (у меня недостаточно репутации, чтобы прокомментировать его сам), пример использования приведения к T[] приведет к предупреждению, потому что компилятор может ' в этом случае гарантируется безопасность типов.


Редактировать комментарии Инго:

public static <T> T[] newArray(Class<T[]> type, int size) {
   return type.cast(Array.newInstance(type.getComponentType(), size));
}
38 голосов
/ 08 ноября 2011

Это единственный ответ типа safe

E[] a;

a = newArray(size);

@SafeVarargs
static <E> E[] newArray(int length, E... array)
{
    return Arrays.copyOf(array, length);
}
29 голосов
/ 15 августа 2013

Чтобы расширить число измерений, просто добавьте [] и параметры измерения к newInstance() (T - это параметр типа, cls - это Class<T>, d1 - d5 - целые числа ):

T[] array = (T[])Array.newInstance(cls, d1);
T[][] array = (T[][])Array.newInstance(cls, d1, d2);
T[][][] array = (T[][][])Array.newInstance(cls, d1, d2, d3);
T[][][][] array = (T[][][][])Array.newInstance(cls, d1, d2, d3, d4);
T[][][][][] array = (T[][][][][])Array.newInstance(cls, d1, d2, d3, d4, d5);

Подробнее см. Array.newInstance().

12 голосов
/ 05 марта 2014

В Java 8 мы можем сделать типовое создание массива, используя лямбду или ссылку на метод. Это похоже на рефлексивный подход (который проходит Class), но здесь мы не используем рефлексию.

@FunctionalInterface
interface ArraySupplier<E> {
    E[] get(int length);
}

class GenericSet<E> {
    private final ArraySupplier<E> supplier;
    private E[] array;

    GenericSet(ArraySupplier<E> supplier) {
        this.supplier = supplier;
        this.array    = supplier.get(10);
    }

    public static void main(String[] args) {
        GenericSet<String> ofString =
            new GenericSet<>(String[]::new);
        GenericSet<Double> ofDouble =
            new GenericSet<>(Double[]::new);
    }
}

Например, это используется <A> A[] Stream.toArray(IntFunction<A[]>).

Это может также быть сделано до Java 8 с использованием анонимных классов, но это более громоздко.

10 голосов
/ 09 февраля 2009

Это описано в главе 5 (Общие положения) Effective Java, 2nd Edition , элемент 25 ... Предпочитать списки массивам

Ваш код будет работать, хотя он будет генерировать непроверенное предупреждение (которое вы можете отключить с помощью следующей аннотации:

@SuppressWarnings({"unchecked"})

Однако, вероятно, было бы лучше использовать List вместо Array.

Существует интересное обсуждение этой ошибки / функции на сайте проекта OpenJDK .

7 голосов
/ 11 февраля 2009

Обобщения Java работают, проверяя типы во время компиляции и вставляя соответствующие преобразования, но стирает типы в скомпилированных файлах. Это делает универсальные библиотеки пригодными для использования кодом, который не понимает универсальные (что было преднамеренным дизайнерским решением), но это означает, что вы обычно не можете узнать, какой тип во время выполнения.

Публичный конструктор Stack(Class<T> clazz,int capacity) требует, чтобы вы передавали объект Class во время выполнения, что означает, что информация о классе доступна во время выполнения для кода, который в ней нуждается. И форма Class<T> означает, что компилятор проверит, что передаваемый вами объект Class является именно объектом Class для типа T. Не подклассом T, не суперклассом T, но именно T.

Это означает, что вы можете создать в своем конструкторе объект массива соответствующего типа, что означает, что типам объектов, которые вы храните в своей коллекции, будут проверяться их типы в тот момент, когда они добавляются в коллекцию.

6 голосов
/ 14 июня 2011

Привет, хотя нить мертва, я хотел бы обратить ваше внимание на это:

Обобщения используются для проверки типов во время компиляции:

  • Поэтому цель состоит в том, чтобы проверить, что вам нужно.
  • То, что вы возвращаете, - это то, что нужно потребителю.
  • Проверьте это:

enter image description here

Не беспокойтесь о предупреждениях при вводе типов при написании универсального класса. Беспокойство, когда вы используете его.

5 голосов
/ 21 февраля 2016

А как насчет этого решения?

@SafeVarargs
public static <T> T[] toGenericArray(T ... elems) {
    return elems;
}

Это работает и выглядит слишком просто, чтобы быть правдой. Есть ли недостаток?

...