理解 Java 泛型广告中的真实原则

Gag*_*ngh 6 java generics

我一直在尝试正确理解 Java 泛型。所以在这个探索中,我遇到了一个原则“广告中的真实原则”,我试图用简单的语言来理解这一点。

广告真理原则:数组的具体化类型必须是其静态类型擦除的子类型。

我已经编写了示例代码 .java 和 .class 文件如下。请查看代码并解释哪部分(在代码中)指定/指示上述语句的哪一部分。

我已经写了评论,我认为我不应该在这里写代码描述。

public class ClassA {

    //when used this method throws exception
    //java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
    public static <T> T[] toArray(Collection<T> collection) {

        //Array created here is of Object type
        T[] array = (T[]) new Object[collection.size()];

        int i = 0;
        for (T item : collection) {
            array[i++] = item;
        }
        return array;
    }

    //Working fine , no exception
    public static <T> T[] toArray(Collection<T> collection, T[] array) {
        if (array.length < collection.size()) {
            //Array created here is of correct intended type and not actually Object type
            //So I think , it inidicates "the reified type of an array" as type here lets say String[]
            // is subtype of Object[](the erasure ), so actually no problem
            array = (T[]) Array.newInstance(array.getClass().getComponentType(), collection.size());
        }

        int i = 0;
        for (T item : collection) {
            array[i++] = item;
        }
        return array;
    }

    public static void main(String[] args) {
        List<String> list = Arrays.asList("A", "B");
        String[] strings = toArray(list);
//        String[] strings = toArray(list,new String[]{});
        System.out.println(strings);
    }
}
Run Code Online (Sandbox Code Playgroud)

请尽量用通俗的语言解释。请指出我错的地方。带有更多注释的更正代码表示赞赏。

谢谢你们

iov*_*uio 9

我将 Java 泛型和集合称为本书,本书的作者称为作者。

我会不止一次支持这个问题,因为这本书在解释 IMO 的原则方面做得很差。

陈述

广告真理原则: 数组的具体化类型必须是其静态类型擦除的子类型

进一步称为原则。

原理有何帮助?

  • 按照它,代码将无异常地编译和运行
  • 不要遵循它,代码将编译,但在运行时抛出异常。

词汇

什么是静态类型?

应该叫做引用类型

提供AB是类型,在下面的代码中

A ref = new B();

Aref(B是 的动态类型ref)的静态类型。学术术语。

数组的具体化类型是什么?

具体化意味着在运行时可用的类型信息。数组被认为是可具体化的,因为 VM 知道它们的组件类型(在运行时)。

在 中arr2 = new Number[30], 的具体化类型arr2Number[]具有组件类型的数组类型Number

什么是类型的擦除?

应该称为运行时类型

类型参数的虚拟机视图(运行时视图)。提供的T是一个类型参数,下面代码的运行时视图

<T extends Comparable<T>> void stupidMethod(T[] elems) {
    T first = elems[0];
}
Run Code Online (Sandbox Code Playgroud)

将会

void stupidMethod(Comparable[] elems) {
    Comparable first = elems[0];
}
Run Code Online (Sandbox Code Playgroud)

这使得Comparable运行时类型为T. 为什么Comparable?因为那是 的最左边的界限T

我应该查看什么样的代码才能使原则相关?

代码应该暗示分配给数组类型的引用。无论是左值右值应该包括一个类型参数。

例如提供的T是一个类型参数

T[] a = (T[])new Object[0]; // type parameter T involved in lvalue
Run Code Online (Sandbox Code Playgroud)

或者

String[] a = toArray(s); // type parameter involved in rvalue
// where the signature of toArray is
<T> T[] toArray(Collection<T> c);
Run Code Online (Sandbox Code Playgroud)

如果左值或右值中不涉及类型参数,则该原则不相关。

例一(遵循的原则)

<T extends Number> void stupidMethod(List<T>elems) {
    T[] ts = (T[]) new Number[0];
}
Run Code Online (Sandbox Code Playgroud)

