【问题标题】:Java generics cast and usageJava 泛型转换和使用
【发布时间】:2020-07-28 07:46:19
【问题描述】:

我对泛型继承有些问题。下面是依赖树:

public class Attributes {
}

public abstract class Feature<T extends Attributes> {
    
    private T attributes;

    public T getAttributes() {
        return attributes;
    }

    public void setAttributes(T attributes) {
        this.attributes = attributes;
    }
}

public abstract class AbstractFeatureRepository<T extends Feature<? extends Attributes>> {
    public abstract String getType();

    public abstract boolean create(T feature);
}

我有这些功能存储库的实现,如下所示:

public class NodeAttributes extends Attributes {
    
    private String startPoint;

    public String getStartPoint() {
        return startPoint;
    }

    public void setStartPoint(String startPoint) {
        this.startPoint = startPoint;
    }
}

public class Node extends Feature<NodeAttributes> {
}

public class NodeRepository extends AbstractFeatureRepository<Node> {
    public String getType() {
        return "Node";
    }
    public boolean create(Node node) {
        return true;
    }
}

public class LinkAttributes extends Attributes {
    
    private String uri;

    public String getUri() {
        return uri;
    }

    public void setUri(String uri) {
        this.uri = uri;
    }

}

public class Link extends Feature<LinkAttributes> {
}

public class LinkRepository extends AbstractFeatureRepository<Link> {
    public String getType() {
        return "Link";
    }
    public boolean create(Link link) {
        return true;
    }
}

我通过构造函数将这些带有 Spring 的存储库注入到控制器(但在本例中,为了简单起见,我在构造函数中手动创建):

public class Controller {
    
    private final Map<String, AbstractFeatureRepository<? extends Feature>> featureRepositories;
    
    public Controller() {
        this.featureRepositories = Arrays.asList(new NodeRepository(), new LinkRepository()).stream()
                .collect(Collectors.toMap(AbstractFeatureRepository::getType, Function.identity()));
    }
    
    public Node createNode() {
        Node newNode = new Node();
        newNode.getAttributes().setStartPoint("Berlin");
        createFeature("Node", newNode);
        return newNode;
    }

    public Link createLink() {
        Link newLink = new Link();
        newLink.getAttributes().setUri("/home/local");
        createFeature("Link", newLink);
        return newLink;
    }
    
    
    private void createFeature(String type, Feature<? extends Attributes> feature) {
        featureRepositories.get(type).create(feature);
    }

}

一切都很好,直到我想在通用“createFeature”中调用“create”方法,但出现编译错误

AbstractFeatureRepository 不适用于参数 (Feature)

我做错了什么?这是因为我可能会传递某种其他类型的“功能”而不是特定的“存储库”可以使用吗?那么我应该如何在 Controller 中更改我的地图存储库,以便编译器不会抱怨?我虽然应该操作精确的类作为 map 的键,但不知道如何使所有这些工作。有什么建议吗?

谢谢。

更新 1。我将地图更改为

private final Map<Class<?>, AbstractFeatureRepository<? extends Feature>> featureRepositories;

更改了 AbstractFeatureRepository,使其现在看起来像这样:

public abstract class AbstractFeatureRepository<T extends Feature> {
    
    public abstract Class<T> getClazz();
    
    public abstract boolean create(T feature);
}

并更改了控制器中的方法:

    public Link createLink() {
        Link newLink = new Link();
        createFeature(Link.class, newLink);
        return newLink;
    }
    
    
    private <T extends Feature> void createFeature(Class<T> class1, T feature) {
        AbstractFeatureRepository<? extends Feature> abstractFeatureRepository = featureRepositories.get(feature.getClass());
        abstractFeatureRepository.create(abstractFeatureRepository.getClazz().cast(feature));
    }

它仍然不允许我这样做。

