扩展Java ArrayList

18
我想扩展ArrayList以添加一些特定类的方法,这些方法被扩展的ArrayList所持有。下面是一个简化的示例代码。
我认为这很合理,但我很新于Java,我看到其他问题不鼓励扩展ArrayList,例如Extending ArrayList and Creating new methods。我不了解足够的Java来理解反对意见。
在我的先前尝试中,我最终创建了许多在ThingContainer中本质上是ArrayList传递的方法,因此扩展似乎更容易。
是否有更好的方法来实现我正在尝试做的事情?如果是这样,应该如何实现?
import java.util.*;

class Thing {
    public String name;
    public int amt;

    public Thing(String name, int amt) {
        this.name = name;
        this.amt = amt;
    }

    public String toString() {
        return String.format("%s: %d", name, amt);
    }

    public int getAmt() {
        return amt;
    }
}

class ThingContainer extends ArrayList<Thing> {
    public void report() {
        for(int i=0; i < size(); i++) {
            System.out.println(get(i));
        }
    }

    public int total() {
        int tot = 0;
        for(int i=0; i < size(); i++) {
            tot += ((Thing)get(i)).getAmt();
        }
        return tot;
    }

}

public class Tester {
    public static void main(String[] args) {
        ThingContainer blue = new ThingContainer();

        Thing a = new Thing("A", 2);
        Thing b = new Thing("B", 4);

        blue.add(a);
        blue.add(b);

        blue.report();
        System.out.println(blue.total());

        for (Thing tc: blue) {
            System.out.println(tc);
        }
    }
}

Jon Skeet的回答包括“除非我真的必须,否则我不会扩展ArrayList<>,而是更喜欢组合而不是继承”或者“正如其他许多人所说,是的,你可以扩展ArrayList类,但这通常不是你应该做的事情;在Java中,这被认为不是一个好的实践。”来源于https://dev59.com/GW445IYBdhLWcg3wmLhe。然而,从您和其他答案中,我似乎误解了一些东西。 - foosion
个人而言,我倾向于避免子类化JDK类,因为它们的实现可能会发生不可控制的变化,或者破坏了我的扩展中我没有意识到的某些东西。当依赖超类行为时,有时会出现微妙的陷阱,我们并不总能考虑到它们,或者如果我们没有实现细节,就不能考虑到它们。 - Dave Newton
@DaveNewton,那个问题使用JDK类同样适用吗? - foosion
你的意思是不使用子类化直接使用它们吗? - Dave Newton
那是不同的,因为这样类的所有方法的使用都受到类的控制。我会在几分钟内在我的答案中添加一个小段落。 - Dave Newton
增加了更多的组合优于继承的策略。 - Dave Newton
3个回答

11

这个答案中并没有阻止扩展ArrayList;只是存在语法问题。类的扩展存在的意义在于可以重复使用代码。

对于扩展类的常见反对意见是"更喜欢组合而非继承"的讨论。扩展并非总是首选机制,但它取决于你实际要做什么。

根据请求编辑组合示例。

public class ThingContainer implements List<Thing> { // Or Collection based on your needs.
    List<Thing> things;
    public boolean add(Thing thing) { things.add(thing); }
    public void clear() { things.clear(); }
    public Iterator<Thing> iterator() { things.iterator(); }
    // Etc., and create the list in the constructor
}

你并不一定需要公开完整的列表接口,只需公开集合或不公开。 尽管如此,不公开任何功能会大大降低其实用性。
在Groovy中,您可以使用@Delegate注释自动构建方法。 Java可以使用Project Lombok的@Delegate注释完成相同的事情。我不确定Lombok会如何公开接口,或者它是否这样做。
像glowcoder一样,我认为在这种情况下扩展没有什么根本性问题-真正的问题是哪个解决方案更适合该问题。
编辑以获取关于继承如何违反封装的详细信息
有关详细信息,请参见Bloch的Effective Java,项目16。
如果子类依赖于超类行为,并且超类的行为发生更改,则子类可能会出现故障。 如果我们无法控制超类,则可能会出现问题。
以下是从书籍中提取的具体示例(抱歉Josh!),伪代码和大量引述(所有错误均为我的)。
class CountingHashSet extends HashSet {
    private int count = 0;
    boolean add(Object o) {
        count++;
        return super.add(o);
    }
    boolean addAll(Collection c) {
        count += c.size();
        return super.addAll(c);
    }
    int getCount() { return count; }
}

