【问题标题】:Generic tree, self bounded-generics泛型树,自有界泛型
【发布时间】:2011-09-27 08:33:04
【问题描述】:

我要为我的一个项目添加通用性。 我喜欢泛型,因为这使我的代码更加健壮、自我记录并消除了所有那些丑陋的演员表。

但是,我遇到了一个棘手的案例,并且在尝试为我的一个结构表达“递归”约束时遇到了一些问题。

这基本上是某种“通用”树,带有双链接(指向子级和父级)。我最大程度地简化了课程以显示问题:

public class GenericTree<
    ParentClass extends GenericTree<?, ?>, 
    ChildClass extends GenericTree<?, ?>> 
{
    // Attributes
    private ArrayList<ChildClass> children = new ArrayList<ChildClass>();
    private ParentClass parent = null;

    // Methods 
    public void setParent(ParentClass parent) {
        this.parent = parent;
    }

    public void addChild(ChildClass child) {
        child.setParent(this);
        this.children.add(child);
    }
}

问题在于指令:child.setParent(this)

Java 给出以下错误:

绑定不匹配:类型为 ChildClass 的方法 setParent(?) 不适用于
参数(通用树)。通配符参数 ?没有更低的 绑定,并且实际上可能比参数更具限制性 通用树

我想要的是能够表达类似的东西:

public class GenericTree<
    ParentClass extends GenericTree<?, ?>, 
    ChildClass extends GenericTree<[THIS_VERY_CLASS], ?>> 

要说子类的父类应该是它自己...

我看过一些关于自边界泛型的文章,但我不知道如何在这种情况下应用它。

任何帮助将不胜感激。

【问题讨论】:

  • 为什么树在节点类型方面不是同质的?我认为没有理由单独指定子节点类型和父节点类型。但是,如果您区分叶子和内部节点,我确实理解。
  • 另外,如果这无法实现,我希望至少能够使用演员表。为此,我需要能够引用我的声明的第三个通配符 (?) 类。有可能吗?
  • 因为那太简单了。在这里,我们有使用 Country -> Location -> Building -> Room 的树,唯一的共同属性是“name”。我觉得这对泛型来说是不可能的......
  • 要能够转换,必须先转换成非泛型类型,像这样:return (GenericTree&lt;Type&gt;)((GenericTree)obj);需要SuppressWarning({"unsecure", "rawtypes"})
  • @Eyal:是的,实际的树具有异构节点类型。类似于:风电场的聚合 WindFarm Wind Turbines Cluster Wind Turbine 例如,风力集群类的声明如下所示: class WindCluster ...不,叶子没有与内部节点分离。

标签: java generics nested-generics


【解决方案1】:

不要对不均匀的树使用泛型,使用接口和强制转换。

虽然您可以使用泛型解决一些问题,但生成的代码会很脆弱,向您显示以前从未见过的错误消息,修复错误通常会导致尝试和错误,即使您编译它,您不知道为什么;-)

[EDIT2] 添加了addChild() 和下面的使用示例。

[编辑] 还在我身边吗?如果你真的需要,请使用这个 API:

interface ParentNode<Child> {
    List<Child> getChildren();
    void addChild(Child child);
}
interface ChildNode<Parent> {
    void setParent(Parent parent);
    Parent getParent();
}
// There is no way to avoid this because we would need to define
// "Node" recursively.
@SuppressWarnings( "rawtypes" )
class Node<
    Parent extends ParentNode<? extends Node>,
    Child extends ChildNode<? extends Node>
>
implements
    ParentNode<Child>,
    ChildNode<Parent>
{
    private Parent parent;
    public Parent getParent() { return parent; }
    public void setParent(Parent parent) { 
        this.parent = parent;
        // Here, we must case the child to a type that will accept Node
        @SuppressWarnings( "unchecked" )
        ParentNode<Node> cast = (ParentNode)parent;
        cast.addChild(this); // Note: Either add the child here ...
    }

    private List<Child> children;
    public List<Child> getChildren() { return children; }
    public void addChild( Child child ) { 
        children.add(child);
        // Here, we must case the child to a type that will accept Node
        @SuppressWarnings( "unchecked" )
        ChildNode<Node> cast = (ChildNode)child;
        cast.setParent(this); // ... or here but not twice :-)
    }
}

