Bagaimana Anda melempar Daftar supertipe ke Daftar subtipe?

244

Misalnya, katakanlah Anda memiliki dua kelas:

public class TestA {}
public class TestB extends TestA{}

Saya memiliki metode yang mengembalikan a List<TestA>dan saya ingin melemparkan semua objek dalam daftar itu TestBsehingga saya berakhir dengan List<TestB>.

Jeremy Logan
sumber

Jawaban:

578

Cukup casting untuk List<TestB>hampir berfungsi; tetapi itu tidak berfungsi karena Anda tidak bisa melemparkan tipe generik dari satu parameter ke yang lain. Namun, Anda dapat menggunakan tipe wildcard perantara dan itu akan diizinkan (karena Anda dapat melakukan cast ke dan dari tipe wildcard, hanya dengan peringatan yang tidak dicentang):

List<TestB> variable = (List<TestB>)(List<?>) collectionOfListA;
berita baru
sumber
88
Dengan segala hormat, saya pikir ini adalah semacam solusi hacky - Anda menghindari jenis keamanan yang coba diberikan Java kepada Anda. Setidaknya lihat tutorial Java ini ( docs.oracle.com/javase/tutorial/java/generics/subtyping.html ) dan pertimbangkan mengapa Anda memiliki masalah ini sebelum memperbaikinya dengan cara ini.
jfritz42
63
@ jfritz42 Saya tidak akan menyebutnya "menghindari" keamanan jenis. Dalam hal ini Anda memiliki pengetahuan yang tidak dimiliki Java. Untuk itulah casting.
Planky
3
Ini memungkinkan saya menulis: Daftar <String> myList = (Daftar <String>) (Daftar <?>) (ArrayList baru <Integer> ()); Ini akan macet saat runtime. Saya lebih suka sesuatu seperti Daftar <? extends Number> myList = ArrayList baru <Integer> ();
Bentaye
1
Jujur saya setuju dengan @ jfritz42 Saya mengalami masalah yang sama dan melihat URL yang dia berikan dan mampu menyelesaikan masalah saya tanpa casting.
Sam Orozco
@ jfritz42 ini tidak secara fungsional berbeda dari melemparkan Obyek ke Integer, yang diizinkan oleh kompiler
kcazllerraf
116

casting generik tidak dimungkinkan, tetapi jika Anda mendefinisikan daftar dengan cara lain, dimungkinkan untuk menyimpan TestB di dalamnya:

List<? extends TestA> myList = new ArrayList<TestA>();

Anda masih harus memeriksa jenis yang harus dilakukan ketika Anda menggunakan objek dalam daftar.

Salandur
sumber
14
Ini bahkan bukan sintaks Java yang valid.
Newacct
2
Itu benar, tetapi itu lebih ilustratif daripada benar secara faktual
Salandur
Saya memiliki metode berikut: createSingleString (Daftar <? Extends Object> objects) Di dalam metode ini saya memanggil String.valueOf (Object) untuk menutup daftar menjadi satu string. Ini berfungsi dengan baik ketika inputnya adalah List <Long>, List <Boolean>, dll. Terima kasih!
Chris Sprague
2
Bagaimana jika TestA adalah antarmuka?
Suvitruf - Andrei Apanasik
8
Bisakah Anda memberi MCVE tempat ini sebenarnya bekerja? Saya mengerti Cannot instantiate the type ArrayList<? extends TestA>.
Nateowami
42

Anda benar-benar tidak dapat *:

Contoh diambil dari tutorial Java ini

Asumsikan ada dua jenis Adan Bsemacamnya B extends A. Maka kode berikut ini benar:

    B b = new B();
    A a = b;

Kode sebelumnya valid karena Bmerupakan subclass dari A. Sekarang, apa yang terjadi dengan List<A>dan List<B>?

Ternyata itu List<B>bukan subklas List<A>karena itu kami tidak bisa menulis

    List<B> b = new ArrayList<>();
    List<A> a = b; // error, List<B> is not of type List<A>

Terlebih lagi, kita bahkan tidak bisa menulis

    List<B> b = new ArrayList<>();
    List<A> a = (List<A>)b; // error, List<B> is not of type List<A>

*: Untuk memungkinkan casting, kami membutuhkan orang tua yang sama untuk keduanya List<A>dan List<B>: List<?>misalnya. Berikut ini valid:

    List<B> b = new ArrayList<>();
    List<?> t = (List<B>)b;
    List<A> a = (List<A>)t;

Namun, Anda akan mendapat peringatan. Anda dapat menekannya dengan menambahkan @SuppressWarnings("unchecked")metode Anda.

Steve Kuo
sumber
2
Selain itu, menggunakan materi dalam tautan ini dapat menghindari konversi yang tidak dicentang, karena kompiler dapat menguraikan seluruh konversi.
Ryan H
29