Q1ts引用的数组的具体化类型是什么?

A1:数组创建提供了答案:Number使用new. Number[].

Q2:什么是静态类型ts

A2 :T[]

Q3:什么是静态类型的擦除ts

A3:为此我们需要擦除T. 鉴于这T extends Number是有界的,T的擦除类型是其最左边的边界 - Number。现在我们知道了 forT的擦除类型,那么for的擦除类型tsNumber[]

Q4 : 是否遵循了原则?

A4:重申问题。是A1的亚型A3?即是Number[]子类型Number[]?是 => 这意味着遵循原则。

例2(不遵循原则)

<T extends Number> void stupidMethod(List<T>elems) {
    T[] ts = (T[]) new Object[0];
}
Run Code Online (Sandbox Code Playgroud)

Q1ts引用的数组的具体化类型是什么?

A1:数组创建使用new,组件类型是Object,因此Object[]

Q2:什么是静态类型ts

A2 :T[]

Q3:什么是静态类型的擦除ts

A3:为此我们需要擦除T. 鉴于这T extends Number是有界的,T的擦除类型是其最左边的边界 - Number。现在我们知道了 forT的擦除类型,那么for的擦除类型tsNumber[]

Q4 : 是否遵循了原则?

A4:重申问题。是A1的亚型A3?即是Object[]子类型Number[]?否 => 这意味着没有遵循原则。

期望在运行时抛出异常。

例3(不遵循原则)

鉴于提供数组的方法

<T> T[] toArray(Collection<T> c){
    return (T[]) new Object[0];
}
Run Code Online (Sandbox Code Playgroud)

客户代码

List<String> s = ...;
String[] arr = toArray(s);
Run Code Online (Sandbox Code Playgroud)

Q1:提供方法返回的数组的具体化类型是什么?

A1:为此,您还需要查看提供方法以了解它是如何初始化的 - new Object[...]。这意味着该方法返回的数组的具体化类型是Object[]

Q2:什么是静态类型arr

A2 :String[]

Q3:什么是静态类型的擦除ts

A3:不涉及类型参数。擦除后的类型与静态类型相同String[]

Q4 : 是否遵循了原则?

A4:重申问题。是A1的亚型A3?即是Object[]子类型String[]?否 => 这意味着没有遵循原则。

期望在运行时抛出异常。

例4(遵循的原则)

鉴于提供数组的方法

<T> T[] toArray(Collection<T> initialContent, Class<T> clazz){
    T[] result = (T[]) Array.newInstance(clazz, initialContent);
    // Copy contents to array. (Don't use this method in production, use Collection.toArray() instead)
    return result;
}
Run Code Online (Sandbox Code Playgroud)

客户代码

List<Number> s = ...;
Number[] arr = toArray(s, Number.class);
Run Code Online (Sandbox Code Playgroud)

Q1:提供方法返回的数组的具体化类型是什么?

A1:使用反射创建的数组,组件类型为从客户端收到。答案是Number[]

Q2:什么是静态类型arr

A2 :Number[]

Q3:什么是静态类型的擦除ts

A3:不涉及类型参数。擦除后的类型与静态类型相同Number[]

Q4 : 是否遵循了原则?

A4:重申问题。是A1的亚型A3?即是Number[]子类型Number[]?是 => 这意味着遵循原则。

有什么好笑的名字?

在此吐槽。广告中的真相可能意味着销售您所说的销售内容。

lvalue = rvalue我们有rvalue作为提供者和lvalue接收者。可能是作者将提供者视为广告商。

参考上述实施例三的提供方法,

<T> T[] toArray(Collection<T> c){
    return (T[]) new Object[0];
}
Run Code Online (Sandbox Code Playgroud)

方法签名

<T> T[] toArray(Collection<T> c);
Run Code Online (Sandbox Code Playgroud)