即将两个函数(向上和向下)拆分为两个接口,然后创建一个实现两者的节点类型。这允许您将叶节点和根节点定义为特殊节点(没有两个 API 之一),或者您可以像定义任何其他节点一样定义它们并在“不支持”方法中返回 null

用法:

public class DirFileNode extends Node<DirFileNode, DirFileNode> {
}
public class TreeUsage {
    public static void main( String[] args ) {
        DirFileNode node = new DirFileNode();
        DirFileNode node2 = new DirFileNode();
        node.addChild( node2 );
        // Why yes, I do love infinite loops. How can you tell?
        node2.addChild( node );
    }
}

您可以看到 API 确保所有内容都是类型安全的,但在内部,您必须进行强制转换。只要您不在节点中使用泛型类型,这很简单。如果这样做,声明就会变得一团糟。

【讨论】:

  • 我喜欢你对 API 所做的事情,这是接口隔离的一个很好的例子,但另一方面 - 由于 Node 不是 抽象的,这是否意味着你将无法在树中使用 Node 本身,因为 ? extends Node 仅将实例限制为 Node 子类?
  • 我不能 100% 确定这些限制,但由于异构树无论如何都会使用子类型作为节点,所以这应该不是问题。如果是,只需创建一个 SimpleNode extends Node 就可以了。
  • 即使作者创建了 SimpleNode 他/她仍然无法调用 child.setParent(this); this.children.add(child);直到类型擦除已知。这是否意味着在每个 SimpleNode 实现中都需要提供 addChild?
  • @Syntax:不,请参阅我的编辑。您必须在 setParent()/add() 中使用未经检查的强制转换,但您可以重复使用它们。
  • 谢谢。这仍然需要演员,但没关系。而且拆分Parent和Child接口的想法很好。
【解决方案2】:

我能得到的最接近的是遵循 Enum 的自我引用模式。它仍然需要未经检查的强制转换,但假设您的子类正确定义了 T,它应该是安全的。

public class GenericTree<T extends GenericTree<T, P, C>, P extends GenericTree<P, ?, T>, C extends GenericTree<C, T, ?>> {

    // Attributes
    private ArrayList<C> children = new ArrayList<C>();
    private P parent = null;

    // Methods
    public void setParent(P parent) {
        this.parent = parent;
    }

    public void addChild(C child) {
        @SuppressWarnings("unchecked")
        final T thisAsType = (T) this;
        child.setParent(thisAsType);
        this.children.add(child);
    }
}

编辑: 实现示例

public static class SingleTypeTree<T> extends
    GenericTree<SingleTypeTree<T>, SingleTypeTree<T>, SingleTypeTree<T>> {

}

【讨论】:

  • 这就是我一直在寻找的“自我参考”技巧。谢谢。
  • 你能提供一个实现这个的类的演示吗?我知道我一定遗漏了一些明显的东西,但是在尝试定义根或叶(即没有父或子类型的类)时,这对我来说似乎是一种无限的回归。 掌心
  • 嗯...是的,我似乎也遇到了同样的问题。让我看看能不能找到解决方法。
  • 我认为不可能使用此解决方案:枚举自引用模式对编译器不友好。以mindprod.com/jgloss/enum.html 此处的代码为例,它显示了从枚举反编译以及它如何引用父 Enum 类型;这个反编译的代码不会重新编译。
  • 我不确定您上面提出的实施示例是否是您所说的解决方案;我希望看到适用于问题中给出的 WindCluster/WindFarm 和 WindTurbine 示例的建议解决方案。特别是在它不是单一类型的树的情况下。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多