使用与从通用集合中定义的对象不同的对象

Consuming different objects than defined from a generic collection

我昨天在回答 问题时发现了一些很奇怪的事情。

我可以将集合定义为 O 类型,即使它的实际类型是 T。 这可能是因为我使用的是原始类型。

然而,最令人惊讶的部分是我能够从这个 List<FileInputStream> 的集合中消费,尽管很明显它是 List<Integer>.

下面是有问题的代码

public static void main(String[] args) {
    String value = "a string with numb3r5";
    Function<String, List<String>> fn1 = List::of;
    Function<List<String>, String> fn2 = x -> x.get(0);
    Function<String, List<Integer>> fn3 = x -> List.of(x.length());

    InputConverter<String> converter = new InputConverter<>(value);
    List<FileInputStream> ints = converter.convertBy(fn1, fn2, fn3);

    System.out.println("ints = " + ints);
    System.out.println("ints.get(0) = " + ints.get(0));
    System.out.println("ints.get(0).getClass() = " + ints.get(0).getClass());
}

public static class InputConverter<T> {
    private final T src;

    public InputConverter(T src) {
        this.src = src;
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    public <R> R convertBy(Function... functions) {
        Function functionsChain = Function.identity();

        for (Function function : functions) {
            functionsChain = functionsChain.andThen(function);
        }

        return (R) functionsChain.apply(src);
    }
}

这是结果

ints = [21]
ints.get(0) = 21
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.io.FileInputStream (java.lang.Integer and java.io.FileInputStream are in module java.base of loader 'bootstrap')
    at Scratch.main(scratch_8.java:18)

为什么可以从这个集合中消费?

我注意到 follwing 在使用它时不会抛出异常

System.out.println("String.valueOf(ints.get(0)) = " + String.valueOf(ints.get(0)));
System.out.println("((Object) ints.get(0)).toString() = " + ((Object) ints.get(0)).toString());

但是以下情况

System.out.println("ints.get(0).toString() = " + ints.get(0).toString());

泛型不是 java 类 的二进制表示的一部分。泛型是通过强制转换实现的。

“Behind the scenes, the compiler replaces all references to T in Crate with Object. In other words, after the code compiles, your generics are actually just Object types. ” Excerpt From: Jeanne Boyarsky. “OCP Oracle Certified Professional Java SE 11 Developer Complete Study Guide”. Apple Books.

这意味着编译器会确保 convertBy return 列表,但无法确认所有类型。但是编译器会替换: ints.get(0).toString()((FileInputStream) ints.get(0)).toString()) 以备不时之需。

一个类似的例子,使用错误的通用参数的转换列表将产生相同的异常。

    List list = List.of("text");
    List<Integer> numbers = (List<Integer>) list;
    System.out.println(numbers.get(0).longValue());

结论: 如果我们想要类型安全的代码,我们必须确保我们不使用原始类型。

InputConverter#convertBy 使用原始类型 Function 声明作为其输入,其 return 类型使用泛型类型推断 <R> 因此它将解析为分配的类型,在你的情况下它将是 List<FileInputStream>.

也就是说,您可以将转换后的类型,即 InputConverter#convertBy 的结果分配给您想要的任何类型:List<FileInputStream>List<HashSet<FileInputStream>>List<Charset>.. . 只要它与转换类型的结果(最后一个函数结果)在运行时兼容,您仍然不会出错,直到在运行时,JVM 将执行 implicit cast在您的情况下,收集擦除类型是 FileInputStream 并且会因为实际类型是 Integer.

而失败

下面的语句不会失败,因为您将集合元素向上转换为 Object 超类型并从 java.lang.Object 类型调用 #toString 方法(您是在访问任何成员/方法之前转换变量):

System.out.println("((Object) ints.get(0)).toString() = " + ((Object) ints.get(0)).toString());

下面的语句实际上与前面的语句相似:

System.out.println("String.valueOf(ints.get(0)) = " + String.valueOf(ints.get(0)));

给定的Object#valueOf方法实现如下:

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

另一方面,下面的语句将失败,因为 ints.get(0) 在访问其 #toString 成员之前将被强制转换(checkcast)为已擦除的运行时类型 FileInputStream

System.out.println("ints.get(0).toString() = " + ints.get(0).toString());

你可以这样想:

System.out.println("ints.get(0).toString() = " + ((FileInputStream) ints.get(0)).toString());