คุณจะเลือก List of supertypes เป็น List of subtypes ได้อย่างไร


244

ตัวอย่างเช่นสมมติว่าคุณมีสองคลาส:

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

ฉันมีวิธีที่ผลตอบแทนList<TestA>และฉันต้องการที่จะโยนวัตถุทั้งหมดในรายการที่จะเพื่อที่ฉันจบลงด้วยTestBList<TestB>

คำตอบ:


578

เพียงแค่หล่อเพื่อใช้List<TestB>งานได้เกือบ; แต่มันไม่ทำงานเพราะคุณไม่สามารถส่งพารามิเตอร์หนึ่งประเภททั่วไปไปยังอีกพารามิเตอร์หนึ่งได้ อย่างไรก็ตามคุณสามารถส่งผ่านอักขระตัวแทนระดับกลางและอนุญาต (เนื่องจากคุณสามารถส่งไปยังและจากประเภทสัญลักษณ์แทนได้โดยมีคำเตือนที่ไม่ถูกเลือก):

List<TestB> variable = (List<TestB>)(List<?>) collectionOfListA;

88
ด้วยความเคารพอย่างเต็มที่ฉันคิดว่านี่เป็นวิธีแก้ปัญหาแฮ็ค - คุณกำลังหลบความปลอดภัยประเภท Java ที่พยายามจะให้บริการแก่คุณ อย่างน้อยให้ดูที่บทช่วยสอน Java นี้ ( docs.oracle.com/javase/tutorial/java/generics/subtyping.html ) และพิจารณาว่าทำไมคุณจึงมีปัญหานี้ก่อนที่จะแก้ไขด้วยวิธีนี้
jfritz42

63
@ jfritz42 ฉันจะไม่เรียกมันว่า "หลบ" ประเภทความปลอดภัย ในกรณีนี้คุณมีความรู้ที่ Java ไม่มี นั่นคือสิ่งที่หล่อสำหรับ
Planky

3
สิ่งนี้ให้ฉันเขียน: List <String> myList = (List <String>) (List <?>) (ArrayList ใหม่ <Integer> ()); สิ่งนี้จะล้มเหลวในขณะทำงาน ฉันชอบสิ่งที่ชอบรายการ <? ขยาย Number> myList = new ArrayList <Integer> ();
Bentaye

1
ฉันเห็นด้วยอย่างจริงใจกับ @ jfritz42 ฉันมีปัญหาเดียวกันและดู URL ที่เขาให้ไว้และสามารถแก้ไขปัญหาของฉันได้โดยไม่ต้องส่ง
Sam Orozco

@ jfritz42 นี้ไม่แตกต่างจากการหล่อ Object เป็น Integer ซึ่งคอมไพเลอร์ได้รับอนุญาต
kcazllerraf

116

การส่งข้อมูลทั่วไปไม่สามารถทำได้ แต่ถ้าคุณกำหนดรายการด้วยวิธีอื่นคุณสามารถจัดเก็บ TestB ไว้ในนั้นได้:

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

คุณยังคงมีการตรวจสอบประเภทที่ต้องทำเมื่อคุณใช้วัตถุในรายการ


14
นี่ไม่ใช่แม้แต่ไวยากรณ์ Java ที่ถูกต้อง
newacct

2
นั่นเป็นความจริง แต่มันก็เป็นตัวอย่างที่ถูกต้องมากกว่าความจริงที่ถูกต้อง
Salandur

ฉันมีวิธีการดังต่อไปนี้: createSingleString (รายการ <? ขยายวัตถุ> วัตถุ) ภายในวิธีนี้ฉันเรียก String.valueOf (Object) เพื่อยุบรายการเป็นหนึ่งสาย มันใช้งานได้ดีเมื่ออินพุตคือ List <Long>, List <Boolean> และอื่น ๆ ขอบคุณ!
Chris Sprague

2
เกิดอะไรขึ้นถ้า TestA เป็นอินเทอร์เฟซ
Suvitruf - Andrei Apanasik

8
คุณสามารถให้MCVE กับที่ซึ่งใช้งานได้จริงหรือไม่? Cannot instantiate the type ArrayList<? extends TestA>ฉันได้รับ
Nateowami

42

คุณไม่สามารถ *:

ตัวอย่างนำมาจากบทช่วยสอนเกี่ยวกับ Java นี้

สมมติว่ามีสองประเภทAและดังกล่าวว่าB B extends Aจากนั้นรหัสต่อไปนี้ถูกต้อง:

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

รหัสก่อนหน้านี้จะถูกต้องเพราะBเป็น subclass Aของ ตอนนี้เกิดอะไรขึ้นกับList<A>และList<B> ?

ปรากฎว่าList<B>ไม่ใช่ subclass ของList<A>ดังนั้นเราไม่สามารถเขียน

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

