递归泛型用法

5

编辑:我收到了来自'erickson'的非常相关的答案,但我的原始示例没有明确涵盖并且没有解决他的答案中存在的一个问题(向上转型?)。我扩展了示例以涵盖这个问题,并将其包含在本帖子的末尾。感谢您的帮助。

我目前遇到的与Java泛型有关的问题与所谓的"奇异递归泛型模式"有关。我读完Jon Skeet对这个问题的回答"java枚举定义"后,认为我已经找到了解决方案。然而,当我尝试在我的代码中应用它时,我发现自己面临不同的问题。

我想出了一个“小”示例,其中出现了我正在面临的问题。我希望它足够清晰,以说明我的问题。

示例说明: 我想构建一个图,其中节点类型可以变化。我定义了一个抽象类Node,它定义了一些基本方法,以及实现这些方法的具体类,即ConcreteNode。我还创建了一个特化的ConcreteNode称为City

在给定的图中,一个重要的要求是所有元素都应该由相同类型或子类型组成,即ConcreteNode的图只能有ConcreteNodes 城市。

这些是我的类的定义:

abstract class Node<T extends Node<T>>
class ConcreteNode<T extends ConcreteNode<T>> extends Node<T>
class City extends ConcreteNode<City>

这些定义使用了“Recurring Generic Pattern”,该模式也出现在Enum类的定义中:
Class Enum<E extends Enum<E>>
问题:我使用这些类时遇到了问题。如果我只需要停留在层次结构的城市级别,即将城市连接到城市,那么我就没有问题,但是当我尝试访问其他类时,我遇到了巨大的问题。

在下面的代码中,我的问题可以在GraphUtil的方法签名中看到:

  1. addNewNeighbors1a使用原始类型Node,但至少它可以工作。
  2. addNewNeighbors1b使用类型Node,但根本无法编译(错误包含在代码中)。
  3. addNewNeighbors1c为Node使用更复杂的参数,我希望它能够工作,但它不能编译(错误包含在代码中)。
  4. addNewNeighbors3使用Node的复杂参数,但它再次不能编译,即使node和newNode的参数相同。

总之,我的问题是如何向上转型这些泛型类型,这些类型的参数化是它们自己?

我很乐意为GraphUtil的方法寻找最佳签名,假设这些方法将位于一个不知道City甚至ConcreteNode的库中。

谢谢大家。

以下是示例的完整代码

package test.city;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

public class TestCity {
    abstract class Node<T extends Node<T>> {
    public abstract void addNeighbor(T n);
    public abstract void addNeighbors(Collection<? extends T> nodes);
    public abstract Collection<T> neighbors();
    }

    class ConcreteNode<T extends ConcreteNode<T>> extends Node<T> {
    protected Collection<T> _neighbors = new ArrayList<T>();

    @Override
    public void addNeighbor(T n) {
        _neighbors.add(n);
    }

    @Override
    public void addNeighbors(Collection<? extends T> nodes) {
        _neighbors.addAll(nodes);
    }

    @Override
    public Collection<T> neighbors() {
        return _neighbors;
    }
    }

    class City extends ConcreteNode<City> {
    protected String _name;

    public City(String name) {
        _name = name;
    }

    @Override
    public String toString() {
        return _name;
    }
    }

    public TestCity() {
    City nyc = new City("NYC");
    nyc.addNeighbor(new City("Boston"));
    nyc.addNeighbor(new City("Wash"));

    GraphUtil.print("Printing cities", nyc.neighbors());

    GraphUtil.printNeighbors1(nyc);
    GraphUtil.printNeighbors2(nyc);
    GraphUtil.printNeighbors3(nyc);
    GraphUtil.printNeighbors4(nyc);
    GraphUtil.addNewNeighbors1a(nyc, new City("Miami"));
    GraphUtil.addNewNeighbors2(nyc, new City("NewOr"));
    GraphUtil.addNewNeighbors3(nyc, new City("Dallas"));
    }