【问题讨论】:

  • 如果你知道,通过构造,featureRepositories.get("Node") 只能是NodeRepository,显式转换。事实上,无论如何,这将是一个检查型强制转换,所以如果不是,它会立即失败。
  • 我的第一个建议是摆脱这种过度设计。看起来您刚刚了解了继承和泛型,并且迫不及待地将它们放在不应该出现的地方。至于实际问题get("Node") 不是类型安全的,get(Node.class) 会是,因为Class 是通用的。但同样,我认为这是一个巨大的设计问题,泛型问题只是一个次要情节。
  • 你能给我们提供你的 AbstractRepository 类吗?也许问题就在那里。
  • @AndriyKucher 尽管 Kayaman 对过度工程发表了评论,但您可能会发现“类型安全的异构容器”会有所帮助。 Effective Java 中有描述;我相信您可以在网络上的其他地方找到它。
  • 我的评论也不是刻薄的意思。对于这类问题,我只是经常有似曾相识的感觉。

标签: java generics capture


【解决方案1】:

这段代码:

featureRepositories.get(type)

根据java.util.Map 的文档,返回一个类型为Map&lt;K, V&gt; 中的V 的对象。在您的代码中,这意味着表达式的类型为 AbstractFeatureRepository&lt;? extends Feature&lt;? extends Attributes&gt;&gt;

让我们稍微简化一下,假设我们有List&lt;? extends Number&gt;

这是有效的java代码:

List<? extends Number> list = new ArrayList<Integer>();

这就是? extends 的意义所在,真的。这不编译:

List<Number> list = new ArrayList<Integer>();

现在,假设您拨打了List&lt;? extends Number&gt;

List<? extends Number> list = new ArrayList<Integer>();
Number n = Double.valueOf(5.0);
list.add(n);

呃。我的整数列表中有一个非整数。哎呀。

这就是为什么你根本不能在这里打电话给add()您根本无法通过List&lt;? extends whatever&gt; 呼叫add。任何以 T 为参数的方法,你的类型是 Foo&lt;? extends T&gt; 都不能被调用*。

让我们现在回到你的代码:

您有一个AbstractFeatureRepository&lt;? extends Feature&lt;? extends Attributes&gt;&gt; - 因此AbstractFeatureRepository 具有的任何采用T 的方法都不能从中调用。一点也不。而create就是这样一种方法。

解决方案有点棘手。如果您以某种方式引用了表示 T 的类,则可以使用类型安全的容器(小心;事物可以是不能是类的 TList&lt;Integer&gt; 是 T,但只有 List.class 存在,你不能写List&lt;Integer&gt;.class! - 你可以用那个:

public <T extends Attribute> void createFeature(Class<T> typeDesired, Feature<T> feature) {
    featureRepositories.get(type).create(typeDesired.cast(feature));
}

是一种方式。

一般而言,您的方法签名存在问题:无法保证您的字符串类型 String type 暗示所需的功能类型 Feature&lt;? extends Attribute&gt; 由与该类型匹配的存储库处理。

第二种选择是让每个AbstractFeatureRepository 负责处理类型不匹配。在这种情况下,您可以将界面更新为改为读取create(Object feature) throws TypeMismatchException。或者,让它返回一个类型 (public Class&lt;T&gt; getType()),然后您可以返回到 cast 构造。

*) 好吧,你可以调用它,如果你按字面意思传递null,因为 null 是所有数据类型。但这显然不是您打算在这里做的,因此不相关。

【讨论】:

  • 请阅读我的更新 1。尽管我认为我正在转换为 repo 支持的类型,但仍然没有运气。
  • 尽管这是一个非常好的和详细的答案(你有我的赞成票),但这段代码给我的错误与原始代码相同:/
  • 可以,只要 ?涉及到您仍然遇到一些麻烦 - 这是更多关于为什么会发生这种情况的一般指南,真正的修复需要重新设计,超出问题中可用的代码。
【解决方案2】:

如果您在 Map 中只有 2 个东西(或 N 个东西,其中 N 是一个小数字,您在评论中提到 4),并且您使用固定键来指示特定类型,那么使用 Map它比必要的更难。

