如何将此对象转换为通用类型?

时间:2018-11-16 12:00:40

标签: java generics casting effective-java

我的理解是通用类型是不变,因此,如果我们将B作为A的子类型,那么List<B>与{{1 }}。因此,投射无法在List<A>List<A>上进行。

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

List<B>

在这里我很困惑。我们将类型为// 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)); } } 的{​​{1}}强制转换为IDENTIFY_FN,后者具有另一个类型参数。

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

Object和T是否有某种联系?在这种情况下,铸造如何成功?

4 个答案:

答案 0 :(得分:4)

此强制转换可以编译,因为这是缩小转换的特殊情况。 (根据§5.5,缩小转换是强制转换允许的转换类型之一,因此,大多数答案将集中在缩小转换的规则上。)

请注意,尽管UnaryOperator<T>不是UnaryOperator<Object>的子类型(因此,转换不是“向下转换”),但仍被认为是缩小转换。来自§5.6.1

  

缩小引用转换将引用类型S的表达式视为其他引用类型T的表达式,其中S不是{的子类型{1}}。 [...]与扩展引用转换不同,类型不必直接相关。但是,如果可以静态证明没有值可以同时属于两种类型,则存在某些限制禁止在某些类型对之间进行转换。

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

T

具体来说,这由§5.1.6.1中的一条规则给出,该规则指出:

  
      
  • 如果存在作为List<String> a = ...; List<Double> b = (List<String>) a; 的超类型的参数化类型X和作为T的超类型的参数化类型Y,则使得SX的擦除是相同的,则YX并没有明显的区别(§4.5)。

         

    Y包中的类型为例,从类型{{1}到类型java.util,从ArrayList<String>ArrayList<Object>都没有缩小的引用转换,反之亦然。 }和String有区别。出于相同的原因,从ObjectArrayList<String>的收窄引用转换不存在,反之亦然。拒绝可证明的不同类型是一个简单的静态门,可以防止“愚蠢”的引用转换变窄。

  •   

换句话说,如果List<Object>a具有相同的擦除且具有相同的超类型(在这种情况下,例如b),则它们必须是JLS由§4.5给予“明显不同”的称呼:

  

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

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

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

  •   

还有§4.5.1

  

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

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

  •   
  • 一个类型参数是类型变量或通配符,其上限(如有必要,来自捕获转换)为List;另一个类型参数S不是类型变量或通配符;而不是T|S| <: |T|

  •   
  • 每个类型参数都是类型变量或通配符,其上限(如有必要,来自捕获转换)为|T| <: |S|S;而不是T|S| <: |T|

  •   

因此,鉴于上述规则,|T| <: |S|List<String> 可证明是有区别的(通过4.5.1中的第一条规则),因为List<Double>和{ {1}}是不同类型的参数。

然而,StringDouble并不是可证明是不同的(通过4.5.1中的第二条规则),因为:

  1. 一个类型参数是类型变量({UnaryOperator<T>,其边界为UnaryOperator<Object>。)

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

由于ObjectObject并没有明显的区别,因此允许缩小转换,因此强制类型转换。


思考为什么编译器允许某些强制转换而不允许其他强制转换的一种方法是:对于类型变量,它无法证明UnaryOperator<T>肯定不是 UnaryOperator<Object>。例如,我们可能会遇到这样的情况:

T

在这种情况下,只要其他人都没有做任何有趣的事情(例如,未经检查的强制Object强制转换),我们实际上就知道强制转换是正确的。

因此,在强制转换为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>强制转换为UnaryOperator<T>的情况下,我们可以很权威地说这总是错误的。

答案 1 :(得分:3)

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成功,因为标识函数返回的参数未经修改。

答案 2 :(得分:2)

泛型在运行时不存在。在运行时,每个 UnaryOperator<T>UnaryOperator<Object>。强制转换是在编译时放置编译器所必需的。在运行时没有意义。

答案 3 :(得分:1)

演员

return (UnaryOperator<T>) IDENTIFY_FN;

基本上相当于对原始类型UnaryOperator的转换,因为T在运行时被擦除,在编译时出于转换的目的而被忽略。您可以 将通用类型转换为原始类型(出于向后兼容的原因),但是您应该收到“未经检查”的警告。

这也可以,例如:

UnaryOperator<String> foo = (UnaryOperator) IDENTITY_FN;