Pertanyaan Bagaimana cara membuat array generik di Java?


Karena penerapan Java generics, Anda tidak dapat memiliki kode seperti ini:

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

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

Bagaimana saya bisa menerapkan ini sambil menjaga keamanan tipe?

Saya melihat solusi di forum Java yang berjalan seperti ini:

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

Tapi aku benar-benar tidak mengerti apa yang terjadi.


880
2018-02-09 17:30


asal


Jawaban:


Saya harus mengajukan pertanyaan sebagai balasan: adalah Anda GenSet "dicentang" atau "tidak dicentang"? Apa artinya?

  • Diperiksa: mengetik kuat. GenSet tahu secara eksplisit apa jenis objek yang dikandungnya (yaitu konstruktornya secara eksplisit disebut dengan Class<E> argumen, dan metode akan melemparkan pengecualian ketika mereka melewati argumen yang bukan tipe E. Lihat Collections.checkedCollection.

    -> dalam hal ini, Anda harus menulis:

    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];
        }
    }
    
  • Tidak dicentang: mengetik lemah. Tidak ada pengecekan tipe yang benar-benar dilakukan pada objek apa pun yang dilewatkan sebagai argumen.

    -> dalam hal ini, Anda harus menulis

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

    Perhatikan bahwa jenis komponen dari array harus menjadi penghapusan dari parameter jenis:

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

Semua ini hasil dari kelemahan yang diketahui, dan disengaja, generik di Jawa: itu diimplementasikan menggunakan penghapusan, jadi "generik" kelas tidak tahu argumen jenis apa yang mereka buat dengan pada waktu berjalan, dan karena itu tidak dapat memberikan jenis- keamanan kecuali beberapa mekanisme eksplisit (tipe-pengecekan) diimplementasikan.


579
2018-02-09 22:19



Anda selalu dapat melakukan ini:

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

Ini adalah salah satu cara yang disarankan untuk menerapkan koleksi generik di Java yang efektif; Item 26. Tidak ada kesalahan ketik, tidak perlu melontarkan larik berulang kali. Namun ini memicu peringatan karena berpotensi berbahaya, dan harus digunakan dengan hati-hati. Seperti yang tertera di komentar, ini Object[] sekarang menyamar sebagai kami E[] ketik, dan dapat menyebabkan kesalahan yang tidak terduga atau ClassCastExceptions jika digunakan dengan tidak aman.

Sebagai aturan praktis, perilaku ini aman selama array cor digunakan secara internal (misalnya untuk mendukung struktur data), dan tidak dikembalikan atau diekspos ke kode klien. Jika Anda perlu mengembalikan array dari tipe generik ke kode lain, refleksi Array kelas yang Anda sebutkan adalah cara yang tepat untuk pergi.


Layak disebutkan bahwa sedapat mungkin, Anda akan memiliki waktu yang lebih bahagia bekerja dengan Listbukan array jika Anda menggunakan generik. Tentu saja terkadang Anda tidak punya pilihan, tetapi menggunakan kerangka koleksi jauh lebih kuat.


160
2018-05-27 20:00



Berikut ini cara menggunakan generik untuk mendapatkan rangkaian jenis yang tepat yang Anda cari sambil mempertahankan keamanan jenis (sebagai lawan dari jawaban lainnya, yang akan memberikan Anda kembali Object larik atau hasilkan peringatan pada saat kompilasi):

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

Yang mengkompilasi tanpa peringatan, dan seperti yang Anda lihat main, untuk jenis apa pun yang Anda nyatakan sebagai contohnya GenSet seperti, Anda dapat menetapkan a ke larik jenis itu, dan Anda dapat menetapkan elemen dari a ke variabel jenis itu, yang berarti bahwa array dan nilai-nilai dalam array adalah dari jenis yang benar.

