Java使用泛型与列表和接口

5

好的,这是我的问题:

我有一个包含接口的列表 - List<Interface> a - 和一个扩展该接口的接口列表: List<SubInterface> b。我想要设置 a = b。我不希望使用 addAll() 或任何会消耗更多内存的东西,因为我正在进行非常费力的操作。我确实需要能够说 a = b。我尝试过 List<? extends Interface> a,但是这样我就不能将接口添加到列表 a 中,只能添加子接口。有什么建议吗?

我想要做类似这样的事情:

List<SubRecord> records = new ArrayList<SubRecord>();
//add things to records
recordKeeper.myList = records;

类RecordKeeper包含接口列表(不是子接口)。
public class RecordKeeper{
    public List<Record> myList;
}

没有干净的方法可以在不修改列表定义的情况下完成这个任务。假设你这样做了,那么这将是正确的,但实际上是错误的:class Record2 extends Record { ... }; recordKeeper.myList = records; recordKeeper.myList.add(new Record2()); 你能看出问题出在哪里吗? - aalku
如果您使用“?extends Record”,那么意味着您不知道列表的确切类型,因此必须使用类型转换(在列表上)才能添加元素。Java会知道列表上的每个元素都扩展了Record,因此您可以从中获取Record对象,但是您无法插入任何Record,因为List的类型是Java不知道的。您想进入派对,但门卫丢失了客人名单。 - aalku
所以,八年后,我回顾这个问题时意识到Java编译器(是吗?已经有一段时间没有使用Java了)只是愚蠢的,并且在LSP方面表现不佳。 - MirroredFate
6个回答

6

这个有效:

public class TestList {

    interface Record {}
    interface SubRecord extends Record {}

    public static void main(String[] args) {
        List<? extends Record> l = new ArrayList<Record>();
        List<SubRecord> l2 = new ArrayList<SubRecord>();
        Record i = new Record(){};
        SubRecord j = new SubRecord(){};

        l = l2;
        Record a = l.get( 0 );
        ((List<Record>)l).add( i );       //<--will fail at run time,see below
        ((List<SubRecord>)l).add( j );    //<--will be ok at run time

    }

}

我的意思是代码可以编译,但在添加任何内容之前,你必须强制转换你的List<? extends Record>。如果你想要转换的类型是Record的子类,Java将允许进行转换,但它无法猜测它将是哪种类型,你必须指定它。

List<Record>只能包含Records(包括subRecords),List<SubRecord>只能包含SubRecords

但是List<SubRecord>不是List<Record>,因为它不能包含Records,而子类应该始终做超类可以做的事情。这很重要,因为继承是特化,如果List<SubRecords>List<Record>的子类,它应该能够包含,但它不能。

List<Record>List<SubRecord>都是List<? extends Record>。但在List<? extends Record>中,你不能添加任何东西,因为Java无法知道List确切的类型是什么。想象一下,如果你可以,那么你可以有以下语句:

List<? extends Record> l = l2;
l.add( new Record() );

正如我们刚才所看到的,这只对List<Record>有效,而不适用于任何List<继承自Record的类型>,比如List<SubRecord>
祝好, Stéphane

所以,事实上有一个巧妙的方法可以解决这个问题。List<Record> a = (List<Record>)(List<? extends Record>) b;通过对子记录列表进行双重转换,实际上可以使用a引用b。 - MirroredFate
我怀疑你无法在运行时添加记录。此外,第二个转换(最右边的那个)似乎是不必要的。而且,在你的解释中,说List<? extends Record>是List<Record>是错误的。反过来才是正确的。 - Snicolas
实际上,在运行时它是可以工作的。而且你把它搞反了。List <? extends Record> 是 List<Record>,因为任何继承 Record 的东西都是 Record,因为继承 Record 的东西包含了 Record 的所有属性。 - MirroredFate
好的。继承是一种特化。对吗?因此,当您说“扩展记录的所有内容都是记录”时,这是正确的。但是,如果您所说的容器是正确的:由于List<Record>可以容纳记录(在Java中以下语句有效:List<Record>.add(new Record());),因此对于扩展它的类也应该是正确的。但是您无法将记录添加到List<? extends Record>中,以下语句无效:List<? extends Record>.add(new Record());。 敬礼,Stéphane - Snicolas
https://dev59.com/vVjUa4cB1Zd3GeqPTrHK#6504576 - Snicolas

3

仅为解释Java为什么不允许此操作:

  • List<Record>是一个列表,您可以将任何实现Record的对象放入其中,并且您获取的每个对象都将实现Record
  • List<SubRecord>是一个列表,您可以将任何实现SubRecord的对象放入其中,并且您获取的每个对象都将实现SubRecord

如果允许简单地将List<SubRecord>用作List<Record>,则将允许以下操作:

