Làm thế nào để bạn đưa một danh sách các siêu kiểu vào một danh sách các kiểu con?


244

Ví dụ: giả sử bạn có hai lớp:

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

Tôi có một phương thức trả về a List<TestA>và tôi muốn truyền tất cả các đối tượng trong danh sách TestBđó để tôi kết thúc bằng a List<TestB>.

Câu trả lời:


578

Đơn giản chỉ cần đúc để List<TestB>gần như làm việc; nhưng nó không hoạt động vì bạn không thể truyền loại chung của một tham số này sang tham số khác. Tuy nhiên, bạn có thể truyền qua một loại ký tự đại diện trung gian và nó sẽ được cho phép (vì bạn có thể truyền đến và từ các loại ký tự đại diện, chỉ với một cảnh báo không được kiểm tra):

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

88
Với tất cả sự tôn trọng, tôi nghĩ rằng đây là một giải pháp hacky - bạn đang né tránh loại an toàn mà Java đang cố gắng cung cấp cho bạn. Ít nhất hãy xem hướng dẫn Java này ( docs.oracle.com/javase/tutorial/java/generics/subtyping.html ) và xem xét lý do tại sao bạn gặp vấn đề này trước khi khắc phục theo cách này.
jfritz42

63
@ jfritz42 Tôi sẽ không gọi nó là "né tránh" loại an toàn. Trong trường hợp này, bạn có kiến ​​thức mà Java không có. Đó là những gì đúc là dành cho.
Planky

3
Điều này cho phép tôi viết: List <String> myList = (List <String>) (List <?>) (ArrayList mới <Integer> ()); Điều này sẽ sụp đổ khi chạy. Tôi thích cái gì đó như Danh sách <? mở rộng Số> myList = new ArrayList <Integer> ();
Bentaye

1
Tôi thành thật đồng ý với @ jfritz42 Tôi cũng gặp vấn đề tương tự và đã xem URL mà anh ấy cung cấp và có thể giải quyết vấn đề của tôi mà không cần truyền.
Sam Orozco

@ jfritz42 điều này không khác về chức năng so với việc truyền Object thành Integer, được trình biên dịch cho phép
kcazllerraf

116

không thể sử dụng phép tạo tướng, nhưng nếu bạn xác định danh sách theo cách khác thì có thể lưu trữ TestB trong đó:

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

Bạn vẫn phải kiểm tra kiểu khi bạn đang sử dụng các đối tượng trong danh sách.


14
Đây thậm chí không phải là cú pháp Java hợp lệ.
newacct

2
Điều đó là đúng, nhưng nó mang tính minh họa hơn là thực tế chính xác
Salandur

Tôi có phương thức sau: createdSingleString (List <? Extends Object> object) Bên trong phương thức này tôi gọi String.valueOf (Object) để thu gọn danh sách thành một chuỗi. Nó hoạt động rất tốt khi đầu vào là Danh sách <Long>, Danh sách <Boolean>, v.v. Cảm ơn!
Chris Sprague

2
Nếu TestA là giao diện thì sao?
Suvitruf - Andrei Apanasik

8
Bạn có thể đưa ra một MCVE nơi điều này thực sự hoạt động? Tôi nhận được Cannot instantiate the type ArrayList<? extends TestA>.
Nateowami

42

Bạn thực sự không thể *:

Ví dụ được lấy từ hướng dẫn Java này

Giả sử có hai loại ABnhư vậy B extends A. Sau đó, mã sau đây là chính xác:

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

Mã trước đó là hợp lệ vì Blà một lớp con của A. Bây giờ, những gì xảy ra với List<A>List<B>?

Hóa ra đó List<B>không phải là một lớp con List<A>do đó chúng ta không thể viết

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

Hơn nữa, chúng tôi thậm chí không thể viết

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

*: Để thực hiện việc truyền có thể, chúng ta cần một cha mẹ chung cho cả hai List<A>List<B>: List<?>ví dụ. Sau đây hợp lệ:

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

Tuy nhiên, bạn sẽ nhận được một cảnh báo. Bạn có thể ngăn chặn nó bằng cách thêm @SuppressWarnings("unchecked")vào phương thức của bạn.


2
Ngoài ra, sử dụng tài liệu trong liên kết này có thể tránh được chuyển đổi không được kiểm tra, vì trình biên dịch có thể giải mã toàn bộ chuyển đổi.
Ryan H

29

Với Java 8, bạn thực sự có thể

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

32
Có thực sự đúc danh sách? Bởi vì nó đọc giống như một loạt các hoạt động để lặp lại toàn bộ danh sách, truyền từng phần tử, sau đó thêm chúng vào một danh sách mới được khởi tạo của loại mong muốn. Nói cách khác, bạn không thể làm chính xác điều này với Java7 - với một new List<TestB>, một vòng lặp và một diễn viên?
Patrick M

