【问题标题】:Dynamic type check matching type parameters动态类型检查匹配类型参数
【发布时间】:2021-01-15 22:35:03
【问题描述】:

我希望实现一个具有以下签名的方法:

<T> void function(Class<T> clazz, Consumer<T> consumer)

这个函数接受Object obj(在别处提供),使用clazz.cast(obj) 将其转换为T,然后用它调用consumer。 (基本上就是consumer的守卫。)

这工作或多或少都很好,我可以写

Consumer<String> c = ...;
function(String.class, c);

很遗憾,以下代码不起作用:

Consumer<List<String>> c = ...;
function(List.class, c);

这是因为 List.class 的类型为 Class&lt;List&gt;,而不是类 Class&lt;List&lt;String&gt;&gt;。 我可以将c 的类型更改为Consumer&lt;List&gt;,但这与我在其他地方的代码不兼容(而且原始类型也不好)。

是否有某种方法可以 (a) 以类型检查的方式调用 function,或 (b) 以使该模式起作用的方式更改 function 的类型签名?

注意事项:

  • 我希望尽可能多地保留编译和运行时类型的安全性。因此,function(Class&lt;?&gt; clazz, Consumer&lt;T&gt; consumer) 是不可接受的,因为用户很容易提供错误的clazz。并且调用为function((Class&lt;List&lt;String&gt;&gt;)List.class, c) 也是不可接受的,因为我可能会不小心将其转换为function((Class&lt;Integer&gt;)List.class, c) 和 这个错误的演员阵容甚至不会在运行时被发现。如果有一个函数允许将Class&lt;C&lt;T&gt;&gt; 转换为Class&lt;C&lt;U&gt;&gt; 但不能转换为Class&lt;D&gt;,那会很酷,但如果没有高阶类型,我看不到这一点。

  • 我知道类型转换将无法在运行时检查我们有 List&lt;String&gt; 而不是 List&lt;Integer&gt;。没关系。 (嗯,不是,但我认为这是最好的 我们可以期待 JVM 语言。)

  • 尽管如此,我更喜欢使function 的调用变得简单(可能使function 更复杂)的解决方案,因为function 在我的库中定义并由该库的用户使用。

  • 出于好奇,我遇到此问题的实际代码是hereInstancefunction