    static class GraphUtil {
    static void printNeighbors1(Node<?> node) {
        print("Nodes", node.neighbors());
    }

    static void printNeighbors2(ConcreteNode<?> node) {
        print("Concrete nodes", node.neighbors());
    }

    static void printNeighbors3(Node<? extends Node<?>> node) {
        print("Nodes2", node.neighbors());
    }

    static void printNeighbors4(ConcreteNode<? extends ConcreteNode<?>> node) {
        print("Concrete nodes2", node.neighbors());
    }

    static void addNewNeighbors1a(Node node, City newNode) {
        node.addNeighbor(newNode);
        print("Add city to node", node.neighbors());
    }

    static void addNewNeighbors1b(Node<?> node, City newNode) {
        // node.addNeighbor( newNode ); <---- DOES NOT COMPILE!!!
        // The method addNeighbor(capture#8-of ?) in the type
        // TestCity.Node<capture#8-of ?>
        // is not applicable for the arguments (TestCity.City)
    }

    static void addNewNeighbors1c(Node<? extends Node<?>> node, City newNode) {
        // node.addNeighbor( newNode ); <---- DOES NOT COMPILE!!!
        // The method addNeighbor(capture#9-of ? extends TestCity.Node<?>)
        // in the type
        // TestCity.Node<capture#9-of ? extends TestCity.Node<?>> is not
        // applicable for the arguments (TestCity.City)

    }

    static void addNewNeighbors2(Node node, ConcreteNode newNode) {
        node.addNeighbor(newNode);
        print("Add concrete node to node", node.neighbors());
    }

    static void addNewNeighbors3(Node<? extends Node<?>> node,
        Node<? extends Node<?>> newNode) {
        // node.addNeighbor( newNode ); <---- DOES NOT COMPILE!!!
        // The method addNeighbor(capture#8-of ? extends TestCity.Node<?>)
        // in the type
        // TestCity.Node<capture#8-of ? extends TestCity.Node<?>> is not
        // applicable for the arguments
        // (TestCity.Node<capture#10-of ? extends TestCity.Node<?>>)
    }

    static void print(String msg, Collection<?> col) {
        System.out.println(msg + ": " + Arrays.toString(col.toArray()));
    }
    }

    public static void main(String[] args) {
    new TestCity();
    }

}

运行此代码的输出如下(完全没有惊喜):
Printing cities: [Boston, Wash]
Nodes: [Boston, Wash]
Concrete nodes: [Boston, Wash]
Nodes2: [Boston, Wash]
Concrete nodes2: [Boston, Wash]
Add city to node: [Boston, Wash, Miami]
Add concrete node to node: [Boston, Wash, Miami, NewOr]

问题的第二部分

有一个相关的问题,我在原始示例中没有包含它,因为我认为解决方案也将适用。

我现在已经向GraphUtil添加了以下方法:

static <T extends Node<T>> T getSomeNeighbor(T node) {
    return node.neighbors().iterator().next();
}

我从我的主类尝试以下操作:

City someCity = GraphUtil.getSomeNeighbor(nyc); 
someCity.addNeighbor(new City("London")); // OK

ConcreteNode someCN1 = GraphUtil.getSomeNeighbor(nyc); 
someCN1.addNeighbor(new City("Paris")); // OK, but raw

ConcreteNode<?> someCN2 = GraphUtil.getSomeNeighbor(nyc); 
someCN2.addNeighbor(new City("Berlin")); // Does not compile

ConcreteNode<?> nc = new City("");
nc.addNeighbor(new City("Bern")); // Does not compile