Dengan Java 8, Anda sebenarnya bisa

List<TestB> variable = collectionOfListA
    .stream()
    .map(e -> (TestB) e)
    .collect(Collectors.toList());
fracz
sumber
32
Apakah itu benar-benar memasukkan daftar? Karena membaca lebih mirip serangkaian operasi untuk beralih ke seluruh daftar, casting setiap elemen, dan kemudian menambahkannya ke daftar yang baru dipakai dari jenis yang diinginkan. Dengan kata lain, tidak bisakah Anda melakukan hal ini dengan Java7- dengan new List<TestB>, loop dan gips?
Patrick M
5
Dari OP "dan saya ingin melemparkan semua objek dalam daftar itu" - jadi sebenarnya, ini adalah salah satu dari sedikit jawaban yang menjawab pertanyaan seperti yang dinyatakan, bahkan jika itu bukan pertanyaan yang dicari orang di sini.
Luke Usherwood
17

Saya pikir Anda melemparkan ke arah yang salah meskipun ... jika metode mengembalikan daftar TestAobjek, maka itu benar-benar tidak aman untuk melemparkannya TestB.

Pada dasarnya Anda meminta kompiler untuk membiarkan Anda melakukan TestBoperasi pada jenis TestAyang tidak mendukungnya.

jerigen
sumber
11

Karena ini adalah pertanyaan yang banyak direferensikan, dan jawaban saat ini terutama menjelaskan mengapa itu tidak bekerja (atau mengusulkan solusi berbahaya yang tidak ingin saya lihat dalam kode produksi), saya pikir pantas menambahkan jawaban lain, menunjukkan perangkap, dan solusi yang mungkin.


Alasan mengapa ini tidak berfungsi secara umum telah ditunjukkan dalam jawaban lain: Apakah konversi benar-benar valid atau tidak tergantung pada jenis objek yang terkandung dalam daftar asli. Ketika ada objek dalam daftar yang jenisnya bukan tipe TestB, tetapi dari subkelas berbeda TestA, maka gips tidak valid.


Tentu saja, gips mungkin valid. Anda terkadang memiliki informasi tentang tipe-tipe yang tidak tersedia untuk kompiler. Dalam kasus ini, dimungkinkan untuk membuat daftar, tetapi secara umum, tidak disarankan :

Orang bisa ...

  • ... cor seluruh daftar atau
  • ... cor semua elemen dari daftar

Implikasi dari pendekatan pertama (yang sesuai dengan jawaban yang diterima saat ini) halus. Tampaknya ini berfungsi dengan baik pada pandangan pertama. Tetapi jika ada jenis yang salah dalam daftar input, maka ClassCastExceptionakan dilempar, mungkin di lokasi yang sama sekali berbeda dalam kode, dan mungkin sulit untuk men-debug ini dan mencari tahu di mana elemen yang salah masuk ke dalam daftar. Masalah terburuk adalah bahwa seseorang bahkan mungkin menambahkan elemen yang tidak valid setelah daftar telah dibuat, mempersulit proses debug.

Masalah debugging palsu ini ClassCastExceptionsdapat diatasi dengan Collections#checkedCollectionkeluarga metode.


Memfilter daftar berdasarkan jenisnya

Cara yang lebih aman dari konversi dari a List<Supertype> ke a List<Subtype>adalah dengan benar-benar memfilter daftar, dan membuat daftar baru yang hanya berisi elemen yang memiliki tipe tertentu. Ada beberapa derajat kebebasan untuk penerapan metode seperti itu (misalnya mengenai perlakuan nullentri), tetapi satu kemungkinan implementasi mungkin terlihat seperti ini:

/**
 * Filter the given list, and create a new list that only contains
 * the elements that are (subtypes) of the class c
 * 
 * @param listA The input list
 * @param c The class to filter for
 * @return The filtered list
 */
private static <T> List<T> filter(List<?> listA, Class<T> c)
{
    List<T> listB = new ArrayList<T>();
    for (Object a : listA)
    {
        if (c.isInstance(a))
        {
            listB.add(c.cast(a));
        }
    }
    return listB;
}

Metode ini dapat digunakan untuk memfilter daftar arbitrer (tidak hanya dengan hubungan Subtype-Supertype yang diberikan mengenai parameter tipe), seperti dalam contoh ini:

// A list of type "List<Number>" that actually 
// contains Integer, Double and Float values
List<Number> mixedNumbers = 
    new ArrayList<Number>(Arrays.asList(12, 3.4, 5.6f, 78));

// Filter the list, and create a list that contains
// only the Integer values:
List<Integer> integers = filter(mixedNumbers, Integer.class);

System.out.println(integers); // Prints [12, 78]
Marco13
sumber
7

