Frage Wie erstelle ich ein generisches Array in Java?


Aufgrund der Implementierung von Java-Generics können Sie keinen Code wie diesen haben:

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

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

Wie kann ich dies unter Beibehaltung der Typensicherheit umsetzen?

Ich sah eine Lösung in den Java-Foren, die so lautet:

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;
}

Aber ich verstehe wirklich nicht, was vor sich geht.


880
2018-02-09 17:30


Ursprung


Antworten:


Ich muss eine Frage im Gegenzug stellen: ist dein GenSet "checked" oder "unchecked"? Was bedeutet das?

  • Überprüft: starkes Tippen. GenSet weiß explizit, welche Art von Objekten es enthält (d. h. sein Konstruktor wurde explizit mit a aufgerufen Class<E> Argument und Methoden werden eine Ausnahme auslösen, wenn Argumente übergeben werden, die nicht vom Typ sind E. Sehen Collections.checkedCollection.

    -> In diesem Fall sollten Sie schreiben:

    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];
        }
    }
    
  • Deaktiviert: schwaches Tippen. An den als Argument übergebenen Objekten wird keine Typprüfung durchgeführt.

    -> In diesem Fall sollten Sie schreiben

    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;
        }
    }
    

    Beachten Sie, dass der Komponententyp des Arrays der sein sollte Löschen vom Typ Parameter:

    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];
        }
    
        ...
    }
    

All dies resultiert aus einer bekannten und vorsätzlichen Schwäche von Generika in Java: Sie wurde durch Löschen implementiert, so dass "generische" Klassen nicht wissen, mit welchem ​​Typargument sie zur Laufzeit erstellt wurden, und daher keine typisierten Argumente liefern können. Sicherheit, sofern nicht ein expliziter Mechanismus (Typprüfung) implementiert ist.


579
2018-02-09 22:19



Sie können dies immer tun:

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

Dies ist einer der vorgeschlagenen Wege zur Implementierung einer generischen Sammlung in Wirksames Java; Punkt 26. Keine Typfehler, keine Notwendigkeit, das Array wiederholt zu übertragen. jedoch Dies löst eine Warnung aus, weil es potentiell gefährlich ist, und sollte mit Vorsicht verwendet werden. Wie in den Kommentaren ausgeführt, dies Object[] tarnt sich jetzt als unser E[] eingeben und kann unerwartete Fehler verursachen oder ClassCastExceptions bei unsachgemäßer Verwendung.

Als Faustregel gilt, dass dieses Verhalten sicher ist, solange das Cast-Array intern verwendet wird (z. B. um eine Datenstruktur zu unterstützen) und nicht zurückgegeben oder dem Client-Code ausgesetzt wird. Wenn Sie ein Array eines generischen Typs zu anderem Code zurückgeben müssen, die Reflektion Array Klasse, die Sie erwähnen, ist der richtige Weg zu gehen.


Erwähnenswert ist, dass Sie, wo immer es möglich ist, viel angenehmer arbeiten können Lists statt Arrays, wenn Sie Generika verwenden. Sicher haben Sie manchmal keine Wahl, aber die Verwendung des Collections-Frameworks ist weitaus robuster.


160
2018-05-27 20:00



Hier erfahren Sie, wie Sie Generika verwenden, um ein Array genau nach dem Typ zu erhalten, den Sie suchen, während Sie die Typensicherheit beibehalten (im Gegensatz zu den anderen Antworten, die Ihnen entweder eine Antwort geben) Object Array oder führen zu Warnungen zur Kompilierzeit):

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];  
    }  
}

Das kompiliert ohne Warnungen, und wie Sie in sehen können main, für welchen Typ Sie eine Instanz von deklarieren GenSet als, können Sie zuweisen a zu einem Array dieses Typs, und Sie können ein Element zuweisen a zu einer Variablen dieses Typs, was bedeutet, dass das Array und die Werte im Array vom richtigen Typ sind.

Es funktioniert mit Klassenliteralen als Laufzeittyp Token, wie in der Java-Tutorials. Klassenliterale werden vom Compiler als Instanzen von behandelt java.lang.Class. Um eines zu verwenden, folgen Sie einfach dem Namen einer Klasse mit .class. Damit, String.class handelt wie ein Class Objekt, das die Klasse darstellt String. Dies funktioniert auch für Schnittstellen, Enums, beliebige dimensionale Arrays (z. String[].class), Grundelemente (z.B. int.class) und das Schlüsselwort void (d.h. void.class).

Class selbst ist generisch (erklärt als Class<T>, woher T steht für die Art, dass die Class Objekt repräsentiert), was bedeutet, dass der Typ von String.class ist Class<String>.

Also, wann immer du den Konstruktor anrufst GenSetübergeben Sie ein Klassenliteral für das erste Argument, das ein Array des GenSet der deklarierte Typ der Instanz (z.B. String[].class zum GenSet<String>). Beachten Sie, dass Sie kein Array von Primitiven erhalten können, da Primitive nicht für Typvariablen verwendet werden können.

Innerhalb des Konstruktors, Aufruf der Methode cast gibt das übergebene zurück Object Argument, das in die von der Class Objekt, auf dem die Methode aufgerufen wurde. Aufruf der statischen Methode newInstance im java.lang.reflect.Array kehrt als ein zurück Object ein Array des Typs, der von der Class Objekt übergeben als das erste Argument und der von der angegebenen Länge int als zweites Argument übergeben. Die Methode aufrufen getComponentType gibt a zurück Class Objekt, das den Komponententyp des Arrays darstellt, das durch den Class Objekt, auf dem die Methode aufgerufen wurde (z. String.class zum String[].class, null wenn die Class Objekt stellt kein Array dar).