可以理解为广告:给我一个Longs的列表,我会给你一个Longs的数组

然而查看方法体,实现表明该方法不是真实的,因为它创建和返回的数组是一个Objects数组。

因此toArray,示例 3 中的方法在于其营销活动。

在示例 4 中,提供方法是真实的,因为签名中的语句(给我一个集合及其类型参数作为类文字,我会给你一个具有该组件类型的数组)与正文中发生的事情相匹配。

示例 3 和 4 具有充当广告的方法签名。

示例 1 和 2 没有显式广告(方法签名)。广告和规定是交织在一起的。

尽管如此,我想不出更好的名字来命名这个原则。这是一个地狱般的名字。

闭幕致辞

由于使用了诸如静态类型擦除类型之类的术语,我认为该原则的陈述不必要地含糊不清。分别在擦除后使用引用类型运行时类型/类型将使 Java 外行(就像您真正的那样)更容易掌握。

作者说这本书是关于 Java 泛型的最好的 [0]。我认为这意味着他们针对的受众范围很广,因此他们介绍的原则的更多示例将非常有帮助。

[0] https://youtu.be/GOMovkQCYD4?t=53


sam*_*jaf 3

可以这样想:

T[] array = (T[]) new Object[collection.size()];创建一个新数组。由于语言设计的原因,运行时 的类型T是未知的。在您的示例中,您知道事实TString,但从虚拟机的角度来看,事实TObject。所有转换操作都发生在调用方法中。

这样就创建了toArray一个数组Object[]。类型参数或多或少是语法糖,对创建的字节码没有任何影响。

那么为什么不能将对象数组转换为字符串数组呢?

让我们举个例子:

void methodA(){
  Object[] array = new Object[10];
  array[0]=Integer.valueOf(10);
  array[1]=Object.class;
  array[2]=new Object();
  array[3]="Hello World";
  methodB((String[])array);
}
void methodB(String[] stringArray){
  String aString=stringArray[1]; //This is not a String, but Object.class!
}
Run Code Online (Sandbox Code Playgroud)

如果你可以转换一个数组,你会说“我之前添加的所有元素都是有效的子类型”。但由于您的数组的类型为Object,因此虚拟机无法保证该数组在所有情况下始终包含有效的子类型。

methodB认为它处理一个字符串数组,但实际上该数组确实包含非常不同的类型。

相反的方法也不起作用:

void methodA(){
  String[] array = new String[10];
  array[0]="Hello World";
  methodB((Object[])array);
  //Method B had controll over the array and could have added any object, especially a non-string!
  System.out.println(array[1]); 
}
void methodB(Object[] oArray){
  oArray[1]=Long.valueOf(2);
}
Run Code Online (Sandbox Code Playgroud)

我希望这能有所帮助。

编辑:再次阅读您的问题后,我认为您正在混合以下内容:

  1. 数组无法转换(正如我上面所解释的)
  2. 引用的句子确实用简单的英语说:“如果创建 A 类型的数组,则该数组中的所有元素都必须是 A 类型或 A 的子类型”。因此,如果您创建一个数组,则Object可以将任何 java 对象放入数组中,但如果您创建一个数组,则Number值必须是类型NumberLong, Double, ...)。总而言之,这句话相当琐碎。或者我也不明白;)

编辑2:事实上,您可以将数组转换为您想要的任何类型。也就是说,您可以将数组转换为任何类型,就像将任何类型转换为 String ( String s=(String)Object.class;) 一样。特别是你可以将 a 转换String[]为 an Object[],反之亦然。正如我在示例中指出的,此操作会引入大量潜在错误,因为读取/写入数组可能会失败。我想不出在任何情况下强制转换数组都是一个好的决定。在某些情况下(例如通用实用程序类),它似乎是一个很好的解决方案,但如果您发现自己处于想要转换数组的情况,我仍然建议您过度考虑设计。感谢 newacct 指出强制转换操作本身是有效的。