【问题标题】:How to encapsulate an unchecked cast to only allow unchecked generic type conversion?如何封装未经检查的强制转换以仅允许未经检查的泛型类型转换?
【发布时间】:2019-09-12 03:15:10
【问题描述】:

我正在寻找一种“适当”的方法来减少在编译时检索/修改泛型类型参数所涉及的 Java 样板。通常,此样板文件涉及:

  • 使用@SuppressWarnings("unchecked")
  • 明确说明目标泛型类型参数。
  • 通常,创建一个原本无用的局部变量,以便仅将抑制应用于该状态。

作为一个理论上的示例,假设我想保留ClassSupplier 的映射,这样对于每个keyClass,其关联的valueSupplier 会产生扩展keyClass 的对象。

编辑 2:我将示例从Class 的映射更改为Class,将Class 的映射更改为Supplier,因为(值)Class 对象对于演员表来说是特殊的,原始示例有另一个不涉及未经检查的演员表的解决方案(感谢@Holger)。同样,我只是添加一个例子来说明问题,我不需要解决任何特定的例子。

编辑 1:更准确地说,单个 SupplierMap 对象从配置文件中填充,并保存诸如“实现接口 I1 的对象由供应商 S1 提供”、“I2 由S2”,以此类推。在运行时,我们会收到诸如I1 i1 = supplierMap.get(I1.class).get() 之类的调用,它应该生成一个带有i1.getClass() == C1.class 的对象。我对修复/快捷方式不感兴趣,例如将演员移到不属于它的位置,例如让get() 返回Supplier<Object>。演员阵容在概念上属于SupplierMap。另外,我不太关心这个具体的例子,而是关心一般的语言问题。

对于SupplierMap,我不相信有一种方法可以在Java 中捕获键值通用参数关系,从而使get() 不涉及未经检查的编译时强制转换。具体来说,我可以:

class SupplierMap {

    // no way to say in Java that keys and values are related
    Map<Class<?>, Supplier<?>> map;

    // can check the relation at compile time for put()
    <T> void put(Class<T> keyClass, Supplier<? extends T> valueSupplier) {
        map.put(keyClass, valueSupplier);
    }

    // but not for get()
    <T> Supplier<? extends T> get(Class<T> keyClass) {
        @SuppressWarnings("unchecked")
        final Supplier<? extends T> castValueSupplier = (Supplier<? extends T>) map.get(keyClass);
        return castValueSupplier;
    }
}

作为替代方案,您可以:

@SupressWarnings("unchecked")
<T> T uncheckedCast(Object o) {
    return (T) o;
}

<T> Supplier<? extends T> get(Class<T> keyClass) {
    return uncheckedCast(map.get(keyClass));
}

这看起来好多了,但问题是uncheckedCast 可以说是太强大了:它可能在编译时将任何东西转换为其他任何东西(通过隐藏警告)。在运行时我们仍然会得到 CCE,但这不是重点。 (论点是……)如果将这个uncheckedCast 放入库中,则该函数可能会被滥用以隐藏在编译时可检测到的问题。

有没有办法定义这样一个类似的未经检查的强制转换函数,以便编译器可以强制它只用于更改泛型类型参数?

我试过了:

// error at T<U>: T does not have type parameters
<T, U> T<U> uncheckedCast(T t) {
    return (T<U>) t;
}

还有

<T, U extends T> U uncheckedCast(T t) {
    return (U) t;
}

void test() {
    Class<?> aClass = String.class;
    // dumb thing to do, but illustrates cast error:
    // type U has incompatible bounds: Class<capture of ?> and Class<Integer>
    Class<Integer> iClass = uncheckedCast(aClass);
}

编辑:你有没有在公共库中看到过这种未经检查的演员表(甚至是上面的全能演员表)?我查看了 Commons Lang 和 Guava,但我唯一能找到的是 Chronicle Core 的ObjectUtils.convertTo():在那里,通过eClass == null 相当于全能的uncheckedCast,除了它还会产生一个不受欢迎的@Nullable(其他分支使用)。

【问题讨论】:

  • 如果您的类型不匹配,uncheckedCast 最终只会抛出运行时 CCE。让我们退后一步:你试图用这种方法解决什么问题?当然,这不仅仅是为了好玩而玩类型标记
  • "如果将这个 uncheckedCast 放入库中,该函数很容易被滥用。"人们可以滥用更多危险的东西。如果他们这样做,这不是你的问题或责任。 Here's 我最近回答的一个相关问题,场景几乎完全相同。
  • @Rogue:我想以“适当”的方式减少样板。你是对的,功能强大的uncheckedCast() 可以工作,但具有将一些错误从编译时移到运行时的负面影响。我正在寻找一种将uncheckedCast 限制为&lt;T,U such that T == rawType(U)&gt; U uncheckedCast(T t) 之类的语言的方法,这样编译时检查就不会丢失。
  • @Kayaman 我个人同意,我只是在这方面遇到了一些困难。这似乎是一个基本的问题,我很惊讶我在一个公共库中找不到它(即使是全能的演员)。我编辑了帖子,查看了 Commons Lang、Guava 和 Chronicle Core。
  • @MateiDavid 他们不太可能将其作为通用工具暴露在外部。那些未经检查的演员表是内联完成的,并且众所周知它们是安全的。必须与类型系统作斗争应该很少见,因此拥有一些 forceCast 功能是没有意义的。

