试验 Java 泛型

Experimenting with java generics

提问人:SSH 提问时间:1/8/2023 更新时间:11/16/2023 访问量:78

问:

我正在玩 java 泛型,我遇到了这段代码,我很困惑为什么会这样。

我正在将我的第二个参数作为传递,并且在泛型方法中,我正在将其转换为我的类型,并且在我接收它时,KIntegerfloatKmain()Integer

在我的代码检查器中,我看到数字完全位于我的列表中(在转换为 后没有被切碎),这是类型的,但是当我尝试选择元素以将其保存在变量中时,它给出了 .FloatIntegerIntegerIntegerClassCastException

有人可以解释泛型出了什么问题,所以它不能使我们免于强制转换异常。

注意:当我从签名中删除第二个参数时,我达到了这种情况,因此不会有任何定义类型,在这种情况下,我认为 Java 会这样做,然后我们可能会遇到强制转换异常,但为什么在这种情况下我也传递类型。KKObjectK

import java.util.ArrayList;
import java.util.List;

public class IntegerPrinter {

    Integer item;
    
    public void  print() {
        System.out.println(item);
    }
    
    public <T,K> List<K>  anyPrint(List<T> num,K lo) {
        List<K> mylist = new ArrayList<>();
        mylist.add( (K) new Float(2.99f));
        return mylist;
    }

    public  IntegerPrinter(Integer item) {
        this.item = item;
    }
}
import java.util.ArrayList;
import java.util.List;

public class GenericsInAction {

    public static void main(String[] args) {
        IntegerPrinter oldPrinter = new IntegerPrinter(188);
        oldPrinter.print();
        List<Integer> dates = oldPrinter.anyPrint(new ArrayList<Integer>(),7);
        Integer x = dates.get(0);
        
        
    }
}
Java 泛型 转换

评论

1赞 Sweeper 1/8/2023
“但为什么在这种情况下,当我也传递 K 类型时”因为泛型在运行时并不重要。他们被完全忽略了。情况与您删除 .泛型只会添加更多的编译时检查。K
1赞 Turing85 1/8/2023
@Sweeper“奇怪”的是,JLS声明“编译器必须确保检查,但仅在必要时”。这可能会导致一些奇怪的情况。
0赞 SSH 1/8/2023
@Sweeper说实话,我不明白你说的,你能详细说明一下,以便我更好地理解它吗?

答:

2赞 Turing85 1/8/2023 #1

我将代码压缩到基本部分,并对其进行了轻微修改,以突出重要的行为:

class Ideone {
  public static void main(String[] args) {
    List<Integer> dates = new IntegerPrinter().anyPrint(7);
    System.out.println(dates.get(0)); // succeeds
    Integer x = dates.get(0);         // Line 8, throws
  }
}

class IntegerPrinter {
  public <K> List<K> anyPrint(K lo) {
    List<K> mylist = new ArrayList<>();
    mylist.add((K) Float.valueOf(2.99f));
    return mylist;
  }
}

执行时,此程序将产生以下输出:

2.99
Exception in thread "main" java.lang.ClassCastException: class java.lang.Float cannot be cast to class java.lang.Integer (java.lang.Float and java.lang.Integer are in module java.base of loader 'bootstrap')
    at Ideone.main(Main.java:8)

Ideone.com demo

现在,让我们逐步执行代码并尝试了解发生了什么。

这一行:

mylist.add((K) new Float(2.99f));

基本上告诉编译器“不要关心类型,我们(作为程序员)保证它是一个,把它踩成一个”。KK

然后,如果我们更深入地挖掘,我们会看到 ArrayList 使用 Object[] 作为后备数据结构。所以这里没有问题,背衬可以存储一切。Object[] elementData

当我们开始检索元素时,事情变得很奇怪。在这些情况下,JLS 对类型断言有些含糊不清(我认为它们包含在 §5.1.5 和 §5.1.6.3 中,但我不完全确定)。它基本上是说“编译器必须断言类型,但仅在必要时”。