5
Từ OP "và tôi muốn bỏ tất cả các đối tượng trong danh sách đó" - thực ra, đây là một trong số ít câu trả lời trả lời câu hỏi như đã nêu, ngay cả khi đó không phải là câu hỏi mà mọi người duyệt ở đây.
Luke Usherwood

17

Tôi nghĩ rằng bạn đang truyền sai hướng mặc dù ... nếu phương thức trả về một danh sách các TestAđối tượng, thì thực sự không an toàn khi sử dụng chúng TestB.

Về cơ bản, bạn đang yêu cầu trình biên dịch cho phép bạn thực hiện các TestBthao tác trên một loại TestAkhông hỗ trợ chúng.


11

Vì đây là một câu hỏi được tham khảo rộng rãi và các câu trả lời hiện tại chủ yếu giải thích lý do tại sao nó không hoạt động (hoặc đề xuất các giải pháp nguy hiểm, hacky mà tôi không bao giờ muốn thấy trong mã sản xuất), tôi nghĩ rằng nó phù hợp để thêm một câu trả lời khác, hiển thị những cạm bẫy, và một giải pháp có thể.


Lý do tại sao điều này không hoạt động nói chung đã được chỉ ra trong các câu trả lời khác: Việc chuyển đổi có thực sự hợp lệ hay không phụ thuộc vào loại đối tượng được chứa trong danh sách ban đầu. Khi có các đối tượng trong danh sách có loại không phải là loại TestB, nhưng thuộc một lớp con khác TestA, thì diễn viên không hợp lệ.


Tất nhiên, các diễn viên thể hợp lệ. Đôi khi bạn có thông tin về các loại không có sẵn cho trình biên dịch. Trong những trường hợp này, có thể bỏ danh sách, nhưng nói chung, không nên :

Người ta có thể ...

  • ... bỏ toàn bộ danh sách hoặc
  • ... bỏ tất cả các yếu tố của danh sách

Ý nghĩa của cách tiếp cận đầu tiên (tương ứng với câu trả lời hiện được chấp nhận) là tinh tế. Nó có vẻ hoạt động đúng ngay từ cái nhìn đầu tiên. Nhưng nếu có các loại sai trong danh sách đầu vào, thì ClassCastExceptionsẽ bị ném, có thể tại một vị trí hoàn toàn khác trong mã và có thể khó gỡ lỗi này và tìm ra phần tử sai nào lọt vào danh sách. Vấn đề tồi tệ nhất là ai đó thậm chí có thể thêm các yếu tố không hợp lệ sau khi danh sách đã được bỏ, khiến việc gỡ lỗi thậm chí còn khó khăn hơn.

Vấn đề gỡ lỗi các giả mạo này ClassCastExceptionscó thể được giảm bớt với Collections#checkedCollectionhọ các phương thức.


Lọc danh sách dựa trên loại

Một cách an toàn hơn để chuyển đổi từ a List<Supertype> sang a List<Subtype>là thực sự lọc danh sách và tạo một danh sách mới chỉ chứa các thành phần có loại nhất định. Có một số mức độ tự do để thực hiện một phương pháp như vậy (ví dụ về việc xử lý các nullmục), nhưng một cách thực hiện có thể có thể như sau:

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

Phương pháp này có thể được sử dụng để lọc các danh sách tùy ý (không chỉ với mối quan hệ Subtype-Supertype đã cho về các tham số loại), như trong ví dụ này:

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

Bạn không thể cast List<TestB>đến List<TestA>như Steve Kuo đề cập NHƯNG bạn có thể đổ nội dung của List<TestA>thành List<TestB>. Hãy thử như sau:

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

Tôi đã không thử mã này nên có thể có lỗi nhưng ý tưởng là nó nên lặp qua đối tượng dữ liệu thêm các phần tử (đối tượng TestB) vào Danh sách. Tôi hi vọng nó làm việc cho bạn.


Tại sao điều này đã bỏ phiếu? Nó rất hữu ích với tôi và tôi không thấy có lời giải thích nào cho việc bỏ phiếu.
zod

7
Chắc chắn bài này không sai. Nhưng người hỏi câu hỏi cuối cùng cũng cần một danh sách TestB. Trong trường hợp đó câu trả lời này là sai lệch. Bạn có thể thêm tất cả các thành phần trong Danh sách <TestB> vào Danh sách <TestA> bằng cách gọi Danh sách <TestA> .add ALL (Danh sách <TestB>). Nhưng bạn không thể sử dụng Danh sách <TestB> .add ALL (Danh sách <TestA>);
prageeth

6

Cách an toàn tốt nhất là triển khai một AbstractListvà các mục trong thực hiện. Tôi đã tạo ListUtillớp người trợ giúp:

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

Bạn có thể sử dụng castphương pháp để truyền một cách mù quáng các đối tượng trong danh sách và convertphương thức để truyền an toàn. Thí dụ:

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