【问题讨论】:

    标签: java generics types casting


    【解决方案1】:

    我认为不可能达到您所期望的类型安全程度。

    让我们区分编译时间和运行时间。

    编译时间

    编译时,编译器会检查clazz的类型是否匹配consumer的类型参数T。但这不适用于本身具有泛型类型参数的类型 T,例如List&lt;String&gt;,因为 Java 中没有 List&lt;String&gt;.class 语法。所以,在这里你能做的最好的就是将List.class 转换为Class&lt;List&lt;String&gt;&gt;

    在您的function 中操作的对象obj 在方法的签名中不可见,因此在编译时没有机会检查其类型是否与consumer 匹配。

    没有什么能阻止您调用function(String.class, stringConsumer) 并拥有Integer 对象。编译器不可能检测到这个错误。

    运行时间

    在运行时,没有泛型信息。该概念称为“类型擦除”。所以,在编译时显示为List&lt;String&gt;,在运行时只是List

    在编译时,objfunction 调用中不可见,您将类型检查移至运行时,使用 Class.cast() 方法。

    如果在编译时您的objList&lt;String&gt;List&lt;Integer&gt;List&lt;Whatever&gt;,则在运行时它只是一个没有参数的原始List,并且您要检查的类也是只是原始的List.class,没有任何类型参数。 cast() 方法将接受所有这些不同的列表。

    因此,在运行时,无法拒绝 List&lt;Integer&gt; 进入期望 List&lt;String&gt; 的消费者。稍后当消费者访问列表元素时,您可能会收到ClassCastException

    总结

    如果你想防止参数化类型的不匹配,你只能在编译时这样做,并且为了实现这一点,你必须将对象和使用者置于相同的语法上下文中,这可能意味着一个大的重构。

    在运行时,您只能防止原始类型不匹配。如果你想这样做,它会迫使你在参数化类型的情况下使用一些丑陋的语法(将类对象转换为参数化类类型),以满足编译器的要求。

    就我个人而言,我会放弃守卫的想法。

    • 它在编译时没有帮助(它只检查您是否能够将匹配 consumerclazz 传递给 function,而不是实际对象是否匹配)。

    • 它可以在运行时检查一些错误情况。但在像List&lt;String&gt; 这样的情况下,它仍然无法保护您免于稍后收到消费者抛出的ClassCastException。没有你的保护也会发生这种情况,只是在更多的情况下。

    【讨论】:

    • 我相信存在误解。根据我的要求(注释中的第二个项目符号),我可以(运行时)只检查 List 部分,而不是 List&lt;String&gt; 部分。这很容易实现(给定List.class)。但是正确键入函数以便我可以传递Consumer&lt;List&lt;String&gt;&gt; 而不会出现编译器错误是困难的部分(即使我不关心String 部分的安全性)。天真的解决方案只允许我通过Consumer&lt;List&gt;。 (那是因为List.class 的类型为Class&lt;List&gt;;不进行强制转换就不可能得到Class&lt;List&lt;String&gt;&gt;
    【解决方案2】:

    仅使用Class&lt;T&gt; 参数是不可能做你想做的事的。你需要一个super type token

    如链接文章中所述,您可以通过以下方式实现此类功能:

    public abstract class TypeToken<T> {
     
        private final Type type;
     
        public TypeToken() {
            Type superclass = getClass().getGenericSuperclass();
            type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
        }
     
        public Type getType() {
            return type;
        }
    }
    

    在 Java 中,您无法获取有关实例的泛型类型变量的信息,但是您可以获取有关超类型的泛型类型变量的一些信息。请注意,TypeToken 类是抽象的。这就是您应该在示例中使用它的方式:

    <T> void function(TypeToken<T> token, Consumer<T> consumer)
    
    Consumer<List<String>> c = ...;
    
    function(new TypeToken<List<String>>() { }, c);
    

    这意味着您将TypeToken 的子类传递给您的函数。

    最后,在function 中,您可以提取Class 实例并按如下方式强制转换对象:

    Class<T> clazz;
    if (type instanceof ParameterizedType)
      clazz = (Class<T>) ((ParameterizedType) type).getRawType();
    else if (typeU instanceof Class)
      clazz = (Class<T>) type;
    else
      throw new RuntimeException("Error handling omitted");
    T t = clazz.cast(obj);
    

    【讨论】:

    • 不错的解决方案,甚至认为“新”和“{}”使语法有点笨拙。轻微优化:通过将function 定义为类而不是方法(abstract class function&lt;T&gt; 和构造函数function(Consumer&lt;T&gt;)),我们有更紧凑的调用:new function&lt;T&gt;(c){} 而不是function(new TypeToken&lt;T&gt;{},c)
    • TypeTag 的一个误用是从 TypeTag 的子类继承。 (那么getGenericSuperclass 会给出错误的Type。)为了避免这种情况,我们可以在构造函数中添加一个额外的检查:if (getClass().getSuperclass() != TypeTag.class) throw ...。不过,我不是 100% 确定 != 语义在这里是正确的。同一个类可以有两个不同的Class对象吗?
    • tryCast 如果T 本身是参数化类型,则上述的tryCast 不起作用,因为type 不是Class 的实例。所以我们需要额外检查是否type instanceOf ParametrizedType,如果是,则使用type.getRawType()获取实际的Class实例。
    • 当然,我并不是说答案缺少某些东西,只是根据您的有用答案添加了一些额外的见解,我认为这些见解对其他人有用。我将添加如何使用 type 标签进行投射。
    • 1+。原来我在玩same thing a while agoType 可以提供更多类型
    猜你喜欢
    • 1970-01-01
    • 2017-01-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-14
    相关资源
    最近更新 更多