ยิ่งกว่านั้นเราไม่สามารถแม้แต่จะเขียนได้

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

*: เพื่อให้การคัดเลือกเป็นไปได้เราจำเป็นต้องมีผู้ปกครองร่วมกันทั้งคู่List<A>และList<B>: List<?>ตัวอย่างเช่น ต่อไปนี้ถูกต้อง:

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

อย่างไรก็ตามคุณจะได้รับคำเตือน คุณสามารถระงับได้โดยเพิ่ม@SuppressWarnings("unchecked")วิธีการของคุณ


2
นอกจากนี้การใช้เนื้อหาในลิงค์นี้สามารถหลีกเลี่ยงการแปลงที่ไม่ได้ทำเครื่องหมายเนื่องจากคอมไพเลอร์สามารถถอดรหัสการแปลงทั้งหมด
Ryan H

29

ด้วย Java 8 คุณสามารถทำได้

List<TestB> variable = collectionOfListA
    .stream()
    .map(e -> (TestB) e)
    .collect(Collectors.toList());

32
นั่นคือการคัดเลือกนักแสดงหรือไม่? เนื่องจากมันอ่านมากขึ้นเช่นชุดของการดำเนินการที่จะวนซ้ำในรายการทั้งหมดคัดเลือกแต่ละองค์ประกอบแล้วเพิ่มลงในรายการอินสแตนซ์ที่เพิ่งพิมพ์ของประเภทที่ต้องการ อีกวิธีหนึ่งคุณไม่สามารถทำสิ่งนี้กับ Java7- ด้วย a new List<TestB>, loop และ cast?
Patrick M

5
จาก OP "และฉันต้องการที่จะโยนวัตถุทั้งหมดในรายการ" - จริง ๆ แล้วนี่คือหนึ่งในไม่กี่คำตอบที่ตอบคำถามตามที่ระบุไว้แม้ว่าจะไม่ใช่คำถามที่ผู้คนกำลังดูที่นี่
ลุคอัชเชอ

17

ฉันคิดว่าคุณจะหล่อในทิศทางที่ผิด แต่ ... ถ้าวิธีการส่งกลับรายการของวัตถุแล้วจริงๆมันไม่ปลอดภัยที่จะโยนให้แก่TestATestB

โดยทั่วไปคุณกำลังขอให้คอมไพเลอร์ให้คุณดำเนินTestBการกับชนิดTestAที่ไม่สนับสนุน


11

เนื่องจากนี่เป็นคำถามที่อ้างอิงกันอย่างกว้างขวางและคำตอบปัจจุบันส่วนใหญ่อธิบายว่าทำไมมันไม่ทำงาน (หรือเสนอแฮ็ค, การแก้ปัญหาที่เป็นอันตรายที่ฉันไม่เคยต้องการที่จะเห็นในรหัสการผลิต) ฉันคิดว่ามันเหมาะสมที่จะเพิ่มคำตอบอื่น ข้อผิดพลาดและทางออกที่เป็นไปได้


สาเหตุที่ทำให้วิธีนี้ใช้งานไม่ได้มีการระบุไว้แล้วในคำตอบอื่น ๆ : การแปลงที่ถูกต้องจริงหรือไม่ขึ้นอยู่กับประเภทของวัตถุที่มีอยู่ในรายการต้นฉบับ เมื่อมีวัตถุในรายการที่มีประเภทไม่เป็นประเภทTestBแต่เป็นประเภทย่อยที่แตกต่างกันจากTestAนั้นนักแสดงที่ไม่ถูกต้อง


แน่นอนการปลดเปลื้องอาจถูกต้อง บางครั้งคุณมีข้อมูลเกี่ยวกับประเภทที่ไม่พร้อมใช้งานสำหรับคอมไพเลอร์ ในกรณีเหล่านี้เป็นไปได้ที่จะส่งรายการ แต่โดยทั่วไปไม่แนะนำ :

อย่างใดอย่างหนึ่งสามารถ ...

  • ... ส่งรายการทั้งหมดหรือ
  • ... cast องค์ประกอบทั้งหมดของรายการ

ความหมายของวิธีแรก (ซึ่งสอดคล้องกับคำตอบที่ยอมรับในปัจจุบัน) นั้นบอบบาง ดูเหมือนว่ามันจะทำงานได้อย่างถูกต้องตั้งแต่แรกเห็น แต่ถ้ามีประเภทที่ไม่ถูกต้องในรายการอินพุทแล้วClassCastExceptionจะถูกโยนอาจจะอยู่ในตำแหน่งที่แตกต่างอย่างสิ้นเชิงในรหัสและมันอาจเป็นเรื่องยากที่จะแก้จุดบกพร่องนี้และเพื่อหาว่าองค์ประกอบที่ผิดลื่นลงในรายการ ปัญหาที่เลวร้ายที่สุดคือบางคนอาจเพิ่มองค์ประกอบที่ไม่ถูกต้องหลังจากนั้นรายการถูกโยนทำให้การดีบักยากขึ้น

