【问题标题】:Generic Type x Generic Parameter: Building a "very generic" structure通用类型 x 通用参数:构建“非常通用”的结构
【发布时间】:2013-08-15 20:19:24
【问题描述】:

目标

例如,我们有一个T 类型的Tree 类:即Tree<T>

我们想让这个Tree<T> 类能够持有

  1. Tree<T>(当然),
  2. SubTree<T>SubTree extends Tree,
  3. Tree<SubT> 其中SubT extends T
  4. SubTree<SubT> 其中SubTree extends TreeSubT extends T

“Hold”表示接受某个子类,根据请求分别返回某个子类的对象。

比如原来的ArrayList有这个属性:

private static class Leaf {
}
private static class RedLeaf extends Leaf {
}
@Test
public final void test() {
    ArrayList<Leaf> al = new ArrayList<Leaf>();
    al.add(new Leaf());
    System.out.println(al.get(al.size()-1).getClass());     // class Leaf
    al.add(new RedLeaf());
    System.out.println(al.get(al.size()-1).getClass());     // class RedLeaf
}

这是因为原始的ArrayList 只是保留了输入对象的引用,而不是重新创建它。这不是构建我的类时所期望的行为,尤其是一棵树。考虑以下示例:

public final void test() {
    ArrayList<Leaf> al = new ArrayList<Leaf>();
    Leaf leaf = new Leaf();
    RedLeaf redLeaf = new RedLeaf();
    al.add(leaf);
    al.add(redLeaf);
    al.add(leaf);
    System.out.println(al.indexOf(al.get( 0 )));    // 0
    System.out.println(al.indexOf(al.get( 1 )));    // 1
    System.out.println(al.indexOf(al.get( 2 )));    // 0 <-- disaster
}

为什么这是一场灾难?考虑对于树中的某个节点,我们想要找到下一个兄弟节点。

其实我们可以在插入元素的时候做个快速修复:

private static class Leaf {
    Leaf() { super(); }
    Leaf(Leaf leaf) { super(); }
}
private static class RedLeaf extends Leaf {
    RedLeaf() { super(); }
    RedLeaf(RedLeaf redLeaf) { super(redLeaf); }
}
@Test
public final void test() {
    ArrayList<Leaf> al = new ArrayList<Leaf>();
    Leaf leaf = new Leaf();
    RedLeaf redLeaf = new RedLeaf();
    al.add(new Leaf(leaf));
    al.add(new RedLeaf(redLeaf));
    al.add(new Leaf(leaf));
    System.out.println(al.indexOf(al.get( 0 )));    // 0
    System.out.println(al.indexOf(al.get( 1 )));    // 1
    System.out.println(al.indexOf(al.get( 2 )));    // 2 <-- nice :-)
}

但是当涉及到构建我们自己的类(Tree)时,这将成为一个大问题。

所以,我们的目标是:

  1. 持有“所有”子类,并且
  2. 结构中的每个元素都是独一无二的。

(下面的解决方案)


原来的问题

我们有一个Tree 类,它使用ArrayList 来保存节点:

public class Tree<T> {
    // some constructors & methods skipped
    private final ArrayList<Tree<T>> mChildren = new ArrayList<Tree<T>>();
}

我们有这个addChild 方法,没有问题:

public void addChild(final Tree<T> subTree) {
    getChildren().add(new Tree<T>(this, subTree));  // copy the tree & set parent attach to this
}

然后,我们想让addChild 方法更通用,它允许添加子类型的树。

private class RedTree<T> extends Tree<T> {}
private void showArrayListIsOkForSubType() {
    RedTree<T> redTree = new RedTree();
    getChildren().add(redTree);
    getChildren().add(new RedTree());
}

在概念上,我们想将addChild方法修改成这样:

(但以下代码存在编译错误,以 cmets 显示。)

public <Leaf extends T, SubTree extends Tree<T>> void add(final SubTree<Leaf> subTree) {
    // The type SubTree is not generic; it cannot be parameterized with arguments <Leaf>

    SubTree<Leaf> tr = new SubTree<Leaf>();
    getChildren().add(new SubTree<Leaf>());
    // SubTree cannot be resolved to a type
    // Leaf cannot be resolved to a type
}

我们已经通过 stackoverflow 进行了搜索,但仍然没有任何帮助。你能帮我们正确的语法吗?


我的代码

在@Jason C 的指导和解释下,这是我的代码。希望它可以帮助其他人:)

另外,请随时纠正我:)

注意:代码并非 100% 完整。但所有主要部分都包括在内。

首先,在默认的零参数构造函数中,确保所有子类都定义了复制构造函数。

/** Default constructor. **/
public Tree() {     // All sub-classes instantiation must invoke this default constructor
    super();                // here is a good place to ensure every sub-class has a copy constructor
    if (Reflection.hasCopyConstructor(this) == false)
        throw new CopyConstructorRequiredException(this.getClass());
}
class Reflection {
    public static boolean hasCopyConstructor(final Object object) {
        return hasCopyConstructor(object.getClass());
    }
    public static boolean hasCopyConstructor(final Class<?> clazz) {
        try {
            clazz.getDeclaredConstructor(clazz);
            return true;
        } catch (SecurityException e) {
            e.printStackTrace();
            return false;
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
            return false;
        }
    }
}

那么这是基类Tree&lt;T&gt;的复制构造函数:

private Tree(final Tree<? extends T> copyFrom) {
    super();
    if (copyFrom != null) {
        this.setData(copyFrom.getData());
        for (final Tree<? extends T> child : copyFrom.getChildren()) {
            this.addChildren(child);    // addChildren() handles null well
        }
    }
}

只有通用参数&lt;T&gt; 需要子类&lt;? extends T&gt; 的通配符。