因此,如果我们从我们的 中检索一个元素,它显然不是一个 ,而是传递给一个可以处理的方法,不需要类型断言。这里的情况正是如此:List<Integer>IntegerObject

System.out.println(dates.get(0));

最接近的签名匹配是 println(Object) 方法。这就是 JLS §5.1.5 中的情况:扩大转换,它永远不会抛出。System.out

另一方面,如果我们现在尝试检索一个并尝试将其存储在 :IntegerInteger

Integer x = dates.get(0);

现在,类型检查已经到位。事实上,如果我们检查程序的输出,我们会看到发生了,但对 -variable 的赋值是触发 .这是 JLS §5.1.6.3 中描述的情况:运行时的缩小转换(来自 的 elementData(int) 方法)。System.out.println(...)IntegerClassCastExceptionArrayList


脚注

泛型无疑是 JLS 中最复杂和最令人困惑的部分之一。我尽了最大努力引用 JLS 的相关部分,这可能是被错误引用的。我也知道这个问题以前被问过,但我找不到重复的。

评论

1赞 Sweeper 1/8/2023
我不认为“仅在必要时”是“含糊不清”的,至少在这种特殊情况下是这样。既然你要赋值给一个类型的变量,为什么没有必要将结果强制转换为 ?是的,我相信这也是一个重复的 - 我记得我自己回答过非常相似的事情 - 但就是找不到它!:)dates.get(0)IntegerInteger
0赞 Turing85 1/8/2023
@Sweeper我想找到我写的副本,因为我知道编译器在放置断言的位置方面有一定的自由。我知道我已经看到堆栈跟踪中包含意外的行号。必须进行检查不是“含糊不清”的,“哪里”(有点)含糊不清。
0赞 Sweeper 1/8/2023
真?我真的很想看到这样的案例。希望你找到骗子!
0赞 Turing85 1/8/2023
@Sweeper我认为我能够重建那个让我失望的例子(Ideone.com)。当时让我失望的是,异常发生在在线上,而不是在线上。今天我明白为什么了......814
0赞 Sean F 1/8/2023 #2

由于是泛型类型,其类型擦除为 ,即存储在列表中的内容。您可以将类型擦除视为运行时类型,即“实际”类型,而不是编译器知道的编译时类型。任何类型都可以存储在程序运行时。ArrayListjava.lang.ObjectArrayList

碰巧 K in 中的类型擦除也是 ,因为你对类型 K 没有限制。该方法对所有用法编译一次,并且必须能够接受 K 的任何类型。因此,当编译代码 for 时,将忽略行中对 K 的强制转换,因为 K 的类型擦除为 .投射到是无用和毫无意义的。它编译为 并且代码将类型的对象插入到类型的列表中。anyPrintjava.lang.ObjectanyPrintmylist.add( (K) new Float(2.99f));java.lang.Objectjava.lang.Objectmylist.add(new Float(2.99f));java.lang.Floatjava.lang.Object

此外,Java 中对对象类型的强制转换只是确保对象具有正确的类型,它不会像对原始类型那样更改对象的值。因此,您没有理由相信 2.99f 的值可能会发生变化。

GenericsInAction单独编译。

K 的参数化类型在 的方法中,因为您传入了 ,它通过自动装箱转换为,以便与 in 的类型擦除兼容。因此,当编译该方法时,编译器会在调用 之后立即插入一个运行时检查,即检查转换,该检查确保对 dates 的调用返回一个 类型的对象,因为 K 的类型必须在 里面。java.lang.IntegermainGenericsInAction7java.lang.Integerjava.lang.ObjectanyPrintmaindates.getdates.get(0);java.lang.Integerjava.lang.Integermain

由于您在列表中插入了 ,因此该运行时检查失败并抛出 .java.lang.FloatClassCastException