映射必然是同质类型的,也就是说,所有的键都有一个共同的超类型,所有的值都有一个共同的超类型。您面临的问题是您希望键的类型与值的类型相关:这可以通过 类型安全的异构容器 来完成(本质上是:您构建的映射使得您可以对类型之间的关系施加约束)。但即使这样对于所描述的问题来说也过于复杂了。

改为使用两个 (N) 字段:

public class Controller {
    private final NodeRepository nodeRepository = new NodeRepository();
    private final LinkRepository linkRepository = new LinkRepository();

这仍然是一种映射:键是字段,值是字段值。

但这样做的好处是您保留了存储库的具体类型,因此您可以将它们传递给方法:

private <A extends Attributes> void createFeature(AbstractFeatureRepository<A> repo, Feature<A> feature) {
    repo.create(feature);
}

例如

public Node createNode() {
    Node newNode = new Node();
    newNode.getAttributes().setStartPoint("Berlin");
    // Or, easier, nodeRepository.create(newNode);
    createFeature(nodeRepository, newNode);
    return newNode;
}

public Link createLink() {
    Link newLink = new Link();
    newLink.getAttributes().setUri("/home/local");
    // Or, easier, linkRepository.create(newNode);
    createFeature(linkRepository, newLink);
    return newLink;
}

【讨论】:

  • 谢谢,但我不想将每个 Repo 都存储在这个控制器中,这就是我想要摆脱的。目前有 4 个存储库,但以后会更多,所以我计划为它们添加存储类型,以便它们将自动注册到 Spring。
【解决方案3】:

为了达到尽可能接近原始代码的a working solution,我进行了三到四个相对较小的重构。

最显着的变化发生在 Controller.createFeature()...

private <T extends Feature<?>> void createFeature(Class<T> class1, T feature) {
    AbstractFeatureRepository<T> abstractFeatureRepository = (AbstractFeatureRepository<T>)featureRepositories.get(feature.getClass());
    abstractFeatureRepository.create(feature);
}

在我看来,演员阵容是最简单、最安全的解决方案。我确信演员表是类型安全的原因是因为如果演员表不存在你会得到编译错误:

Controller.java:31: error: incompatible types: AbstractFeatureRepository<CAP#1> cannot be converted to AbstractFeatureRepository<T>
    AbstractFeatureRepository<T> abstractFeatureRepository = featureRepositories.get(feature.getClass());

where T is a type-variable:
    T extends Feature<?> declared in method <T>createFeature(Class<T>,T)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Feature<?> from capture of ? extends Feature<?>
1 error

如果您仔细阅读错误消息的底部,您会发现 T extends Feature&lt;?&gt;CAP#1 extends Feature&lt;?&gt; 之间的唯一区别是两个类型变量。它们都有相同的上限 (extends Feature&lt;?&gt;)。这告诉我有理由推断强制转换是类型安全的。

所以,我用 SuppressWarnings("unchecked") 注释了该方法。

为了确认该解决方案是否可用,我用 toString() 充实了 NodeLink 类。在the solution demo 中调用Controller.createNode()Controller.createLink() 可以让您...

Node: [NodeAttributes - Start Point: 'Berlin']

Link: [LinkAttributes - URI: 'urn::foo::bar']

我不得不承认,您要解决的问题对我来说不是很清楚。因此,我仅根据我的一般 Java 知识做出了一些假设。如果解决方案符合您的要求,请告诉我?

【讨论】:

