java - 如何封装未经检查的强制转换以仅允许未经检查的泛型类型转换?

标签 java generics language-lawyer compile-time

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

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

作为一个理论示例,假设我想保留 Class 的 map 至Supplier这样对于每个 keyClass ,其关联valueSupplier生成扩展 keyClass 的对象.

编辑 2:我从 Class 的 map 更改了示例至Class ,到 Class 的 map 至Supplier ,因为(值)Class对象对于强制转换而言是特殊的,原始示例有另一个不涉及未经检查的强制转换的解决方案(感谢@Holger)。再次强调,我只是添加一个示例来说明问题,我不需要解决任何特定的示例。

编辑 1:更准确地说,是单个 SupplierMap对象是从配置文件填充的,并保存诸如“实现接口(interface) I1 的对象由供应商 S1 提供”、“I2 由 S2 提供”等信息。在运行时,我们会收到诸如 I1 i1 = supplierMap.get(I1.class).get() 之类的调用它应该生成一个带有 i1.getClass() == C1.class 的对象。我对修复/快捷方式不感兴趣,例如将强制转换移动到不属于的位置,例如 get()返回Supplier<Object> 。从概念上讲, Actor 阵容属于 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);
}

编辑:您在公共(public)库中见过这种未经检查的 Actor (甚至是上面的全能 Actor )吗?我查看了 Commons Lang 和 Guava,但我唯一能找到的是 Chronicle Core 的 ObjectUtils.convertTo() :那里,路过eClass == null相当于全能的uncheckedCast ,除了它还会产生不需要的 @Nullable (由其他分支使用)。

最佳答案

你说

Also, I don't much care about this specific example, but about the general language problem.

但实际上,此类问题应该始终结合您要解决的实际问题来处理。我想说的是,这种情况很少发生,因此无论实际用例如何,进行未经检查的强制转换的通用实用方法都是不合理的。

考虑到SupplierMap,你说

I don't believe there is a way to capture the key-value generic parameter relation in Java so that the get() does not involve an unchecked compile-time cast.

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

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;
    }
}

这里,未经检查的类型转换位于执行实际检查的方法内,使读者对操作的正确性充满信心。

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

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

请注意,这可以扩展到完全可以工作的示例,而无需使用 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;
    }
}

允许多个相同类型的 key :

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);
}

关键的部分是,现在不可避免的未经检查的强制转换仍然通过对 key 有效性的实际检查来增强。如果没有,我绝对不会允许这样做。

这并不排除您根本无法检查正确性的情况。但是,最好认为像 @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 方法填充了 map ,没有任何未经检查的操作,则此测试永远不会失败。

唯一不同的是对 null 的处理,例如当 key 不在 map 中时。与强制转换不同,这会引发异常,因为它是方法调用。

因此,如果允许使用不存在的键调用此方法并且会导致 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);
    }
}
<小时/>

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

关于java - 如何封装未经检查的强制转换以仅允许未经检查的泛型类型转换?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57899235/

相关文章:

java - 尝试嵌入 a-href 时出现 JSP 语法错误

java - 检查第一个字符串是否小于或等于第二个字符串中的对应字母

c# - 将类型限制为 C# 中泛型类型的任何实例

c# - 为什么通用类型在 CIL 中的函数定义处看起来像 (!!T)

opengl - 可以在片段着色器中声明一个超大的输出数组并保留一些未使用的索引吗?

c++ - 内联 constexpr 函数定义是否合法? gcc (ok) vs clang (error)

java - 如何在 Java 中以编程方式合并 EMF 模型?

java - Hibernate ManyToOne,连接表中的重复键

Typescript 通用类型检查无法按预期工作

c++ - 下标时是否必须使用 constexpr 数组?