如何在采用多个泛型参数的方法中使用 Java 泛型通配符?

发布于 2024-12-28 11:20:41 字数 1269 浏览 1 评论 0原文

因此,我们有一个像这样的通用方法,它是依赖注入初始化的一部分:

public static <TS, TI extends TS> void registerTransient(
    Class<TS> serviceClass, Class<TI> implementationClass)
{
    //
}

在某些时候,我们发现类可能不一定存在的情况。它是一个实现类,我们将注入多个关闭(因此服务类与实现类相同。)自然你会这样写:

Class<?> clazz = Class.forName("com.acme.components.MyPersonalImplementation");
registerTransient(clazz, clazz);

IDEA 对此没有问题,但 javac 抱怨:

error: method registerTransient in class TestTrash cannot be applied to given types;
required: Class<TS>,Class<TI>
found: Class<CAP#1>,Class<CAP#2>
reason: inferred type does not conform to declared bound(s)
inferred: CAP#2
bound(s): CAP#1
where TS,TI are type-variables:
TS extends Object declared in method <TS,TI>registerTransient(Class<TS>,Class<TI>)
TI extends TS declared in method <TS,TI>registerTransient(Class<TS>,Class<TI>)
where CAP#1,CAP#2 are fresh type-variables:
CAP#1 extends Object from capture of ?
CAP#2 extends Object from capture of ?

什么给出了?该方法要求第二个参数是第一个参数的子类。无论 ? 是什么类,它的两个参数都是同一个类对象,而且我认为,类总是可以从自身分配。这几乎就像 javac 不必要地发明了第二个通配符类型来用于第二个参数,然后“哦,亲爱的,你这里有两个通配符,所以我无法判断一个是否可以从另一个通配符分配”。

So we have a generic method like this, which is part of dependency injection initialisation:

public static <TS, TI extends TS> void registerTransient(
    Class<TS> serviceClass, Class<TI> implementationClass)
{
    //
}

At some point we found a case where a class might not necessarily be present. And it's an implementation class which we would be injecting multiple off (so the service class is the same as the implementation class.) Naturally you would write this like this:

Class<?> clazz = Class.forName("com.acme.components.MyPersonalImplementation");
registerTransient(clazz, clazz);

IDEA has no problems with this, but javac complains:

error: method registerTransient in class TestTrash cannot be applied to given types;
required: Class<TS>,Class<TI>
found: Class<CAP#1>,Class<CAP#2>
reason: inferred type does not conform to declared bound(s)
inferred: CAP#2
bound(s): CAP#1
where TS,TI are type-variables:
TS extends Object declared in method <TS,TI>registerTransient(Class<TS>,Class<TI>)
TI extends TS declared in method <TS,TI>registerTransient(Class<TS>,Class<TI>)
where CAP#1,CAP#2 are fresh type-variables:
CAP#1 extends Object from capture of ?
CAP#2 extends Object from capture of ?

What gives? The method requires the second parameter to be a subclass of the first. Irrespective of what class ? happens to be, it's the same class object for both parameters and a class is, I thought, always assignable from itself. It's almost as if javac is unnecessarily inventing a second wildcard type to use for the second parameter and then going "oh dear, you have two wildcards here, so I can't tell if one is assignable from the other."

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

┈┾☆殇 2025-01-04 11:20:42

问题是 Class 不能转换为任何其他类型,除非通过显式转换(它实际上变成 Class<#capture-... of ?> 在捕获转换期间)。因此,编译器无法(静态)将这些捕获的类型边界与方法定义的参数化类型进行匹配。

首先尝试将其显式转换为 Class,如下所示:

registerTransient((Class<Object>)clazz, clazz); 

这样编译器可以将 TS 绑定到 Object 并将 TI 绑定到扩展 Object 的对象(它但仍然会发出警告)。

事实上,IntelliJ 的编译器没有抱怨这一点,可能是由于某些优化,甚至可能是编译器错误。您应该将其发布并等待回复。

如果您想用稍微不同的方法检查它,即使它“看起来”没问题,以下内容仍然不会编译:

public class A {
    static class B {}

    static class C extends B {}

    static <T, R extends T> void method(final Class<T> t, final Class<R> r) {}

    public static final void main(String... args) {
        B b = new B();
        C c = new C();
        Class<?> cb = b.getClass();
        Class<?> cc = c.getClass();
        method(cb, cc);
    }
}

看看 此处。它呈现了 Java 类型系统的非凡视图(尽管相当密集)。

The problem is that Class<?> cannot be cast to any other type unless via an explicit cast (it actually becomes Class<#capture-... of ?> during capture conversion). As such, the compiler cannot (statically) match the type bounds of those captures with the parameterized types of the method definition.

Try casting it explicitly to Class first, like in:

registerTransient((Class<Object>)clazz, clazz); 

This way the compiler can bind TS to Object and TI to something that extends Object (it will still emit a warning, though).

The fact that IntelliJ's compiler does not complain about this, might be due to some optimization or might even be a compiler bug. You should post it as such and wait for a reply.

If you wish to check it with a slightly different approach, the following will still not compile, even though it "looks" ok:

public class A {
    static class B {}

    static class C extends B {}

    static <T, R extends T> void method(final Class<T> t, final Class<R> r) {}

    public static final void main(String... args) {
        B b = new B();
        C c = new C();
        Class<?> cb = b.getClass();
        Class<?> cc = c.getClass();
        method(cb, cc);
    }
}

Have a look in here. It presents an extraordinary view of Java's type system (although quite dense).

如此安好 2025-01-04 11:20:42

您遇到的问题是,根据 JLS 7 §6.5.6.1 简单表达式名称

如果表达式名称出现在需要进行赋值转换、方法调用转换或强制类型转换的上下文中,则表达式名称的类型是之后声明的字段、局部变量或参数的类型捕获转换 (§5.1.10)。

在您的例子中,“表达式名称”是标识符clazz。正如编译器输出所示,根据 JLS 的要求,它被捕获两次。

处理这个问题的常用技术是引入一个辅助方法将通配符绑定到类型变量:

private static <T> void registerTransient(Class<T> serviceAndImplClass)
{
    registerTransient(serviceAndImplClass, serviceAndImplClass);
}

使用通配符调用这个新方法将会起作用:

Class<?> clazz = Class.forName("com.acme.components.MyPersonalImplementation");
registerTransient(clazz);

我看到您在评论中提到了这个解决方法。这可能看起来很奇怪,但这实际上是语言设计者想要的方法。

The issue you're having is that the capture conversion happens individually for each method argument according to JLS 7 §6.5.6.1 Simple Expression Names:

If the expression name appears in a context where it is subject to assignment conversion or method invocation conversion or casting conversion, then the type of the expression name is the declared type of the field, local variable, or parameter after capture conversion (§5.1.10).

In your case, the "expression name" is the identifier clazz. As the compiler output shows, it is being captured twice, as required by the JLS.

The common technique for dealing with this is to introduce a helper method that binds the wildcard to a type variable:

private static <T> void registerTransient(Class<T> serviceAndImplClass)
{
    registerTransient(serviceAndImplClass, serviceAndImplClass);
}

Calling this new method with a wildcard will work:

Class<?> clazz = Class.forName("com.acme.components.MyPersonalImplementation");
registerTransient(clazz);

I see that you mentioned this workaround in a comment. It may seem strange, but it's actually the approach intended by the language designers.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文