Cách duy nhất tôi biết là bằng cách sao chép:

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

2
Khá kỳ lạ Tôi nghĩ rằng điều này không đưa ra một cảnh báo trình biên dịch.
Simon Forsberg

Đây là một sự kỳ quặc với mảng. Thở dài, mảng java rất vô dụng. Nhưng, tôi nghĩ rằng nó chắc chắn không tệ hơn, và dễ đọc hơn nhiều so với các câu trả lời khác. Tôi không thấy điều đó không an toàn nữa so với diễn viên.
Thufir

3

Khi bạn truyền tham chiếu đối tượng, bạn chỉ truyền kiểu tham chiếu, không phải kiểu đối tượng. đúc sẽ không thay đổi loại thực tế của đối tượng.

Java không có các quy tắc ngầm để chuyển đổi các loại Đối tượng. (Không giống như người nguyên thủy)

Thay vào đó, bạn cần cung cấp cách chuyển đổi loại này sang loại khác và gọi nó bằng tay.

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

Điều này dài dòng hơn trong một ngôn ngữ có hỗ trợ trực tiếp, nhưng nó hoạt động và bạn không cần phải làm điều này rất thường xuyên.


2

nếu bạn có một đối tượng của lớp TestA, bạn không thể sử dụng nó TestB. mỗi TestBlà một TestA, nhưng không phải là cách khác.

trong đoạn mã sau:

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

dòng thứ hai sẽ ném a ClassCastException.

bạn chỉ có thể truyền TestAtham chiếu nếu chính đối tượng đó là TestB. ví dụ:

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

vì vậy, không phải lúc nào bạn cũng có thể đưa danh sách TestAvào danh sách TestB.


1
Tôi nghĩ rằng anh ta đang nói về một cái gì đó như, bạn lấy 'a' làm đối số phương thức được tạo như - TestA a = new TestB (), sau đó bạn muốn lấy đối tượng TestB và sau đó bạn cần truyền nó - TestB b = ( TestB) a;
samsamara

2

Bạn có thể sử dụng selectInstancesphương thức trong Bộ sưu tập Eclipse . Điều này sẽ liên quan đến việc tạo ra một bộ sưu tập mới, tuy nhiên sẽ không hiệu quả như giải pháp được chấp nhận sử dụng đúc.

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

Tôi đã đưa StringBuffervào ví dụ để chỉ ra rằng selectInstanceskhông chỉ truyền tải loại mà còn lọc nếu bộ sưu tập có chứa các loại hỗn hợp.

Lưu ý: Tôi là người đi làm cho Bộ sưu tập Eclipse.


1

Điều này có thể là do loại tẩy. Bạn sẽ thấy rằng

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

Trong nội bộ cả hai danh sách là loại List<Object>. Vì lý do đó, bạn không thể bỏ cái này sang cái khác - không có gì để đúc.


"Không có gì để diễn" và "Bạn không thể bỏ cái này sang cái khác" là một chút mâu thuẫn. Integer j = 42; Integer i = (Integer) j;hoạt động tốt, cả hai đều là số nguyên, cả hai đều có cùng một lớp, vì vậy "không có gì để đúc".
Simon Forsberg

1

Vấn đề là phương thức của bạn KHÔNG trả về danh sách TestA nếu nó chứa TestB, vậy nếu nó được gõ chính xác thì sao? Sau đó, diễn viên này:

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

hoạt động tốt như bạn có thể hy vọng (Eclipse cảnh báo bạn về một dàn diễn viên không được kiểm soát, đó chính xác là những gì bạn đang làm, vì vậy meh). Vì vậy, bạn có thể sử dụng điều này để giải quyết vấn đề của bạn? Trên thực tế bạn có thể vì điều này:

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

Chính xác những gì bạn yêu cầu, phải không? hoặc để thực sự chính xác:

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

dường như biên dịch tốt cho tôi


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

Xét nghiệm này cho thấy cách để cast List<Object>để List<MyClass>. Nhưng bạn cần chú ý rằng objectListphải chứa các thể hiện cùng loại với MyClass. Và ví dụ này có thể được xem xét khi List<T>được sử dụng. Đối với mục đích này, nhận trường Class<T> clazztrong constructor và sử dụng nó thay vì MyClass.class.


0

Điều này sẽ làm việc

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

1
Điều này làm cho một bản sao không cần thiết.
chắc chắn

0

Khá kỳ lạ là việc truyền danh sách theo cách thủ công vẫn không được cung cấp bởi một số hộp công cụ triển khai một cái gì đó như:

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

Tất nhiên, điều này sẽ không kiểm tra từng mục một, nhưng đó chính xác là những gì chúng tôi muốn tránh ở đây, nếu chúng tôi biết rằng việc triển khai của chúng tôi chỉ cung cấp loại phụ.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.