第一个案例有效,因为我知道返回的具体类型,并且它与参数中提供的类型一致。
在第二和第三种情况下,我假设我不知道 City 类型。第二种情况有效,但我使用原始类型 ConcreteNode。
在第三种情况下,第二行出现编译错误:"The method addNeighbor(capture#3-of ?) in the type TestCity.ConcreteNode is not applicable for the arguments (TestCity.City)." 在这个例子中,我使用 'new City("-")' 作为参数,因为我不知道如何将它们向上转换。在第四种情况下,我试图将 City 向上转换为 ConcreteNode,但失败了。当前的编译器错误如下:"The method addNeighbor(capture#4-of ?) in the type TestCity.ConcreteNode is not applicable for the arguments (TestCity.City)" 问题:
1. 如何在不知道 City 类型的情况下修复第2和第3种情况? 2. 如何将 City 向上转换为 ConcreteNode(或 Node)?
感谢您的帮助。

抱歉,你更新的问题中的示例过于牵强,无法帮助我想象出你真正遇到的问题。在你所有的示例中,你都知道addNeighbor方法的参数类型(它总是City)。你能否展示一下你真正想要编写的方法——也就是其中参数类型没有静态声明的方法?你可能需要展示一下如何调用这个方法。 - erickson
将近14年过去了...你最后解决了第二部分的问题吗? - undefined
4个回答

2

您可以创建通用的 方法 和通用的类型。使用这些功能,就可以像这样修复GraphUtils中的问题方法:

static <T extends Node<T>> void addNewNeighbors1a(T node, T newNode)
{
  node.addNeighbor(newNode);
  print("Add city to node", node.neighbors());
}

static <T extends Node<T>> void addNewNeighbors2(T node, T newNode)
{
  node.addNeighbor(newNode);
  print("Add concrete node to node", node.neighbors());
}

嘿,等一下……那些方法是相同的!

事实证明,由于它们只依赖于 Node 的接口,你只需要其中一个来处理任何 Node 实现。

在后续过程中,你可能会发现需要改变 Node 接口,比如这样:

public abstract <S extends T> void addNeighbor(S n);

你好,感谢你的回答:非常清晰并且解决了我问题的一部分。然而,你的回答没有涵盖另一个方面。我已经更新了原帖以包含这个方面。如果你能帮助我解决它,那就太好了。非常感谢。 - nozebacle

2

对于第一部分,我会使方法签名

static <T extends Node<T>> void addNewNeighbors(Node<? super T> node, T newNode)
{
  node.addNeighbor(newNode);
}

这样,节点不必特别与新节点的类型相同。

对于第二部分,我会做类似的事情。

ConcreteNode<? super City> nc = new City("");
nc.addNeighbor(new City("Bern"));

第二部分并不有用,因为我需要知道市才能编写ConcreteNode <? super City>,而我假设我不知道。 - nozebacle

1
关于你提到的第二部分,通配符确实是一个问题。ConcreteNode可能是某种类型,而City并没有扩展它 - 例如,ConcreteNode<Suburb>并没有被City扩展 - 尽管你知道它是什么,但编译器不知道。

你说得对...这意味着我的图可能由不同类型的ConcreteNodes组成,是吗? 如果我知道这不是情况,那么我的唯一选择就是使用原始类型或使用强制转换? - nozebacle
我不会说那是唯一的选择,但你目前的设计有点束缚了你。 - Carl

0
一、您在静态方法中使用的通配符允许添加任何类型的节点,但编译器知道这是不允许的。假设您正在处理蛋白质图(例如我所做的),并且您有一个Protein extends Node<Protein>类及其实例。如果要使用通配符方法,则可以添加蛋白质。
二、要解决此问题,请尝试按以下方式定义add方法:
public <U extends T> void add(U other)

T由类参数定义,该方法允许添加任何T的子类,如本地参数U所定义。这应该解决了你在一个方向上的问题,尽管你只能将子类添加到超类中。


感谢您的回答。蛋白质图确实是同一应用程序的另一个应用。 正如Erickson所提到的,添加类型U以添加是有用的,以支持未来的类型。然而,我的原始问题是关于GraphUtil的方法,而这些问题已经按照他的建议得到解决。 现在我已经将问题的另一部分添加到了原始帖子中。 - nozebacle

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