Dieser letzte Satz ist nicht ganz korrekt. Berufung String[].class.getComponentType() gibt a zurück Class Objekt, das die Klasse darstellt String, aber sein Typ ist Class<?>nicht Class<String>Aus diesem Grund können Sie Folgendes nicht tun.

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

Gleiches gilt für jede Methode in Class das gibt a zurück Class Objekt.

Zu Joachim Sauers Kommentar zu diese Antwort (Ich habe nicht genug Reputation, um es selbst zu kommentieren), das Beispiel mit dem Cast zu T[] führt zu einer Warnung, da der Compiler in diesem Fall keine Typsicherheit garantieren kann.


Bearbeiten Sie in Bezug auf Ingo Kommentare:

public static <T> T[] newArray(Class<T[]> type, int size) {
   return type.cast(Array.newInstance(type.getComponentType(), size));
}

53
2017-11-19 03:30



Dies ist die einzige Antwort, die typsicher ist

E[] a;

a = newArray(size);

@SafeVarargs
static <E> E[] newArray(int length, E... array)
{
    return Arrays.copyOf(array, length);
}

34
2017-11-08 15:28



Um zu mehr Dimensionen zu erweitern, fügen Sie einfach hinzu []'s und Dimensionsparameter zu newInstance() (T ist ein Typparameter, cls ist ein Class<T>, d1 durch d5 sind ganze Zahlen):

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);

Sehen Array.newInstance() für Details.


25
2017-08-15 13:47



In Java 8 können wir eine Art generische Array-Erstellung mit einem Lambda oder einer Methodenreferenz durchführen. Dies ist ähnlich dem reflektiven Ansatz (der a Class), aber hier verwenden wir keine Reflexion.

@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);
    }
}

Zum Beispiel wird dies von verwendet <A> A[] Stream.toArray(IntFunction<A[]>).

Dies könnte auch vor-Java 8 mit anonymen Klassen durchgeführt werden, aber es ist mühsamer.


11
2018-03-05 14:14



Dies wird in Kapitel 5 (Generika) von Effektives Java, 2. Ausgabe, Punkt 25 ...Bevorzugt Listen für Arrays

Ihr Code funktioniert, obwohl er eine ungeprüfte Warnung generiert (die Sie mit der folgenden Anmerkung unterdrücken könnten):

@SuppressWarnings({"unchecked"})

Es wäre jedoch wahrscheinlich besser, anstelle eines Arrays eine Liste zu verwenden.

Es gibt eine interessante Diskussion über diesen Bug / Feature auf die OpenJDK-Projektseite.


10
2018-02-09 18:50



Java-Generics funktionieren, indem sie Typen zur Kompilierzeit prüfen und entsprechende Umwandlungen einfügen, aber löschen die Typen in den kompilierten Dateien. Dies macht generische Bibliotheken nutzbar Code, der Generika nicht versteht (was eine bewusste Designentscheidung war), aber das heißt, Sie können in der Regel nicht herausfinden, was der Typ zur Laufzeit ist.

Die Öffentlichkeit Stack(Class<T> clazz,int capacity) Konstruktor erfordert, dass Sie zur Laufzeit ein Klassenobjekt übergeben, was Klasseninformationen bedeutet ist verfügbar zur Laufzeit zu Code, der es braucht. Und das Class<T> Form bedeutet, dass der Compiler prüft, ob die Klasse Objekt, das Sie passieren gerade das Klassenobjekt für Typen T nicht eine Unterklasse von T, kein Super von T, aber genau T.

Das bedeutet dann, dass Sie ein Array-Objekt des entsprechenden Typs in Ihrem Konstruktor erstellen können, was bedeutet, dass die Art der Objekte, die Sie in Ihrer Sammlung speichern muß ihre Typen an der Stelle überprüft sie zu der Sammlung hinzugefügt werden.


7
2018-02-11 10:07



Hallo, obwohl der Thread tot ist, möchte ich deine Aufmerksamkeit darauf lenken:

Generics wird für die Typprüfung während der Kompilierzeit verwendet:

  • Deshalb ist es das Ziel zu überprüfen, dass das, was reinkommt, das ist, was Sie brauchen.
  • Was Sie zurückgeben, ist was der Verbraucher braucht.
  • Überprüfen Sie dies:

enter image description here

Machen Sie sich keine Gedanken über Typumwandlungen von Warnungen, wenn Sie eine generische Klasse schreiben. Machen Sie sich Sorgen, wenn Sie es benutzen.


6
2018-06-14 19:26



Was ist mit dieser Lösung?

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

Es funktioniert und sieht zu einfach um wahr zu sein. Gibt es einen Nachteil?


5
2018-02-21 01:28



Schau auch auf diesen Code:

public static <T> T[] toArray(final List<T> obj) {
    if (obj == null || obj.isEmpty()) {
        return null;
    }
    final T t = obj.get(0);
    final T[] res = (T[]) Array.newInstance(t.getClass(), obj.size());
    for (int i = 0; i < obj.size(); i++) {
        res[i] = obj.get(i);
    }
    return res;
}

Es konvertiert eine Liste von beliebigen Objekten in ein Array desselben Typs.


4
2017-08-08 23:32