ปัญหาของการแก้จุดบกพร่องเหล่านี้ปลอมClassCastExceptionsสามารถบรรเทากับCollections#checkedCollectionครอบครัวของวิธีการ


กรองรายการตามประเภท

วิธีที่ปลอดภัยกว่าสำหรับการแปลงจาก a เป็นList<Supertype> a List<Subtype>คือการกรองรายการและสร้างรายการใหม่ที่มีเฉพาะองค์ประกอบที่มีประเภทบางอย่างเท่านั้น มีระดับของเสรีภาพในการใช้งานวิธีดังกล่าว (เช่นเกี่ยวกับการรักษาnullข้อมูล) แต่การดำเนินการที่เป็นไปได้อาจมีลักษณะเช่นนี้:

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

วิธีนี้สามารถนำมาใช้เพื่อกรองรายการโดยพลการ (ไม่เพียง แต่มีความสัมพันธ์ Subtype-Supertype ที่กำหนดเกี่ยวกับพารามิเตอร์ประเภท) ดังที่แสดงในตัวอย่างนี้:

// 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]

7

คุณไม่สามารถลงList<TestB>ไปList<TestA>ขณะที่สตีฟ Kuo กล่าวถึง แต่คุณสามารถถ่ายโอนข้อมูลเนื้อหาของเข้าList<TestA> List<TestB>ลองทำสิ่งต่อไปนี้:

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

ฉันไม่ได้ลองใช้รหัสนี้ดังนั้นอาจมีข้อผิดพลาด แต่แนวคิดก็คือควรวนซ้ำข้อมูลวัตถุที่เพิ่มองค์ประกอบ (วัตถุ TestB) ลงในรายการ ฉันหวังว่าจะเหมาะกับคุณ


ทำไมเรื่องนี้ลงคะแนน? มันมีประโยชน์สำหรับฉันและฉันไม่เห็นคำอธิบายสำหรับการลงคะแนนเสียง
โซเด็กซ์

7
แน่นอนโพสต์นี้ไม่ถูกต้อง แต่คนที่ถามคำถามในที่สุดก็ต้องการรายชื่อ TestB ในกรณีนั้นคำตอบนี้ทำให้เข้าใจผิด คุณสามารถเพิ่มองค์ประกอบทั้งหมดในรายการ <TestB> ไปยังรายการ <TestA> โดยเรียกรายการ <TestA> เพิ่มทั้งหมด (รายการ <TestB>) แต่คุณไม่สามารถใช้รายการ <TestB>. เพิ่มทั้งหมด (รายการ <TestA>);
prageeth

6

วิธีที่ปลอดภัยที่สุดคือการนำAbstractListไอเท็มไปใช้และโยนในการนำไปใช้ ฉันสร้างListUtilคลาสผู้ช่วยเหลือ:

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

คุณสามารถใช้castวิธีการในการร่ายวัตถุในรายการและconvertวิธีสำหรับการร่ายอย่างปลอดภัย ตัวอย่าง:

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
}

4

วิธีเดียวที่ฉันรู้คือการคัดลอก:

List<TestB> list = new ArrayList<TestB> (
    Arrays.asList (
        testAList.toArray(new TestB[0])
    )
);

2
ค่อนข้างแปลกฉันคิดว่านี่ไม่ได้ให้คำเตือนผู้แปล
Simon Forsberg

นี่คือสิ่งที่แปลกกับอาร์เรย์ ถอนหายใจอาร์เรย์ java ไม่มีประโยชน์อะไรมากมาย แต่ฉันคิดว่ามันไม่เลวร้ายกว่าและสามารถอ่านได้มากกว่าคำตอบอื่น ๆ ฉันไม่เห็นว่าไม่ปลอดภัยอีกต่อไปกว่านักแสดง
Thufir

3

เมื่อคุณส่งการอ้างอิงวัตถุคุณเพียงแคสต์ชนิดของการอ้างอิงไม่ใช่ประเภทของวัตถุ การคัดเลือกนักแสดงจะไม่เปลี่ยนประเภทของวัตถุจริง

Java ไม่มีกฎโดยนัยสำหรับการแปลงประเภทวัตถุ (ไม่เหมือนดั้งเดิม)

แต่คุณต้องให้วิธีการแปลงประเภทหนึ่งไปเป็นประเภทอื่นและเรียกมันด้วยตนเอง

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

นี่เป็น verbose มากกว่าในภาษาที่มีการสนับสนุนโดยตรง แต่ใช้งานได้และคุณไม่จำเป็นต้องทำสิ่งนี้บ่อยนัก