List<SubRecord> subrecords = new ArrayList<SubRecord>();
List<Record> records = subrecords;
records.add(new Record()); // no problem here

SubRecord sr = subrecords.get(0); // bang!

你看,这样就不是类型安全的了。一个 List(或者任何参数化类的对象)不能在类型安全的情况下改变其参数类型。

针对你的情况,我提供以下解决方案:

  • Use List<Record> from start. (You can add SubRecord objects to this without problems.)
    • as a variation of this, you can use List<? super Subrecord> for the method which adds stuff. List<Record> is a subtype of this.
  • copy the list:

    List<Record> records = new ArrayList<Record>(subrecords);
    

稍微解释一下这个变化:

void addSubrecords(List<? super Subrecord> subrecords) {
    ...
}


List<Record> records = new ArrayList<Record>();
addSubrecords(records);
recordkeeper.records = records;

我将尝试您的List<? super Subrecord>解决方案,因为那可能正是我正在寻找的。我希望如此,因为我觉得虽然我的当前解决方案可行,但有点不太正规。 - MirroredFate
啊,可惜我仍然找不到一种方法来使用它,使其能够完成与我的答案相同的事情。我曾经抱有如此高的期望。:'( - MirroredFate
没有办法以类型安全的方式完成这个任务。你可以欺骗编译器(如果你不使用特殊的类型检查集合,虚拟机不知道类型,但它会在运行时工作),但这不是类型安全的。 - Paŭlo Ebermann

2

这个方法可以工作,但是你需要注意!!!

@Override
// Patient is Interface
public List<Patient> getAllPatients() {

   // patientService.loadPatients() returns a list of subclasess of Interface Patient
   return (List<Patient>)(List<? extends Patient>)patientService.loadPatients(); 
}

您可以通过以下方式将对象列表转换为接口列表。 但是,如果您在代码的某个地方获取了此列表,并尝试向其添加内容,您会添加什么?接口还是该接口的子类?实际上,您失去了列表类型的信息,因为您让它保持接口,因此您可以添加实现此接口的任何内容,但是列表仅保留子类,并且如果您尝试使用其他子类对此列表执行添加或获取等操作,则可能很容易出现类转换异常。解决方案是:将源列表的类型更改为list<Interface>而不是强制转换,然后您就可以自由地进行操作 :)

1

你不能这样做并保持安全,因为在Java中List<Interface>List<SubInterface>是不同的类型。即使你可以将SubInterface类型添加到Interface列表中,但如果它们是不同接口的子/超级接口,则无法将两个具有不同接口的列表相等。

为什么你这么想要做b = a呢?你只是想存储对SubInterface列表的引用吗?

顺便说一下,我建议你阅读Oracle网站上的这份文档:http://download.oracle.com/javase/tutorial/java/generics/index.html 它很好地解释了泛型。


1

所以,我的一个朋友找到的相当简单的解决方案是这样的:

recordKeeper.myList = (List<Record>)(List<? extends Record>)records;

据我理解,这个方法之所以有效是因为它采用了逐步推进的方式。List<SubRecord> 是一个 List<? extends Record>,而 List<? extends Record> 又是一个 List<Record>。虽然可能不太美观,但它仍然有效。


不,List<? extends Record> 既有 List<Record> 也有 List<Subrecord> 作为子类型。编译器应该在这里给出警告。"is a" 关系是错误的符号表示。 - Paŭlo Ebermann
6
如果你感到高兴,那很好。只要确保当你在别的地方更改程序时,这种非类型安全转换会导致它在另一个点上开始出现错误,这一点不会让你感到惊讶。 - Paŭlo Ebermann

0

没有一种类型安全的方法可以做到这一点。

List<SubInterface> 无法添加任意的 Interface。毕竟它只是一个 SubInterface 的列表。

如果您确信即使不是类型安全也可以安全执行此操作,则可以执行以下操作:

@SuppressWarnings("unchecked")
void yourMethodName() {
  ...
  List<Interface> a = (List<Interface>) b;
  ...
}

如果您不想添加,那么为什么在您的问题中写道:“但是我不能将接口添加到列表中,只能添加子接口”? - Snicolas
@MirroredFate,我错了。我不明白。这里只有一个列表--new ArrayList<SubRecord>()。你想要有两个变量引用这个列表,myListrecords,对吗? - Mike Samuel
@Snicolas 如果我将List<Interface>添加泛型,使其变为List<? extends Interface>,那么我就不能再向该列表中添加Interface,只能添加实现或扩展Interface的内容。但我不想这样做。 @Mike Samuel 我编辑了一下,让它更清晰了。是的,myList和records应该引用同一个列表。 - MirroredFate
@MirroredFate,你想要能够执行myRecordKeeper.myList.add(anyRecord)吗? - Mike Samuel
@MirroredFate,你必须提供代码来解释如何使用你的通用列表(超类型)。 - Snicolas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接