Anda tidak dapat melemparkan List<TestB>ke List<TestA>Steve Kuo menyebutkan TETAPI Anda dapat membuang isi List<TestA>ke dalam List<TestB>. Coba yang berikut ini:

List<TestA> result = new List<TestA>();
List<TestB> data = new List<TestB>();
result.addAll(data);

Saya belum mencoba kode ini sehingga mungkin ada kesalahan tetapi idenya adalah harus mengulangi objek data dengan menambahkan elemen (objek TestB) ke dalam Daftar. Saya harap itu berhasil untuk Anda.

martinatime
sumber
mengapa ini turun sebagai Itu berguna bagi saya, dan saya tidak melihat penjelasan untuk suara turun.
zod
7
Tentunya postingan ini tidak salah. Tetapi orang yang mengajukan pertanyaan akhirnya membutuhkan daftar TestB. Dalam hal ini jawaban ini menyesatkan. Anda dapat menambahkan semua elemen dalam Daftar <TestB> ke Daftar <TestA> dengan memanggil Daftar <TestA> .addAll (Daftar <TestB>). Tetapi Anda tidak dapat menggunakan Daftar <TestB> .addAll (Daftar <TestA>);
prageeth
6

Cara aman terbaik adalah menerapkan AbstractListdan melemparkan item dalam implementasi. Saya membuat ListUtilkelas pembantu:

public class ListUtil
{
    public static <TCastTo, TCastFrom extends TCastTo> List<TCastTo> convert(final List<TCastFrom> list)
    {
        return new AbstractList<TCastTo>() {
            @Override
            public TCastTo get(int i)
            {
                return list.get(i);
            }

            @Override
            public int size()
            {
                return list.size();
            }
        };
    }

    public static <TCastTo, TCastFrom> List<TCastTo> cast(final List<TCastFrom> list)
    {
        return new AbstractList<TCastTo>() {
            @Override
            public TCastTo get(int i)
            {
                return (TCastTo)list.get(i);
            }

            @Override
            public int size()
            {
                return list.size();
            }
        };
    }
}

Anda dapat menggunakan castmetode untuk secara buta melemparkan benda-benda dalam daftar dan convertmetode untuk casting yang aman. Contoh:

void test(List<TestA> listA, List<TestB> listB)
{
    List<TestB> castedB = ListUtil.cast(listA); // all items are blindly casted
    List<TestB> convertedB = ListUtil.<TestB, TestA>convert(listA); // wrong cause TestA does not extend TestB
    List<TestA> convertedA = ListUtil.<TestA, TestB>convert(listB); // OK all items are safely casted
}
Igor Zubchenok
sumber
4

Satu-satunya cara saya tahu adalah dengan menyalin:

List<TestB> list = new ArrayList<TestB> (
    Arrays.asList (
        testAList.toArray(new TestB[0])
    )
);
Peter Heusch
sumber
2
Cukup aneh saya pikir ini tidak memberikan peringatan kompiler.
Simon Forsberg
ini adalah keanehan dengan array. Sigh, array java sangat tidak berguna. Tapi, saya pikir itu pasti tidak lebih buruk, dan lebih mudah dibaca, daripada jawaban lain. Saya tidak melihat itu lagi tidak aman daripada para pemain.
Thufir
3

Ketika Anda melemparkan referensi objek Anda hanya casting tipe referensi, bukan tipe objek. casting tidak akan mengubah tipe objek yang sebenarnya.

Java tidak memiliki aturan implisit untuk mengonversi jenis objek. (Tidak seperti primitif)

Alih-alih, Anda perlu menyediakan cara mengonversi satu jenis ke jenis lainnya dan menyebutnya secara manual.

public class TestA {}
public class TestB extends TestA{ 
    TestB(TestA testA) {
        // build a TestB from a TestA
    }
}

List<TestA> result = .... 
List<TestB> data = new List<TestB>();
for(TestA testA : result) {
   data.add(new TestB(testA));
}

Ini lebih verbose daripada dalam bahasa dengan dukungan langsung, tetapi ini berfungsi dan Anda tidak perlu sering melakukannya.

Peter Lawrey
sumber
2

jika Anda memiliki objek kelas TestA, Anda tidak bisa mengubahnya TestB. setiap TestBadalah TestA, tetapi tidak sebaliknya.

dalam kode berikut:

TestA a = new TestA();
TestB b = (TestB) a;

baris kedua akan melempar a ClassCastException.

Anda hanya bisa memberikan TestAreferensi jika objek itu sendiri TestB. sebagai contoh:

TestA a = new TestB();
TestB b = (TestB) a;

jadi, Anda mungkin tidak selalu melemparkan daftar TestAke daftar TestB.

