如何将此对象强制转换为泛型类型?

2022-09-04 22:02:11

我的理解是泛型类型是不变的,所以如果我们有作为子类型的,那么与没有关系。因此,在 和 上转换不起作用。BAList<B>List<A>List<A>List<B>

从有效的Java第三版中,我们有以下代码片段:

// Generic singleton factory pattern
private static UnaryOperator<Object> IDENTIFY_FN = (t) -> t;

@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identifyFunction() {
    return (UnaryOperator<T>) IDENTIFY_FN; //OK But how, why?
}

public static void main(String[] args) {
    String[] strings = {"a", "b", "c"};
    UnaryOperator<String> sameString = identifyFunction();
    for (String s : strings) {
        System.out.println(sameString.apply(s));
    }
}

在这里,我很困惑。我们有 cast ,其类型是 , 到 ,它有另一个类型参数。IDENTIFY_FNUnaryOperator<Object>UnaryOperator<T>

当类型擦除发生时,字符串是Object的子类型,但据我所知不是的子类型。UnaryOperator<String>UnaryOperator<Object>

对象和 T 是否以某种方式相关?在这种情况下,铸造如何成功?


答案 1

此强制转换会编译,因为它是缩小转换范围的特殊情况。(根据 §5.5,缩小转换范围是强制转换允许的转换类型之一,因此此答案的大部分内容将侧重于缩小转换范围的规则。

请注意,虽然它不是 的子类型(因此投射不是“向下转换”),但它仍然被认为是一个缩小的转换。根据 §5.6.1UnaryOperator<T>UnaryOperator<Object>

缩小引用范围的转换将引用类型的表达式视为其他引用类型的表达式,其中 不是 的子类型。[...]与加宽引用转换不同,类型不需要直接相关。但是,当可以静态证明没有值可以同时属于这两种类型时,存在禁止在某些类型对之间进行转换的限制。STST

其中一些“横向”转换由于特殊规则而失败,例如以下将失败:

List<String> a = ...;
List<Double> b = (List<String>) a;

具体而言,这是由§5.1.6.1中的规则给出的,该规则指出:

  • 如果存在一个参数化类型,该类型是 的超类型,而参数化类型是 的超类型,使得 和 的擦除是相同的,则 并且不能证明是不同的 (§4.5)。XTYSXYXY

    java.util 包中的类型为例,不存在从 ArrayList<String> 到 ArrayList<Object> 或反之亦然的缩小引用转换,因为类型参数 StringObject 是可证明不同的。出于同样的原因,不存在从 ArrayList<String>List<Object> 或反之亦然的缩小引用转换。拒绝可证明不同的类型是一个简单的静态门,以防止“愚蠢”的缩小参考转换。

换句话说,如果 并且具有具有相同擦除的常见超类型(例如,在这种情况下),则它们必须是 JLS 所称的“可证明不同”,由 §4.5 给出:abList

如果满足以下任一条件,则两个参数化类型可证明是不同的:

  • 它们是不同泛型类型声明的参数化。

  • 它们的任何类型参数都可以证明是不同的。

和 §4.5.1

如果满足以下条件之一,则两个类型参数可证明是不同的

  • 这两个参数都不是类型变量或通配符,并且这两个参数不是同一类型。

  • 一个类型参数是类型变量或通配符,其上限(如有必要,从捕获转换)为 ;并且另一个类型参数不是类型变量或通配符;和 既不也不是 。ST|S| <: |T||T| <: |S|

  • 每个类型参数都是一个类型变量或通配符,其上限(如有必要,从捕获转换开始)为 和 ;和 既不也不是 。ST|S| <: |T||T| <: |S|

因此,给定上述规则,并且可以证明不同的(通过4.5.1的第一条规则),因为和是不同的类型参数。List<String>List<Double>StringDouble

但是,并且不能证明是不同的(通过4.5.1的第二条规则),因为:UnaryOperator<T>UnaryOperator<Object>

  1. 一个类型参数是类型变量(上限为 .)TObject

  2. 该类型变量的边界与另一个类型 () 的类型参数相同。Object

由于 和 不能证明是不同的,因此允许缩小转换,因此强制转换编译。UnaryOperator<T>UnaryOperator<Object>


思考为什么编译器允许其中一些强制转换而不允许其他强制转换的一种方法是:在类型变量的情况下,它不能证明绝对不是 。例如,我们可能会遇到这样的情况:TObject

UnaryOperator<String> aStringThing = Somewhere::doStringThing;
UnaryOperator<Double> aDoubleThing = Somewhere::doDoubleThing;

<T> UnaryOperator<T> getThing(Class<T> t) {
    if (t == String.class)
        return (UnaryOperator<T>) aStringThing;
    if (t == Double.class)
        return (UnaryOperator<T>) aDoubleThing;
    return null;
}

在这些情况下,我们实际上知道演员阵容是正确的,只要没有其他人在做一些有趣的事情(比如不受检查的演员)。Class<T>

因此,在一般情况下,我们可能实际上正在做一些合法的事情。相比之下,在 cast to 的情况下,我们可以非常权威地说它总是错误的。UnaryOperator<T>List<String>List<Double>


答案 2

JLS 允许这样的转换:

除非至少满足以下条件之一,否则将取消选中从类型 S 到参数化类型 T 的强制转换:

  • S <: T

  • T 的所有类型参数都是无界通配符。

  • [ ... ]

因此,除非被注释禁止显示,否则未经检查的强制转换会导致编译时未选中警告。SuppressWarnings

此外,在类型擦除过程中,并编译为:identifyFunctionIDENTIFY_FN

private static UnaryOperator IDENTIFY_FN;

public static UnaryOperator identifyFunction() {
    return IDENTIFY_FN; // cast is removed
}

,并添加到调用站点:checkcast

System.out.println(sameString.apply(s));
                         ^
INVOKEINTERFACE java/util/function/UnaryOperator.apply (Ljava/lang/Object)Ljava/lang/Object
CHECKCAST java/lang/String
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V

checkcast成功,因为标识函数返回其未修改的参数。


推荐