参数Tree 通过自动转换固有地接受Tree 的所有子类。

所以,这个复制构造函数已经能够接受Tree&lt;T&gt;SubTree&lt;T&gt;Tree&lt;SubT&gt;SubTree&lt;SubT&gt;

对于扩展类的拷贝构造函数,可以很简单:

private static class BlueTree<T> extends Tree<T> {
    private BlueTree(final BlueTree<T> blueTree) { super(blueTree); }
}

回到基类Tree。以下是addChild 存储对象的方式。

public Tree<T> addChildren(final Tree<? extends T>... subTrees) {
    if (subTrees == null)               // called addChildren((Tree<T>) null)
        addChild((Tree<T>) null);           // add null to children
    else
        for (final Tree<? extends T> subTree : subTrees)      // empty parameter goes here != null array
            addChild(subTree);
    return this;
}
public Tree<T> addChild(final Tree<? extends T> subTree) {
    if (subTree == null)            // for addChild((Tree<T>) null)
        getChildren().add(null);        // add null to children
    else {                          // else
        getChildren().add(              // copy (constructor) the tree & set parent attach to this
                Reflection.<Tree<T>>invokeConstructor(subTree, new ParameterTypeAndArg(subTree.getClass(), subTree))
                .setParent(this));
    }
    return this;
}

因为我们已经检查了每个子类都必须包含默认构造函数,所以我们可以在这里通过反射安全地调用它,以获取子类的新实例,并将其存储到childrenArrayList 中。

附:我们需要使用反射调用,因为普通的new 不适用于泛型参数。

【问题讨论】:

  • 你能告诉我们更多代码吗?构造函数等是什么?
  • public void add(final Tree&lt;? extends T&gt; subTree) 就足够了。无需参数化add()。请参阅下面的答案。
  • “编译器错误”部分看起来像 Scala =D 在 Scala 中,我们可以编写采用泛型类型参数的函数。

标签: java generics methods arraylist generic-programming


【解决方案1】:

嗯,首先,你把它复杂化了。您真正需要做的就是:

public void add(final Tree<? extends T> subTree) {

无需参数化add()

但无论如何,我会解决你最初的尝试:你想要SubTree extends Tree&lt;Leaf&gt;,因为即使Leaf extends T 你也不能保证SubTree extends Tree&lt;T&gt;SubTree&lt;Leaf&gt; 匹配。例如。如果您的类层次结构是:

public class Base { }
public class A extends Base { }
public class B extends Base { }

如果LeafA 并且SubTreeTree&lt;B&gt; 那么add (final SubTree&lt;Leaf&gt;) 不匹配Tree&lt;B&gt;

所以从概念上讲,你实际上想要这个:

public <Leaf extends T, SubTree extends Tree<Leaf>> void add(final SubTree<Leaf> subTree) {

当然,这不是有效的语法。你真正需要做的就是:

public <Leaf extends T, SubTree extends Tree<Leaf>> void add(final SubTree subTree) {

这足以匹配所有必要的类型。试试看:

{
    Tree<Object> x = new Tree<Object>();
    MyTree<Integer> y = new MyTree<Integer>();
    Tree<Integer> z = new Tree<Integer>();

    x.add(y);
    y.add(x); // not valid, as Tree<Object> does not extend Tree<Integer>  
    y.add(z); // fine, as Tree<Integer> matches
}

public static class MyTree<T> extends Tree<T> {     
}

add() 中,您也不要参数化SubTree,因为它的类型已经在别处指定:

SubTree tr = ...;

但是,现在您遇到了一个更大的问题,这是一个经典问题,并且在此处的许多其他地方都有答案:

tr = new SubTree();

由于类型擦除,您无法实例化泛型类型的对象。您必须在某处指定子树的 Class 并使用 .newInstance() 实例化它。

【讨论】:

  • 谢谢!真的很好的答案,很好的解释!我有几个问题。如果我使用&lt;? extends T&gt; 方法,我是否仍然可以调用SubTree 的构造函数来真正创建一个子树,但不能将其转换为Tree?这确实是一个大问题,我们无法为泛型类型创建对象。那么newInstance() 就是解决方案。这是否意味着我必须在之后将值从 subTree 复制到 anotherNewNode
  • 不能仅从泛型类型实例化对象。时期。您必须拥有可用对象的Class。然后可以使用Class.newInstance() 用默认构造函数实例化该类型的对象,也可以使用Class.getConstructor(...) 获取特定的非默认构造函数。您是否要复制值或存储引用或其他完全取决于您的应用程序要求。如果你需要复制它们,那么你必须复制它们。如果您不需要复制它们,那么您不必复制它们。做你需要做的。 :)
  • 另外,您永远不需要将SubTree 显式转换为Tree。您始终可以将SubTree 存储在一个可以保存Tree 的变量中,而无需任何显式转换,因为SubTree 扩展了Tree。您需要明确地强制转换(除非您知道TreeSubTree,否则该转换可能会失败)。不管怎样,Class&lt;T&gt;.newInstance() 的返回类型是T。见docs.oracle.com/javase/7/docs/api/java/lang/Class.html
  • 非常感谢!!根据您的回答和解释,我学到了很多东西,并构建了一个非常通用的 Tree,它可以接受并保存(不重新创建到超类)SubTree、Tree 的节点, 子树。我使用默认构造函数来确保所有子类都定义了一个复制构造函数。这样,在插入时,我可以安全地使用反射来调用子类的复制构造函数。代码贴在上面。请随时纠正我。我希望它也能帮助其他人:-)
猜你喜欢
  • 1970-01-01
  • 2015-01-08
  • 2011-03-03
  • 2017-12-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多