是否适合扩展List以添加字段

时间:2015-09-09 13:57:15

标签: java oop collections

这个问题是List的框架,但很容易适用于java集合框架中的其他人。

例如,我想说制作一个新的List子类型来存储类似添加计数器的东西当然是合适的,因为它是列表操作的一个组成部分并且不会改变它“是一个清单”。像这样:

public class ChangeTrackingList<E> extends ArrayList<E> {
   private int changeCount;
   ...
   @Override public boolean add(E e) {
      changeCount++;
      return super.add(e);
   }
   // other methods likewise overridden as appropriate to track change counter
}

但是,如何根据列表ADT的知识添加其他功能,例如存储与列表元素关联的任意数据?当然,假设在添加和删除元素时正确管理了相关数据。像这样:

public class PayloadList<E> extends ArrayList<E> {
   private Object[] payload;
   ...
   public void setData(int index, Object data) {
      ... // manage 'payload' array
      payload[index] = data;
   }
   public Object getData(int index) {
      ... // manage 'payload' array, error handling, etc.
      return payload[index];
   }
}

在这种情况下,我通过不仅添加其他功能(行为)而且添加其他状态来改变它“仅仅是一个列表”。当然是类型规范和继承的一部分,但是Java集合类型是否有一个隐含的限制(禁忌,弃用,不良实践等)来专门处理它们?

例如,当将此PayloadList引用为java.util.List时,将会正常变异和迭代并忽略有效负载。当涉及到持久性或复制等不期望List携带要维护的其他数据的事情时,这是有问题的。我见过许多接受对象的地方,检查一下它是“列表”,然后简单地将其视为java.util.List。他们是否应该允许任意应用程序贡献来管理特定的具体子类型?

由于此实现会在实例切片(忽略子类型字段)中不断产生问题,因此以这种方式扩展集合并在需要管理其他数据时始终使用合成是一个坏主意吗?或者它是持久性或复制的工作来解释所有具体的子类型,包括其他字段?

3 个答案:

答案 0 :(得分:3)

这纯粹是一个意见问题,但我个人建议不要在几乎所有情况下扩展像ArrayList这样的课程,而不是赞成作文。

即使你的ChangeTrackingList也很狡猾。什么

list.addAll(Arrays.asList("foo", "bar"));

做什么?它会增加changeCount两次,还是根本不增加?这取决于ArrayList.addAll()是否使用add(),这是一个您不必担心的实现细节。您还必须使方法与ArrayList方法保持同步。例如,目前addAll(Collection<?> collection) 是在add()之上实现的,但是如果他们在将来的版本中决定首先检查collection instanceof ArrayList,如果是,请使用要System.arraycopy直接复制数据,您必须将addAll()方法更改为仅changeCount增加collection.size(),如果该集合为ArrayList(否则在add()完成。

此外,如果某个方法被添加到List(例如forEach()stream()),如果您使用该方法名称来表示其他内容,则会导致问题。在扩展abstract类时也会发生这种情况,但至少abstract类没有状态,因此您不太可能通过覆盖方法造成太多损害。

我仍然会使用List界面,理想情况下会扩展AbstractList。像这样的东西

public final class PayloadList<E> extends AbstractList<E> implements RandomAccess {

    private final ArrayList<E> list;
    private final Object[] payload;

    // details missing
}

这样你就有了一个实现List的类,并使用ArrayList而不必担心实现细节。

(顺便说一句,在我看来,课程AbstractList非常棒。您只需覆盖get()size()即可实现有效List的实施和方法例如containsAll()toString()stream()都可以正常工作。)

答案 1 :(得分:0)

以下只是意见,这个问题会引发自以为是的答案(我认为它的边界线不适合SO)。

虽然你的方法在某些情况下是可行的,但我认为它的设计不好而且非常脆弱。覆盖所有漏洞也很复杂(甚至可能是不可能的,例如当列表通过其他而不是List.sort进行排序时)。

如果您需要存储和管理额外的数据,它可以更好地集成到列表项本身,或者可以使用现有的集合类型(如Map)关联数据。

如果您确实需要关联列表类型,请考虑使其是java.util.List的实例,而是使用专用API的专用类型。这样,通过将其传递到预期的清单,就不会发生意外事故。

答案 2 :(得分:0)

您应该考虑的一个方面是,从AbstractList继承的所有类都是值类。这意味着它们具有有意义的equals(Object)hashCode()方法,因此即使它们不是任何类的相同实例,也会判断两个列表相等。

此外,来自equals()的{​​{1}}合同允许将任何列表与当前列表进行比较 - 而不仅仅是具有相同实现的列表。

现在,如果在扩展值类时将值项添加到值类,则需要在AbstractListequals()方法中包含该值项。否则,您将允许考虑具有不同有效负载的两个hashCode()列表&#34;相同&#34;有人在地图,集合中使用它们,或者在任何算法中只进行简单的PayloadList比较。

但实际上,通过添加一个值项来扩展一个值类是不可能的!您最终会破坏equals()合约,或者破坏对称性(与包含{{1}的equals()相比,包含ArrayList的普通[1,2,3]将返回true有效负载为PayloadList,但反向比较不会返回true。或者你会打破传递性。

这意味着基本上,扩展值类的唯一正确方法是添加非值行为(例如,记录每个[1,2,3][a,b,c]的列表。避免违约的唯一方法是使用构图。并且它必须是根本不实现add() 的组合(因为同样,其他列表将接受实现remove()的任何内容并在迭代时提供相同的值)。

此答案基于Joshua Bloch的第2版“有效Java”第8项