2

ถ้าคุณมีวัตถุของคลาสที่คุณไม่สามารถโยนมันทิ้งไปTestA TestBทุกคนTestBคือTestAแต่ไม่ใช่วิธีอื่น

ในรหัสต่อไปนี้:

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

บรรทัดที่สองจะขว้าง ClassCastExceptionบรรทัดที่สองจะโยน

คุณสามารถโยนอ้างอิงถ้าวัตถุที่ตัวเองเป็นTestA TestBตัวอย่างเช่น:

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

ดังนั้นคุณอาจไม่หล่อเสมอรายชื่อไปยังรายการของTestATestB


1
ฉันคิดว่าเขากำลังพูดถึงบางสิ่งบางอย่างเช่นคุณได้ 'a' เป็นอาร์กิวเมนต์ของเมธอดซึ่งสร้างเป็น - TestA a = new TestB () จากนั้นคุณต้องการรับวัตถุ TestB แล้วคุณต้องส่งมัน - TestB b = ( TestB)
samsamara

2

คุณสามารถใช้selectInstancesวิธีการในEclipse คอลเลกชัน สิ่งนี้จะเกี่ยวข้องกับการสร้างคอลเลกชันใหม่อย่างไรก็ตามจะไม่มีประสิทธิภาพเท่ากับโซลูชันที่ยอมรับซึ่งใช้การคัดเลือกนักแสดง

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

ฉันรวมไว้StringBufferในตัวอย่างเพื่อแสดงว่าselectInstancesไม่เพียง แต่ลดทอนประเภทเท่านั้น แต่ยังจะกรองว่าคอลเลกชันนั้นมีหลายประเภท

หมายเหตุ: ฉันเป็นคอมมิชชันสำหรับ Eclipse Collections


1

สิ่งนี้เป็นไปได้เนื่องจากการลบประเภท คุณจะพบว่า

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

List<Object>ภายในทั้งสองรายการเป็นประเภท ด้วยเหตุนี้คุณจึงไม่สามารถส่งต่อให้กันและกันได้ - ไม่มีอะไรที่จะส่ง


"ไม่มีอะไรที่จะร่าย" และ "คุณไม่สามารถร่ายหนึ่งไปสู่อีกแบบได้" เป็นความขัดแย้ง Integer j = 42; Integer i = (Integer) j;ทำงานได้ดีทั้งคู่เป็นจำนวนเต็มทั้งคู่มีคลาสเดียวกันดังนั้น "ไม่มีอะไรที่จะส่ง"
Simon Forsberg

1

ปัญหาคือว่าวิธีการของคุณจะไม่ส่งกลับรายการ TestA หากมันมี TestB ดังนั้นถ้ามันถูกพิมพ์อย่างถูกต้อง? จากนั้นนักแสดงนี้:

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

ทำงานได้ดีเท่าที่คุณจะคาดหวังได้ (Eclipse เตือนคุณเกี่ยวกับนักแสดงที่ไม่ถูกตรวจสอบซึ่งเป็นสิ่งที่คุณกำลังทำดังนั้นฉันเลย) ดังนั้นคุณสามารถใช้สิ่งนี้เพื่อแก้ปัญหาของคุณ? ที่จริงแล้วคุณทำได้เพราะสิ่งนี้:

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

สิ่งที่คุณถามใช่ไหม? หรือจะแน่นอน:

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

ดูเหมือนว่าจะรวบรวมได้ดีสำหรับฉัน


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

การทดสอบนี้แสดงให้เห็นถึงวิธีการที่จะโยนไปList<Object> List<MyClass>แต่คุณจะต้องให้ความสนใจในการที่จะต้องมีกรณีของประเภทเดียวกับobjectList MyClassและตัวอย่างนี้สามารถพิจารณาได้เมื่อList<T>มีการใช้งาน สำหรับวัตถุประสงค์นี้ได้รับข้อมูลในตัวสร้างและใช้มันแทนClass<T> clazzMyClass.class


0

สิ่งนี้น่าจะใช้ได้

List<TestA> testAList = new ArrayList<>();
List<TestB> testBList = new ArrayList<>()
testAList.addAll(new ArrayList<>(testBList));

1
ทำให้เป็นสำเนาที่ไม่จำเป็น
defnull

0

ค่อนข้างแปลกที่การส่งรายการด้วยตนเองยังไม่ได้รับการจัดเตรียมโดยกล่องเครื่องมือบางอย่างที่ใช้สิ่งต่อไปนี้:

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

แน่นอนว่าสิ่งนี้จะไม่ตรวจสอบรายการทีละรายการ แต่นั่นคือสิ่งที่เราต้องการหลีกเลี่ยงที่นี่หากเรารู้ดีว่าการใช้งานของเรามีเพียงประเภทย่อยเท่านั้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.