1. Java中为什么不能创建泛型数组?

所谓泛型数组,是指泛型类型的数组,或直接由泛型参数定义的数组。

  • 泛型类型的数组 ```java // 定义一个泛型类型 class Holder { private final T value;

    public Holder(T value) {

    1. this.value = value;

    } }

public class GenericArrayTest { public static void main(String[] args) { // 创建泛型类型的数组。无法通过编译。 Holder[] arr = new Holder[10];

    // 注意:创建原始类的数组,可通过编译。
    Holder<String>[] arr = new Holder[10];
}

}


- 直接用泛型参数定义数组
```java
public <T> void Create(){
        // 无法通过编译
        T[] arr = new T[10];
    }

为何不允许创建泛型数组?

泛型类型擦除

泛型是Java在1.5版本才引入的。为了保证与老代码的兼容性,Java的泛型只存在于编译时。在编译后的字节码中,泛型标记都被删除,这种机制称为“类型擦除”。
对于上面的例子,假设都可以通过编译,那么编译后的代码等价于:

// 例1
Holder[] arr = new Holder[10];

// 例2。由于编译时无法得到T的类型,因此只能认为是Object。
Object[] arr=new Object[];

数组协变

Java的数组是“强类型”的,即数组本身就是有类型的,是“真泛型”(即编译后类型新依然存在,其他Java集合类是假泛型)。这意味着Java的数组中每一个元素都必须是指定的相同类型,无论编译时还是运行时。
但是,Java的数组是可协变的。这是一个设计缺陷。初衷可能是因为在泛型出现以前,想要编写处理各种类型数组的通用方法(Arrays中的方法),因此放松了类型检查。
这就导致了一个问题。以下代码是可以通过编译的:

String[] a = new String[10];
    Object[] b = a;
    b[0] = 1; // 可以通过编译,但运行时会报java.lang.ArrayStoreException

在原本为String[]类型的数组中,居然放入了一个int类型值,即数组元素的类型不一致,这违背了强类型要求。
但好在尽管可以通过编译,但创建数组时指定的元素类型是不会丢失的(不会擦除),Java会在运行时记住数组的元素类型,并检查放入其中的每一个元素。若类型不匹配,会报java.lang.ArrayStoreException。因此上述代码在执行时会这个错。

如果允许创建泛型数组,会怎么样?

已知,泛型数组编译后丢失类型信息,最终生成的是原始类型的数组。即创建数组时的元素类型会丢失,在运行时无法检查放入的元素是否合法
若允许创建泛型数组,则可能发生“堆污染”,进而造成类型转化错误ClassCastException。https://en.wikipedia.org/wiki/Heap_pollution。
比如:

Holder<String>[] strArr = new Holder<String>[10];
    Object[] objArr = strArr;
    objArr[0] = new Holder<String>("string");
    objArr[1] = new Holder<Integer>(20);

    Holder<String> readVal = strArr[1];
    String s=readVal.getValue();

类型擦除后:

Holder[] strArr = new Holder[10]; // 创建的数组类型是原始类型Holder[],不包含类型参数信息
    Object[] objArr = strArr;
    objArr[0] = new Holder("string"); 
    objArr[1] = new Holder(20); // 元素都是原始类型Holder。不包含类型参数信息。与数组类型匹配,不会报错。但在编译时,不同类型参数的具体Holder应属于不同类型,不能放入一个数组。这违背了强类型要求。

    Holder readVal = strArr[1]; 
    String s=readVal.getValue(); // 类型转化错误

更明显的,比如:

T[] arr = new T[];
    Object[] objArr = arr;
    objArr[0] = "Fly";
    objArr[1] = 1;

类型擦除后:

Object[] arr = new Object[]; // 失去了数组类型信息
    Object[] objArr = arr;
    objArr[0] = "Fly";
    objArr[1] = 1; // 无法进行运行时类型检查

总结

由于Java的泛型是假泛型,泛型数组在运行时将退化为“原始类型数组”,失去运行时类型检查。又由于数组协变的设计缺陷,失去了编译时的类型检查。从而泛型数组可能发生“堆污染”,进而可能导致类型转换错误。因此,Java不允许创建泛型数组。