Ia bekerja dengan menggunakan literal kelas sebagai token tipe runtime, seperti yang dibahas dalam Tutorial Java. Kelas literal diperlakukan oleh kompilator sebagai contoh java.lang.Class. Untuk menggunakannya, cukup ikuti nama kelas dengan .class. Begitu, String.class bertindak sebagai Class objek yang mewakili kelas String. Ini juga berfungsi untuk antarmuka, enum, susunan dimensi (mis. String[].class), primitif (misalnya int.class), dan kata kunci void (yaitu. void.class).

Class sendiri adalah generik (dideklarasikan sebagai Class<T>, dimana T singkatan dari tipe yang Class objek mewakili), yang berarti bahwa jenis String.class aku s Class<String>.

Jadi, setiap kali Anda memanggil konstruktor untuk GenSet, Anda lulus dalam kelas literal untuk argumen pertama yang mewakili suatu array GenSet contoh tipe yang dideklarasikan (mis. String[].class untuk GenSet<String>). Perhatikan bahwa Anda tidak akan bisa mendapatkan array primitif, karena primitif tidak dapat digunakan untuk variabel jenis.

Di dalam konstruktor, memanggil metode cast mengembalikan berlalu Object argumen dilemparkan ke kelas yang diwakili oleh Class objek di mana metode itu disebut. Memanggil metode statis newInstance di java.lang.reflect.Array kembali sebagai Object larik jenis yang diwakili oleh Class objek lulus sebagai argumen pertama dan panjang yang ditentukan oleh int disahkan sebagai argumen kedua. Memanggil metode getComponentType mengembalikan a Class objek yang mewakili jenis komponen larik yang diwakili oleh Class objek di mana metode itu dipanggil (mis. String.class untuk String[].class, null jika Class objek tidak mewakili larik).

Kalimat terakhir itu tidak sepenuhnya akurat. Panggilan String[].class.getComponentType() mengembalikan a Class objek yang mewakili kelas String, tetapi tipenya adalah Class<?>tidak Class<String>, itulah mengapa Anda tidak dapat melakukan hal seperti berikut ini.

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

Sama berlaku untuk setiap metode di Class yang mengembalikan a Class obyek.

Mengenai komentar Joachim Sauer tentang jawaban ini (Saya tidak memiliki reputasi yang cukup untuk mengomentari itu sendiri), contoh menggunakan pemain untuk T[] akan menghasilkan peringatan karena compiler tidak dapat menjamin keamanan tipe dalam kasus itu.


Sunting terkait komentar Ingo:

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

53
2017-11-19 03:30



Ini adalah satu-satunya jawaban yang aman

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



Untuk memperluas ke dimensi lain, cukup tambahkan []parameter dan dimensi untuk newInstance() (T adalah parameter tipe, cls adalah Class<T>, d1 melalui d5 adalah bilangan bulat):

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

Lihat Array.newInstance() untuk detailnya.


25
2017-08-15 13:47



Di Java 8, kita bisa melakukan semacam pembuatan array generik menggunakan lambda atau referensi metode. Ini mirip dengan pendekatan reflektif (yang melewati a Class), tetapi di sini kita tidak menggunakan refleksi.

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

Misalnya, ini digunakan oleh <A> A[] Stream.toArray(IntFunction<A[]>).

Ini bisa juga dilakukan pra-Java 8 menggunakan kelas anonim tetapi lebih rumit.


11
2018-03-05 14:14



Ini tercakup dalam Bab 5 (Generik) dari Java efektif, 2nd Edition, butir 25 ...Lebih suka daftar ke array

Kode Anda akan berfungsi, meskipun akan menghasilkan peringatan yang tidak dicentang (yang dapat Anda tekan dengan anotasi berikut:

@SuppressWarnings({"unchecked"})

Namun, mungkin akan lebih baik menggunakan Daftar daripada Array.

Ada diskusi menarik tentang bug / fitur ini situs proyek OpenJDK.


10
2018-02-09 18:50