标签: java generics language-lawyer compile-time


【解决方案1】:

你说

另外,我不太关心这个具体的例子,而是关心一般的语言问题。

但实际上,此类问题应始终根据您要解决的实际问题来处理。我会说这种情况很少发生,所以不管实际用例如何进行未经检查的强制转换的通用实用方法是不合理的。

考虑到SupplierMap,你说

我不相信有一种方法可以在 Java 中捕获键值通用参数关系,以便 get() 不涉及未经检查的编译时强制转换。

这就是解决问题并指出干净的解决方案。您必须在键和值之间创建关系,例如

class SupplierMap {
    static final class SupplierHolder<T> {
        final Class<T> keyClass;
        final Supplier<? extends T> valueSupplier;

        SupplierHolder(Class<T> keyClass, Supplier<? extends T> valueSupplier) {
            this.keyClass = keyClass;
            this.valueSupplier = valueSupplier;
        }
        @SuppressWarnings("unchecked") // does check inside the method
        <U> SupplierHolder<U> cast(Class<U> type) {
            if(type != keyClass) throw new ClassCastException();
            return (SupplierHolder<U>)this;
        }
    }

    Map<Class<?>, SupplierHolder<?>> map = new HashMap<>();

    <T> void put(Class<T> keyClass, Supplier<? extends T> valueSupplier) {
        map.put(keyClass, new SupplierHolder<>(keyClass, valueSupplier));
    }

    <T> Supplier<? extends T> get(Class<T> keyClass) {
        return map.get(keyClass).cast(keyClass).valueSupplier;
    }
}

这里,未经检查的类型转换位于执行实际检查的方法中,使读者可以确信操作的正确性。

这是实际代码中实际使用的模式。这有望解决您的问题“您是否在公共库中看到过这种未经检查的演员表(甚至是上面的全能演员表)?”。我不认为任何库都使用“完全未经检查”的方法,而是它们的方法具有尽可能窄的可见性,并且可能针对实际用例量身定制。

是的,这意味着“样板”。这对于开发人员在继续之前应该花费几秒钟的操作来说还不错。

请注意,这可以扩展到不使用 Class 作为令牌的完全工作的示例:

interface SomeKey<T> {}

class SupplierMap {
    static final class SupplierHolder<T> {
        final SomeKey<T> keyClass;
        final Supplier<? extends T> valueSupplier;

        SupplierHolder(SomeKey<T> keyToken, Supplier<? extends T> valueSupplier) {
            this.keyClass = keyToken;
            this.valueSupplier = valueSupplier;
        }
        @SuppressWarnings("unchecked") // does check inside the method
        <U> SupplierHolder<U> cast(SomeKey<U> type) {
            if(type != keyClass) throw new ClassCastException();
            return (SupplierHolder<U>)this;
        }
    }

    Map<SomeKey<?>, SupplierHolder<?>> map = new HashMap<>();

    <T> void put(SomeKey<T> keyClass, Supplier<? extends T> valueSupplier) {
        map.put(keyClass, new SupplierHolder<>(keyClass, valueSupplier));
    }

    <T> Supplier<? extends T> get(SomeKey<T> keyClass) {
        return map.get(keyClass).cast(keyClass).valueSupplier;
    }
}

允许多个相同类型的键:

enum MyStringKeys implements SomeKey<String> {
    SAY_HELLO, SAY_GOODBYE        
}
public static void main(String[] args) {
    SupplierMap m = new SupplierMap();
    m.put(MyStringKeys.SAY_HELLO, () -> "Guten Tag");
    m.put(MyStringKeys.SAY_GOODBYE, () -> "Auf Wiedersehen");
    System.out.println(m.get(MyStringKeys.SAY_HELLO).get());
    Supplier<? extends String> s = m.get(MyStringKeys.SAY_GOODBYE);
    String str = s.get();
    System.out.println(str);
}

关键部分是现在不可避免的未经检查的强制转换仍然增加了对密钥有效性的实际检查。如果没有,我绝不允许。

这并不排除您根本无法检查正确性的情况。但是,最好认为像@SuppressWarnings("unchecked") 注释这样的冗长工件在需要它的地方就表明了这一点。即使我们有可能将其使用限制为泛型类型,便利方法也只会隐藏仍然存在的问题。


对上一版问题的回答:

这实际上比你想象的要容易:

class ImplMap {
    Map<Class<?>, Class<?>> map;

    <T> void put(Class<T> keyClass, Class<? extends T> valueClass) {
        map.put(keyClass, valueClass);
    }

    <T> Class<? extends T> get(Class<T> keyClass) {
        final Class<?> implClass = map.get(keyClass);
        return implClass.asSubclass(keyClass);
    }
}