cd1
sumber
1
Saya pikir dia berbicara tentang sesuatu seperti, Anda mendapatkan 'a' sebagai argumen metode yang dibuat sebagai - TestA a = TestB baru (), maka Anda ingin mendapatkan objek TestB dan kemudian Anda perlu melemparkannya - TestB b = ( TestB) a;
samsamara
2

Anda dapat menggunakan selectInstancesmetode ini di Eclipse Collections . Ini akan melibatkan pembuatan koleksi baru namun demikian tidak akan seefisien solusi yang diterima yang menggunakan casting.

List<CharSequence> parent =
        Arrays.asList("1","2","3", new StringBuffer("4"));
List<String> strings =
        Lists.adapt(parent).selectInstancesOf(String.class);
Assert.assertEquals(Arrays.asList("1","2","3"), strings);

Saya termasuk StringBufferdalam contoh untuk menunjukkan bahwa selectInstancestidak hanya menurunkan tipe, tetapi juga akan memfilter jika koleksi berisi tipe campuran.

Catatan: Saya pengendara untuk Eclipse Collections.

Donald Raab
sumber
1

Hal ini dimungkinkan karena penghapusan tipe. Anda akan menemukannya

List<TestA> x = new ArrayList<TestA>();
List<TestB> y = new ArrayList<TestB>();
x.getClass().equals(y.getClass()); // true

Secara internal kedua daftar adalah tipe List<Object>. Untuk alasan itu Anda tidak dapat saling melemparkan - tidak ada yang bisa dilemparkan.

ke-3
sumber
"Tidak ada yang bisa dilemparkan" dan "Anda tidak bisa saling melemparkan" adalah sedikit kontradiksi. Integer j = 42; Integer i = (Integer) j;berfungsi dengan baik, keduanya bilangan bulat, mereka berdua memiliki kelas yang sama, jadi "tidak ada yang dilemparkan".
Simon Forsberg
1

Masalahnya adalah bahwa metode Anda TIDAK mengembalikan daftar TestA jika mengandung TestB, jadi bagaimana jika itu diketik dengan benar? Kemudian pemain ini:

class TestA{};
class TestB extends TestA{};
List<? extends TestA> listA;
List<TestB> listB = (List<TestB>) listA;

bekerja dengan baik dan Anda bisa berharap untuk (Eclipse memperingatkan Anda tentang pemain yang tidak diperiksa yang persis apa yang Anda lakukan, jadi meh). Jadi bisakah Anda menggunakan ini untuk menyelesaikan masalah Anda? Sebenarnya Anda bisa karena ini:

List<TestA> badlist = null; // Actually contains TestBs, as specified
List<? extends TestA> talist = badlist;  // Umm, works
List<TextB> tblist = (List<TestB>)talist; // TADA!

Tepat seperti yang Anda minta, bukan? atau lebih tepatnya:

List<TestB> tblist = (List<TestB>)(List<? extends TestA>) badlist;

tampaknya mengkompilasi baik untuk saya.

Bill K.
sumber
1
class MyClass {
  String field;

  MyClass(String field) {
    this.field = field;
  }
}

@Test
public void testTypeCast() {
  List<Object> objectList = Arrays.asList(new MyClass("1"), new MyClass("2"));

  Class<MyClass> clazz = MyClass.class;
  List<MyClass> myClassList = objectList.stream()
      .map(clazz::cast)
      .collect(Collectors.toList());

  assertEquals(objectList.size(), myClassList.size());
  assertEquals(objectList, myClassList);
}

Tes menunjukkan ini bagaimana dilemparkan List<Object>ke List<MyClass>. Tetapi Anda harus memperhatikan bahwa objectListharus mengandung contoh dari jenis yang sama MyClass. Dan contoh ini dapat dipertimbangkan saat List<T>digunakan. Untuk tujuan ini, dapatkan bidang Class<T> clazzdalam konstruktor dan gunakan sebagai ganti MyClass.class.

Alex Po
sumber
0

Ini seharusnya bekerja

List<TestA> testAList = new ArrayList<>();
List<TestB> testBList = new ArrayList<>()
testAList.addAll(new ArrayList<>(testBList));
rumah anonim
sumber
1
Ini membuat salinan yang tidak perlu.
defnull
0

Cukup aneh bahwa secara manual casting daftar masih belum disediakan oleh beberapa kotak alat yang mengimplementasikan sesuatu seperti:

@SuppressWarnings({ "unchecked", "rawtypes" })
public static <T extends E, E> List<T> cast(List<E> list) {
    return (List) list;
}

Tentu saja, ini tidak akan memeriksa item satu per satu, tetapi justru itulah yang ingin kita hindari di sini, jika kita tahu bahwa implementasi kita hanya menyediakan sub-jenis.

Donatello
sumber