然后我们使用它:
s = new CountingHashSet();
s.addAll(Arrays.asList("bar", "baz", "plugh");

它返回...三个吗?不是,是六个。为什么?

HashSet.addAll() 是在 HashSet.add() 上实现的,但这是一个内部实现细节。我们的子类 addAll() 添加了三个元素,调用 super.addAll(),该方法调用 add(),也会增加计数。

我们可以删除子类的 addAll(),但现在我们依赖于超类的实现细节,这可能会发生变化。我们可以修改我们的 addAll() 来迭代并在每个元素上调用 add(),但现在我们正在重新实现超类的行为,这违反了目的,并且如果超类行为取决于访问私有成员,则可能并不总是可行。

或者,超类可能会实现一个我们的子类没有的新方法,这意味着我们的类的使用者可能会通过直接调用超类方法而无意中绕过预期的行为,因此我们必须跟踪超类 API 以确定子类何时以及是否应更改。


3
我喜欢你最后一句话,“扩展并不总是首选机制”。这就说清楚了。当涉及到像这样的情况时,你需要使用java.util.List来组合,如果你仍然想让你的类去继承它,你必须创建一个成员List,然后编写大约20个其他的样板方法?所以,通常我同意组合比继承更好。但是我认为在这种情况下,继承才是正确的方式。 - corsiKa
我不明白如何在这里实现组合。请详细说明一下。或者你是说正常的反对意见在这里不适用? - foosion
@foosion 要进行合成,您需要创建应用程序特定的类,给它一个List实例变量,并将列表功能直接委托给该列表。我会在答案中放一个小例子。 - Dave Newton
@DaveNewton,啊哈。那实际上是我开始的方式。然后我认为扩展会更好,因为我不需要实现添加、清除等方法。扩展会自动提供它们。 - foosion

0

我认为扩展ArrayList并不必要。

public class ThingContainer {

    private ArrayList<Thing> myThings;

    public ThingContainer(){
        myThings = new ArrayList<Thing>();
    }

    public void doSomething(){
         //code
    }

    public Iterator<Thing> getIter(){
        return myThings.iterator();
    }
}

你应该在你的ThingContainer类中包装ArrayList。ThingContainer可以拥有任何你需要的处理方法。不需要扩展ArrayList;只需保持一个私有成员即可。 希望这可以帮助你。
你还可以考虑创建一个代表你的Thing类的接口。这为扩展性提供了更多的灵活性。
public Interface ThingInterface {
   public void doThing();
}

...

public OneThing implements ThingInterface {
   public void doThing(){
        //code
   }
}

public TwoThing implements ThingInterface {
   private String name;
   public void doThing(){
        //code
   }
}

3
在这种情况下,如果我想要从ArrayList中添加(add)、添加所有(addall)、清空(clear)、获取(get)、删除(remove)等操作,似乎需要创建相应的方法,而使用继承则可以让我直接使用ArrayList中的这些操作,无需进一步操作。我有什么遗漏吗? - foosion
怎样才能在ThingContainer中添加Thing呢? :P - Kowser

0

这是我的建议:

interface ThingStorage extends List<Thing> {
    public int total();
}

class ThingContainer implements ThingStorage {

    private List<Thing> things = new ArrayList<Thing>();

    public boolean add(Thing e) {
        return things.add(e);
    }

    ... remove/size/... etc     

    public int total() {
        int tot = 0;
        for(int i=0; i < size(); i++) {
            tot += ((Thing)get(i)).getAmt();
        }
        return tot;
    }

}

而且实际上不需要report()。toString()可以完成其余的工作。


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