这不是未经检查的操作,因为方法asSubclass 真正检查implClass 类是否是keyClass 的子类。假设地图仅通过put 方法填充,没有任何未经检查的操作,此测试将永远不会失败。

唯一不同的是null的处理方式,例如当地图中不存在密钥时。与强制转换不同,这会引发异常,因为它是一个方法调用。

因此,如果允许在缺少键的情况下调用此方法并且应该导致null,则必须显式处理:

<T> Class<? extends T> get(Class<T> keyClass) {
    final Class<?> implClass = map.get(keyClass);
    return implClass == null? null: implClass.asSubclass(keyClass);
}

注意同样的方法

@SupressWarnings("unchecked")
<T> T uncheckedCast(Object o) {
    return (T) o;
}

如果您有Class 对象,则不需要,那么您可以在其上调用cast

例如,以下内容将是您的 ImplMap 类的有效补充:

<T> T getInstance(Class<T> keyClass) {
    try {
        return keyClass.cast(map.get(keyClass).getConstructor().newInstance());
    } catch (ReflectiveOperationException ex) {
        throw new IllegalStateException(ex);
    }
}

作为附加说明,通过配置文件注册接口实现听起来你应该看看ServiceLoader API 和底层机制。另请参阅 Java 教程的 Creating Extensible Applications 章节。

【讨论】:

  • 我知道Class 对象带有关于它们的泛型类型参数的信息,这使得它们对于强制转换来说是特殊的,但通常情况并非如此。您通常无法在运行时判断一个空的List&lt;?&gt; 是不是List&lt;Integer&gt;。我不只关心铸造Class-es。怎么样?一个SupplierMap 包装Map&lt;Class&lt;?&gt;,Supplier&lt;?&gt;&gt; 支持supplierMap.put(I1.class, C1::new)I1 i1 = supplierMap.get(I1.class).get()?我对这个或那个例子不感兴趣,但对根本问题不感兴趣。
  • 所以您建议返回Supplier&lt;?&gt; 并将演员表移到其他地方。正如我在 OP 中解释的(甚至在任何编辑之前),我认为演员在概念上属于 SupplierMap。换句话说:您不知道用户代码与Supplier 有关系。也许它会立即应用 get(),但也许它将 Supplier 提供给其他一些功能组合。
  • 对不起,我想我错误地删除了你的评论,我试图编辑我的。也许一个模组可以提供帮助?
  • 我真的希望get() 有以下签名&lt;T&gt; Supplier&lt;? extends T&gt; get(Class&lt;T&gt; keyClass)。为什么 - 因为键 Class 和值 Supplier 的泛型类型参数之间的连接属于 put() 并在 SupplierMap 期间强制执行。我不想返回Supplier&lt;?&gt;,迫使用户代码随时进行强制转换。这里担心的是编译时转换,它恢复了编译时类型安全,而不是运行时转换。
  • 哦,我明白了,您是在建议 return () -&gt; keyClass.cast(map.get(keyClass).get())(或保存查找到的 Supplier 以供以后使用,以免延迟查找)。好的,你能帮我举一个更好的例子吗,也许根本不涉及Class? :)
【解决方案2】:

你为什么不尝试这样的事情,而不是为函数单独声明类型,使类成为通用类,然后类处理 T 类型的 Class 实例。

class ImplMap<T> {

    // Values are already related here
    Map<Class<T>, Class<? extends T>> map;

    // Already compiler aware of the type.
    void put(Class<T> keyClass, Class<? extends T> valueClass) {
        map.put(keyClass, valueClass);
    }

    // Compiler already aware of the type just like with 'put'.
    Class<? extends T> get(Class<T> keyClass) {
        return map.get(keyClass);
    }
}

这不涉及未经检查的强制转换,因为类型关系已经用Map 声明定义,不需要SuppressWarning(编译器不会发出警告)。

尽管编译器会在调用putget 函数时警告unchecked 调用,但如果您没有在ImplMap 对象创建时定义类型,因为它根本不需要类型并且如果您定义了一个类型,您可以只将那种类型的键放入重复的映射中。

【讨论】:

  • 我的真正意思是这是一个语言问题。我对ImplMap 不感兴趣。但是让我们谈谈它。我想要的是一个单一的ImplMap 对象,它通过读取配置来填充,并保存诸如接口 I1 由 C2 类实现,I2 由 C2 实现等信息。所以在配置解析期间,你会得到implMap.put(I1.class, C1.class) ,在运行时你会得到I1 i1 = implMap.get(I1.class).newInstance(),结果是i1.getClass() == C1.class。我不确定您的解决方案是否适合。 T 这里是什么?您的 ImplMap&lt;T&gt; 对象之一中的地图不是仅限于一个键吗?
  • 我在 OP 中添加了 ImplMap 说明。
猜你喜欢
  • 1970-01-01
  • 2019-09-13
  • 2015-06-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-06-04
  • 1970-01-01
相关资源
最近更新 更多