2. List, List<?>, List的区别

这里的List可以替换为其他任何泛型类型。

  • List:代表原始类型。是类型擦除后、运行时的真正类型。也是泛型出现以前存在的类型。不具有类型参数的检查功能。
  • List<?>:类型参数为无界通配符的泛型List。由于使用了无界通配符,所以支持协变和逆变。但是不能调用任何以泛型参数类型作为参数的方法,也不能调用返回泛型参数类型方法。
  • List:类型参数为Object的泛型List。不支持协变和逆变。

      public static void main(String[] args) {
          ArrayList<String> a = new ArrayList<>(); // 把这里的类型参数String缓存其他类型,或者Object,或者通配符?,都可以。因为都属于参数化后的泛型。
    
          // 原始类型和参数化类型之间的转换
          ArrayList b = a; // 可以赋值成功
    
          // 以下赋值编译器会提示“Unchecked assignment: 'java.util.ArrayList' to 'java.util.ArrayList<java.lang.String>' ”
          // 可通过添加@SuppressWarnings("unchecked")来避免该警告。
          ArrayList<String> c = b; 
    
          // 以下赋值编译会报错,因为泛型默认情况下不支持协变。
          ArrayList<Object> d = a;
    
          // 以下赋值正确。因为通配符说明了变量e支持协变。
          ArrayList<?> e = a;
          ArrayList<? extend Object> f = a;
      }
    

    3. 泛型参数类型推导

    类型推导就是在某些情况下,我们不用显示指定泛型类型参数,编译器会自动帮我们推断出来。简单情况好理解,比如根据参数类型来推断泛型参数。比较让人疑惑的是,根据“目标类型(Target Type)”来进行推断。这也有两种情况:

    1. 在new一个泛型对象时,可以只在目标变量类型上指定类型参数即可,泛型对象本身只需要一个<>符号:

      ArrayList<String> s = new ArrayList<>(); // 可省略泛型类中的类型参数
      
    2. 在进行方法调用时,如果方法返回的类型是未确定类型参数的泛型,那么编译器会根据赋值的目标类型来推断参数类型。

       // Collections工具类中的一个方法,作用是返回一个空列表
       // 需要注意的是,这里无法通过参数推断出返回值的具体类型
       public static final <T> List<T> emptyList() {
           return (List<T>) EMPTY_LIST;
       }
      
       public static void main(String[] args) {
           // 在调用时,可以根据目标类型来推断泛型参数类型
           List<String> a = Collections.emptyList();
       }
      

      根据目标类型进行推断不太容易理解。据我所知C#是没有这个特性的。
      在项目开发中的一个实际例子:

       @ApiOperation("查询单个VRP的详情")
       @GetMapping("{vrpNumber}")
       public ResponseEntity<RestResponse<VrpMasterDto>> queryVrpByNumber(@PathVariable String vrpNumber) {
           VrpMasterDto vrpMasterDto = this.application.QueryVrpByNumber(vrpNumber);
           if (vrpMasterDto == null) {
               return ResponseUtils.notFound(); // 根据目标类型(ResponseEntity<RestResponse<VrpMasterDto>>)自动推导
           }
           return ResponseUtils.object(vrpMasterDto);
       }
      
       // notFound方法的定义
       public static <T> ResponseEntity<T> notFound() {
           return ResponseEntity.notFound()
                   .build();
       }
      

    参考:
    Java 8 新特性之泛型的类型推导


    4. 泛型类中,静态方法和字段无法直接使用类定义的类型参数

    在Java中,由于类型擦除机制,所有已参数化的泛型类型,在运行时实际上共用的同一个类。因此,对于本身就由所有对象共享的静态方法和字段,就不同通过某个已参数话的泛型类型来“独家”确定。因为一个已参数话的类型确定了静态字段的类型,那另外的已参数话类型就类型不匹配了。

    class Holder<T> {
        public static T a; // 静态字段类型不能为类的类型参数
    
        public static T get(T a) {  // 静态方法的参数和返回值类型,不能为类的类型参数。但可以定义该方法独有的类型参数。
        }
    }