  • 你可以用更严格的界限替换我演示中的所有&lt;?&gt;s。例如&lt;S extends Attributes, T extends Feature&lt;S&gt;&gt; void createFeature(...)AbstractFeatureRepo&lt;T extends Feature&lt;? extends Attributes&gt;&gt;Map&lt;Class&lt;?&gt;, AbstractFeatureRepo&lt;? extends Feature&lt;? extends Attributes&gt;&gt;&gt; featureRepositories。我对 &lt;?&gt; 情有独钟。 Since they're reifiable,它们的类型信息在运行时完全可用。
  • 你知道,我找到了类似的解决方案。实际如此,涉及演员阵容。这是我在这里看到的唯一解决方案,我真的很惊讶在如此简单的情况下这是一种痛苦。我稍后会发布我的解决方案,但它与您的建议类似。
  • 不幸的是,您的解释为什么它是安全的并不能说服我。 T extends Feature> 实际上可以是任何东西。
  • T extends Feature> 实际上可以是任何东西“——你能在我之前的评论中看到替代建议吗?
  • 你的解释为什么它是安全的并不能说服我“——为了理解我的解释,run this。您会看到“...错误:方法 m() 已定义...”它是 已定义,因为&lt;CAP1 extends Foo&gt; 声明的类型和 &lt;T extends Foo&gt; 声明的类型是完全相同的类型Look here for details。如果这个例子不能让你相信 T extends Feature&lt;?&gt;CAP#1 extends Feature&lt;?&gt; 也是完全相同的类型 ,那么什么都不会。
【解决方案4】:

这是我使用的方法:

public class Controller {

    private final Map<Class<?>, AbstractFeatureRepository<? extends Feature>> featureRepositories;

    public Controller3(List<AbstractFeatureRepository<? extends Feature>> featureRepositories) {
        this.featureRepositories = featureRepositories.stream()
                .collect(Collectors.toMap(AbstractFeatureRepository::getClazz, Function.identity()));
    }

    public Node createNode() {
        Node newNode = new Node();
        createFeature(Node.class, newNode);
        return newNode;
    }

    public Link createLink() {
        Link newLink = new Link();
        createFeature(Link.class, newLink);
        return newLink;
    }

    private <T extends Feature> void createFeature(Class<T> clazz, T feature) {
        AbstractFeatureRepository<T> repository = getRepository(clazz);
        repository.create(feature);
    }
    
    @SuppressWarnings("unchecked")
    private <T extends Feature, V extends AbstractFeatureRepository<T>> V getRepository(Class<T> clazz) {
        return (V) featureRepositories.get(clazz);
    }

    public static void main(String[] args) {
        Controller3 controller = new Controller3();
        controller.createLink();
    }
}

它不完全满足无强制转换的方法(尤其是没有@SuppressWarnings),但它对我来说是最不邪恶的,因为强制转换仅在控制器中的一种方法中完成,所有其他方法都没有强制转换,也没有@SuppressWarnings .

【讨论】:

  • 代码——正如你在这里展示的那样——fails to compileLine: 27 type argument T is not within bounds of type-variable TLine: 32 type argument T is not within bounds of type-variable T。点击that demo页面顶部绿色的开始按钮,查看你得到的编译失败。
  • 另外,您已将 Feature&lt;T extends Attributes&gt; 声明为泛型类。但是everywhere,您将它用作原始类型。此处:&lt;T extends Feature&gt; void createFeature(...)此处:&lt;T extends Feature...&gt; V getRepository(...)此处:Map&lt;Class&lt;?&gt;, AbstractFeatureRepository&lt;? extends Feature&gt;&gt;... 并在 ctcr 中:Controller3(List&lt;AbstractFeatureRepository&lt;? extends Feature&gt;&gt;。到处!那是“Heap Pollution”。在我的答案下的第一条评论中查看修复?
【解决方案5】:

试试

private static <T extends AbstractFeatureRepository> void createFeature(Class<T> clazz, Feature<? extends Attributes> feature) {
    ((T) featureRepositories.get(clazz)).create(feature);
}

您应该相应地修改 featureRepositories

private static final Map<Class<?>, AbstractFeatureRepository<? extends Feature>> featureRepositories

但我不建议使用这样的泛型。

【讨论】:

  • 我不认为这是我需要的。据我了解,您建议将存储库类作为参数传递,以便稍后我可以对其进行转换。但我不想使用实现,而是想在抽象级别使用 repos。
  • 这与您在代码中使用的方法相同,但不是传递刺痛,而是传递类,然后转换存储库。我不使用实现 createFeature 函数不依赖于 AbstractFeatureRepository 的具体实现
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-08-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多