具体细节内容参考如下网址,此处不做过多重复描述javascript
https://www.cnblogs.com/dengchengchao/p/9717097.htmlphp
https://blog.csdn.net/qq_34944851/article/details/55049106html
https://www.cnblogs.com/JokerShi/p/8117556.htmljava
泛型类的定义node
class 类名<声明自定义泛型>{ }
泛型类要注意的事项:
1)在类上自定义泛型的具体数据类型,是在使用该类建立对象的时候肯定的。
2)若是一个类在类上已经声明了自定义泛型,可是在使用该类建立对象的时候没有指定泛型的具体数据类型时,那么默认为Object类型。 nginx
3)在类上自定义泛型时,不能做用于静态的方法。若是静态的方法须要使用自定义泛型,那么须要在方法上本身声明使用。swift
1 public class caseTest<T>{ 2 public void toString(T[] args){ 3 //..... 4 } 5 /** 6 *静态方法的自定义泛型类型不会与类自定义的泛型类型产生冲突,缘由是 7 * 静态方法不须要建立对象即可调用 8 * 另外,在方法中定义了局部变量,那么操做的即是局部变量,不会受到成员变量的影响 9 */ 10 public static <T>void print(T[] args){ 11 //...... 12 } 13 public void static main(String[] args){ 14 caseTest<String> obj1= new caseTest<String>();//此时只能传入String类型的参数 15 caseTest obj2 = new caseTest();//建立对象时没有指定对象类型,默认为Object 16 17 } 18 } 19
泛型接口的定义数组
interface 接口名<声明自定义泛型>{ }
泛型接口要注意的事项:
1)接口上自定义泛型的具体数据类型是在实现一个接口的时候指定的。安全
2)若是在接口上自定义了泛型,可是在实现接口的时候没有指定具体的数据类型,那么默认为Object类型。app
1 public interface Dao<T>{ 2 public void add(T t); 3 } 4 5 public class Demo implements Dao<String> { 6 7 public void add(String t) { 8 9 } 10 11 }
补充事项:
1)需求: 目前实现一个接口的时候,还不明确目前要操做的数据类型,要等待建立接口实现类对象的时候才能指定泛型的具体数据类型。
2)要延长接口自定义泛型的具体数据类型时,格式以下:
1 interface Dao<T>{ 2 public void add(T t); 3 } 4 5 public class Demo<T> implements Dao<T> { 6 7 public void add(T t) { 8 9 } 10 11 public static void main(String[] args) { 12 Demo<String> d = new Demo<String>(); 13 } 14 15 }
泛型方法的定义
1 泛型方法在方法名称前面有一个<T>声明
1 泛型在最终会擦除为Object类型。这样致使的是在编写泛型代码的时候,对泛型元素的操做只能使用Object自带的一些方法,可是有时候咱们想使用其余类型的方法呢? 2 答案是extend,泛型重载了extend关键字,能够经过extend关键字指定最终擦除所替代的类型。
引用其余人写的,以为不错:
定义泛型方法时,必须在返回值前边加一个<T>,来声明这是一个泛型方法,持有一个泛型T,而后才能够用泛型T做为方法的返回值。
Class<T>的做用就是指明泛型的具体类型,而Class<T>类型的变量c,能够用来建立泛型类的对象。
为何要用变量c来建立对象呢?既然是泛型方法,就表明着咱们不知道具体的类型是什么,也不知道构造方法如何,所以没有办法去new一个对象,但能够利用变量c的newInstance方法去建立对象,也就是利用反射建立对象。
泛型方法要求的参数是Class<T>类型,而Class.forName()方法的返回值也是Class<T>,所以能够用Class.forName()做为参数。其中,forName()方法中的参数是何种类型,返回的Class<T>就是何种类型。在本例中,forName()方法中传入的是User类的完整路径,所以返回的是Class<User>类型的对象,所以调用泛型方法时,变量c的类型就是Class<User>,所以泛型方法中的泛型T就被指明为User,所以变量obj的类型为User。
固然,泛型方法不是仅仅能够有一个参数Class<T>,能够根据须要添加其余参数。
为何要使用泛型方法呢?由于泛型类要在实例化的时候就指明类型,若是想换一种类型,不得不从新new一次,可能不够灵活;而泛型方法能够在调用的时候指明类型,更加灵活。
泛型你们都接触的很多,可是因为Java 历史的缘由,Java 中的泛型一直被称为伪泛型,所以对Java中的泛型,有不少不注意就会遇到的“坑”,在这里详细讨论一下。对于基础而又常见的语法,这里就直接略过了。
什么是泛型
自JDK 1.5 以后,Java 经过泛型解决了容器类型安全这一问题,而几乎全部人接触泛型也是经过Java的容器。那么泛型到底是什么?
泛型的本质是参数化类型
也就是说,泛型就是将所操做的数据类型做为参数的一种语法。
public class Paly<T>{ T play(){} }
其中T
就是做为一个类型参数在Play
被实例化的时候所传递来的参数,好比:
Play<Integer> playInteger=new Play<>();
这里T
就会被实例化为Integer
泛型的做用
- 使用泛型能写出更加灵活通用的代码
泛型的设计主要参照了C++的模板,旨在能让人写出更加通用化,更加灵活的代码。模板/泛型代码,就好像作雕塑时的模板,有了模板,须要生产的时候就只管向里面注入具体的材料就行,不一样的材料能够产生不一样的效果,这即是泛型最初的设计宗旨。
- 泛型将代码安全性检查提早到编译期
泛型被加入Java语法中,还有一个最大的缘由:解决容器的类型安全,使用泛型后,能让编译器在编译的时候借助传入的类型参数检查对容器的插入,获取操做是否合法,从而将运行时ClassCastException
转移到编译时好比:
List dogs =new ArrayList(); dogs.add(new Cat());
在没有泛型以前,这种代码除非运行,不然你永远找不到它的错误。可是加入泛型后
List<Dog> dogs=new ArrayList<>(); dogs.add(new Cat());//Error Compile
会在编译的时候就检查出来。
- 泛型可以省去类型强制转换
在JDK1.5以前,Java容器都是经过将类型向上转型为Object
类型来实现的,所以在从容器中取出来的时候须要手动的强制转换。
Dog dog=(Dog)dogs.get(1);
加入泛型后,因为编译器知道了具体的类型,所以编译期会自动进行强制转换,使得代码更加优雅。
泛型的具体实现
咱们能够定义泛型类,泛型方法,泛型接口等,那泛型的底层是怎么实现的呢?
从历史上看泛型
因为泛型是JDK1.5以后才出现的,在此以前须要使用泛型(模板代码)的地方都是经过Object
向上转型以及强制类型转换实现的,这样虽然能知足大多数需求,可是有个最大的问题就在于类型安全。在获取“真正”的数据的时候,若是不当心强制转换成了错误类型,这种错误只能在真正运行的时候才能发现。
所以Java 1.5推出了“泛型”,也就是在本来的基础上加上了编译时类型检查的语法糖。Java 的泛型推出来后,引发来不少人的吐槽,由于相对于C++等其余语言的泛型,Java的泛型代码的灵活性依然会受到不少限制。这是由于Java被规定必须保持二进制向后兼容性,也就是一个在Java 1.4版本中能够正常运行的Class文件,放在Java 1.5中必须是可以正常运行的:
在1.5以前,这种类型的代码是没有问题的。
public static void addRawList(List list){ list.add("123"); list.add(2); }
1.5以后泛型大量应用后:
public static void addGenericList(List<String> list){ list.add("1");//Only String list.add("2"); }
虽然咱们认为addRawList()
方法中的代码不是类型安全的,可是某些时候这种代码是有用的,在设计JDK1.5的时候,想要实现泛型有两种选择:
- 须要泛型化的类型(主要是容器(Collections)类型),之前有的就保持不变,而后平行地加一套泛型化版本的新类型;
- 直接把已有的类型泛型化,让全部须要泛型化的已有类型都原地泛型化,不添加任何平行于已有类型的泛型版。
什么意思呢?也就是第一种办法是在原有的Java库的基础上,再添加一些库,这些库的功能和本来的如出一辙,只是这些库是使用Java新语法泛型实现的,而第二种办法是保持和本来的库的高度一致性,不添加任何新的库。
在出现了泛型以后,本来没有使用泛型的代码就被称为raw type
(原始类型)
Java 的二进制向后兼容性使得Java 须要实现先后兼容的泛型,也就是说之前使用原始类型的代码能够继续被泛型使用,如今的泛型也能够做为参数传递给原始类型的代码。
好比
List<String> list=new ArrayList<>(); List rawList=new ArrayList(); addRawList(list); addGenericList(list); addRawList(rawList); addGenericList(rawList);
上面的代码可以正确的运行。
Java 设计者选择了第二种方案
C# 在1.1过渡到2.0中增长泛型时,使用了第一种方案。
为了实现以上功能,Java 设计者将泛型彻底做为了语法糖加入了新的语法中,什么意思呢?也就是说泛型对于JVM来讲是透明的,有泛型的和没有泛型的代码,经过编译器编译后所生成的二进制代码是彻底相同的。
这个语法糖的实现被称为擦除
擦除的过程
泛型是为了将具体的类型做为参数传递给方法,类,接口。
擦除是在代码运行过程当中将具体的类型都抹除。
前面说过,Java 1.5 以前须要编写模板代码的地方都是经过Object
来保存具体的值。好比:
public class Node{ private Object obj; public Object get(){ return obj; } public void set(Object obj){ this.obj=obj; } public static void main(String[] argv){ Student stu=new Student(); Node node=new Node(); node.set(stu); Student stu2=(Student)node.get(); } }
这样的实现能知足绝大多数需求,可是泛型仍是有更多方便的地方,最大的一点就是编译期类型检查,因而Java 1.5以后加入了泛型,可是这个泛型仅仅是在编译的时候帮你作了编译时类型检查,成功编译后所生成的.class
文件仍是如出一辙的,这即是擦除
1.5 之后实现
public class Node<T>{ private T obj; public T get(){ return obj; } public void set(T obj){ this.obj=obj; } public static void main(String[] argv){ Student stu=new Student(); Node<Student> node=new Node<>(); node.set(stu); Student stu2=node.get(); } }
两个版本生成的.class文件:
Node:
public Node(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public java.lang.Object get(); Code: 0: aload_0 1: getfield #2 // Field obj:Ljava/lang/Object; 4: areturn public void set(java.lang.Object); Code: 0: aload_0 1: aload_1 2: putfield #2 // Field obj:Ljava/lang/Object; 5: return }
Node
public class Node<T> { public Node(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public T get(); Code: 0: aload_0 1: getfield #2 // Field obj:Ljava/lang/Object; 4: areturn public void set(T); Code: 0: aload_0 1: aload_1 2: putfield #2 // Field obj:Ljava/lang/Object; 5: return }
能够看到泛型就是在使用泛型代码的时候,将类型信息传递给具体的泛型代码。而通过编译后,生成的.class
文件和原始的代码如出一辙,就好像传递过来的类型信息又被擦除了同样。
泛型语法
Java 的泛型就是一个语法糖,而语法糖最大的好处就是让人方便使用,可是它的缺点也在于若是不剥开这颗语法糖,有不少奇怪的语法就很难理解。
- 类型边界
前面说过,泛型在最终会擦除为Object
类型。这样致使的是在编写泛型代码的时候,对泛型元素的操做只能使用Object
自带的一些方法,可是有时候咱们想使用其余类型的方法呢?
好比:
public class Node{ private People obj; public People get(){ return obj; } public void set(People obj){ this.obj=obj; } public void playName(){ System.out.println(obj.getName()); } }
如上,代码中须要使用obj.getName()
方法,所以好比规定传入的元素必须是People
及其子类,那么这样的方法怎么经过泛型体现出来呢?
答案是extend
,泛型重载了extend
关键字,能够经过extend
关键字指定最终擦除所替代的类型。
public class Node<T extend People>{ private T obj; public T get(){ return obj; } public void set(T obj){ this.obj=obj; } public void playName(){ System.out.println(obj.getName()); } }
经过extend
关键字,编译器会将最后类型都擦除为People
类型,就好像最开始咱们看见的原始代码同样。
泛型与向上转型的概念
先讲一讲几个概念:
- 协变:子类能向父类转换
Animal a1=new Cat();
- 逆变: 父类能向子类转换
Cat a2=(Cat)a1;
- 不变: 二者均不能转变
对于协变,咱们见得最多的就是多态,而逆变常见于强制类型转换。
这好像没什么奇怪的。可是看如下代码:
public static void error(){ Object[] nums=new Integer[3]; nums[0]=3.2; nums[1]="string"; //运行时报错,nums运行时类型是Integer[] nums[2]='2'; }
由于数组是协变的,所以Integer[]
能够转换为Object[]
,在编译阶段编译器只知道nums
是Object[]
类型,而运行时nums
则为Integer[]
类型,所以上述代码可以编译,可是运行会报错。
这就是常见的人们所说的数组是协变的。这里带来一个问题,为何数组要设计为协变的呢?既然不让运行,那么经过编译有什么用?
答案是在泛型还没出现以前,数组协变可以解决一些通用的问题:
public static void sort(Object[] a) { if (LegacyMergeSort.userRequested) legacyMergeSort(a); else ComparableTimSort.sort(a, 0, a.length, null, 0, 0); }
/** * 摘自JDK 1.8 Arrays.equals() */ public static boolean equals(Object[] a, Object[] a2) { //... for (int i=0; i<length; i++) { Object o1 = a[i]; Object o2 = a2[i]; if (!(o1==null ? o2==null : o1.equals(o2))) return false; } //.. return true; }
能够看到,只操做数组自己,而关心数组中具体保存的原始,或则是无论什么元素,取出来就做为一个Object
存储的时候,只用编写一个Object[]
就能写出通用的数组参数方法。好比:
Arrays.sort(new Student[]{...}) Arrays.sort(new Apple[]{...})
等,可是这样的设计留下来的诟病就是偶尔会出现对数组元素有具体的操做的代码,好比上面的error()
方法。
泛型的出现,是为了保证类型安全的问题,若是将泛型也设计为协变的话,那也就违背了泛型最初设计的初衷,所以在Java中,泛型是不变的,什么意思呢?
List<Number>
和List<Integer>
是没有任何关系的,即便Integer
是Number
的子类
也就是对于
public static void test(List<Number> nums){...}
方法,是没法传递一个List<Integer>
参数的
逆变通常常见于强制类型转换。
Object obj="test"; String str=(String)obj;
原理即是Java 反射机制可以记住变量obj
的实际类型,在强制类型转换的时候发现obj
其实是一个String
类型,因而就正常的经过了运行。
泛型与向上转型的实现
前面说了这么多,应该关心的问题在于,如何解决既能使用数组协变带来的方便性,又能获得泛型不变带来的类型安全?
答案依然是extend
,super
关键字与通配符?
泛型重载了extend
,super
关键字来解决通用泛型的表示。
注意:这句话可能比较熟悉,没错,前面说过
extend
还被用来指定擦除到的具体类型,好比<E extend Fruit>
,表示在运行时将E
替换为Fruit
,注意E
表示的是一个具体的类型,可是这里的extend
和通配符连续使用<? extend Fruit>
这里通配符?
表示一个通用类型,它所表示的泛型在编译的时候,被指定的具体的类型必须是Fruit
的子类。好比List<? extend Fruit> list= new ArrayList<Apple>
,ArrayList<>
中指定的类型必须是Apple
,Orange
等。不要混淆。
概念麻烦,直接看代码:
协变泛型
public static void playFruit(List < ? extends Fruit> list){ //do somthing } public static void main(String[] args) { List<Apple> apples=new ArrayList<>(); List<Orange> oranges=new ArrayList<>(); List<Food> foods =new ArrayList<>(); playFruit(apples); playFruit(oranges); //playFruit(foods); 编译错误 }
能够看到,参数List < ? extend Fruit>
所表示是须要一个List<>
,其中尖括号所指定的具体类型必须是继承自Fruit
的。
这样便解决了泛型没法向上转型的问题,前面说过,数组也能向上转型,可是存取元素有问题啊,这里继续深刻,看看泛型是怎么解决这一问题的。
public static void playFruit(List < ? extends Fruit> list){ list.add(new Apple()); }
向传入的list
添加元素,你会发现编译器直接会报错
逆变泛型
public static void playFruitBase(List < ? super Fruit> list){ //.. } public static void main(String[] args) { List<Apple> apples=new ArrayList<>(); List<Food> foods =new ArrayList<>(); List<Object> objects=new ArrayList<>(); playFruitBase(foods); playFruitBase(objects); //playFruitBase(apples); 编译错误 }
同理,参数List < ? super Fruit>
所表示是须要一个List<>
,其中尖括号所指定的具体类型必须是Fruit
的父类类型。
public static void playFruitBase(List < ? super Fruit> list){ Object obj=list.get(0); }
取出list
的元素,你会发现编译器直接会报错
思考: 为何要这么麻烦要区分开究竟是xxx的父类仍是子类,不能直接使用一个关键字表示么?
前面说过,数组的协变之因此会有问题是由于在对数组中的元素进行存取的时候出现的问题,只要不对数组元素进行操做,就不会有什么问题,所以可使用通配符?
达到此效果:
public static void playEveryList(List < ?> list){ //.. }
对于playEveryList
方法,传递任何类型的List
都没有问题,可是你会发现对于list
参数,你没法对里面的元素存和取。这样便达到了上面所说的安全类型的协变数组的效果。
可是以为多数时候,咱们仍是但愿对元素进行操做的,这就是extend
和super
的功能。
<? extend Fruit>
表示传入的泛型具体类型必须是继承自Fruit
,那么咱们能够里面的元素必定能向上转型为Fruit
。可是也仅仅能肯定里面的元素必定能向上转型为Fruit
public static void playFruit(List < ? extends Fruit> list){ Fruit fruit=list.get(0); //list.add(new Apple()); }
好比上面这段代码,能够正确的取出元素,由于咱们知道所传入的参数必定是继承自Fruit
的,好比
List<Apple> apples=new ArrayList<>(); List<Orange> oranges=new ArrayList<>();
都能正确的转换为Fruit
,
可是咱们并不知道里面的元素具体是什么,有多是Orange
,也有多是Apple
,所以,在list.add()
的时候,就会出现问题,有可能将Apple
放入了Orange
里面,所以,为了避免出错,编译器会禁止向里面加入任何元素。这也就解释了协变中使用add
会出错的缘由。
同理:
<? super Fruit>
表示传入的泛型具体类型必须是Fruit
的父类,那么咱们能够肯定只要元素是Fruit
以及能转型为Fruit
的,必定能向上转型为对应的此类型,好比:
public static void playFruitBase(List < ? super Fruit> list){ list.add(new Apple()); }
由于Apple
继承自Fruit
,而参数list最终被指定的类型必定是Fruit
的父类,那么Apple
必定能向上转型为对应的父类,所以能够向里面存元素。
可是咱们只能肯定他是Furit
的父类,并不知道具体的“上限”。所以没法将取出来的元素统一的类型(固然能够用Object
)。好比
List<Eatables> eatables=new ArrayList<>(); List<Food> foods=new ArrayList<>();
除了
Object obj; obj=eatables.get(0); obj=foods.get(0);
以外,没有肯定类型能够修饰obj
以达到相似的效果。
针对上述状况。咱们能够总结为:PECS原则,Producer-Extend,Customer-Super
,也就是泛型代码是生产者,使用Extend
,泛型代码做为消费者Super
泛型的阴暗角落
经过擦除而实现的泛型,有些时候会有不少让人难以理解的规则,可是了解了泛型的真正实现又会以为这样作仍是比较合情合理。下面分析一下关于泛型在应用中有哪些奇怪的现象:
擦除的地点---边界
static <T> T[] toArray(T... args) { return args; } static <T> T[] pickTwo(T a, T b, T c) { switch(ThreadLocalRandom.current().nextInt(3)) { case 0: return toArray(a, b); case 1: return toArray(a, c); case 2: return toArray(b, c); } throw new AssertionError(); // Can't get here } public static void main(String[] args) { String[] attributes = pickTwo("Good", "Fast", "Cheap"); }
这是在《Effective Java》中看到的例子,编译此代码没有问题,可是运行的时候却会类型转换错误:Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
当时对泛型并无一个很好的认识,一直不明白为何会有Object[]
转换到String[]
的错误。如今咱们来分析一下:
- 首先看
toArray
方法,由本章最开始所说泛型使用擦除实现的缘由是为了保持有泛型和没有泛型所产生的代码一致,那么:
static <T> T[] toArray(T... args) { return args; }
和
static Object[] toArray(Object... args){ return args; }
生成的二进制文件是一致的。
进而剥开可变数组的语法糖:
static Object[] toArray(Object[] args){ return args; }
static <T> T[] pickTwo(T a, T b, T c) { switch(ThreadLocalRandom.current().nextInt(3)) { case 0: return toArray(a, b); case 1: return toArray(a, c); case 2: return toArray(b, c); } throw new AssertionError(); // Can't get here }
和
static Object[] pickTwo(Object a, Object b, Object c) { switch(ThreadLocalRandom.current().nextInt(3)) { case 0: return toArray(new Object[]{a,b});//可变参数会根据调用类型转换为对应的数组,这里a,b,c都是Object case 1: return toArray(new Object[]{a,b}); case 2: return toArray(new Object[]{a,b}); } throw new AssertionError(); // Can't get here }
是一致的。
那么调用pickTwo
方法实际编译器会帮我进行类型转换
public static void main(String[] args) { String[] attributes =(String[])pickTwo("Good", "Fast", "Cheap"); }
能够看到,问题就在于可变参数那里,使用可变参数编译器会自动把咱们的参数包装为一个数组传递给对应的方法,而这个数组的包装在泛型中,会最终翻译为new Object
,那么toArray
接受的实际类型是一个Object[]
,固然不能强制转换为String[]
上面代码出错的关键点就在于泛型通过擦除后,类型变为了Object
致使可变参数直接包装出了一个Object
数组产生的类型转换失败。
基类劫持
public interface Playable<T> { T play(); } public class Base implements Playable<Integer> { @Override public Integer play() { return 4; } } public class Derived extend Base implements Playable<String>{ ... }
能够发如今定义Derived
类的时候编译器会报错。
观察Derived
的定义能够看到,它继承自Base
那么它就拥有一个Integer play()
和方法,继而实现了Playable<String>
接口,也就是它必须实现一个String play()
方法。对于Integer play()
和String play()
两个方法的函数签名相同,可是返回类型不一样,这样的方法在Java 中是不容许共存的:
public static void main(String[] args){ new Derived().play(); }
编译器并不知道应该调用哪个play()
方法。
自限定类型
自限定类型简单点说就是将泛型的类型限制为本身以及本身的子类。最多见的在于实现Compareable
接口的时候:
public class Student implements Comparable<Student>{ }
这样就成功的限制了能与Student
相比较的类型只能是Student
,这很好理解。
可是正如Java 中返回类型是协变的:
public class father{ public Number test(){ return nll; } } public class Son extend father{ @Override public Interger test(){ return null; } }
有些时候对于一些专门用来被继承的类须要参数也是协变的。好比实现一个Enum
:
public abstract class Enum implements Comparable<Enum>,Serializable{ @Override public int compareTo(Enum o) { return 0; } }
这样是没有问题的,可是正如常规所说,假如Pen
和Cup
都继承于Enum
,可是按道理来讲笔和杯子之间相互比较是没有意义的,也就是说在Enum
中compareTo(Enum o)
方法中的Enum
这个限定词太宽泛,这个时候有两种思路:
- 子类分别本身实现
Comparable
接口,这样就能够规定更详细的参数类型,可是因为前面所说,会出现基类劫持的问题 - 修改父类的代码,让父类不实现
Comparable
接口,让每一个子类本身实现便可,可是这样会有大量如出一辙的代码,只是传入的参数类型不一样而已。
而更好的解决方案即是使用泛型的自限定类型:
public abstract class Enum<E extend Enum<E>> implements Comparable<E>,Serializable{ @Override public int compareTo(E o) { return 0; } }
泛型的自限定类型比起传统的自限定类型有个更大的优势就是它能使泛型的参数也变成协变的。
这样每一个子类只用在集成的时候指定类型
public class Pen extends Enum<Pen>{} public class Cup extends Cup<Cup>{}
便可以在定义的时候指定想要与那种类型进行比较,这样达到的效果便至关于每一个子类都分别本身实现了一个自定义的Comparable
接口。
自限定类型通常用在继承体系中,须要参数协变的时候。
尊重原创,转载请注明出处
参考文章:
Java不能实现真正泛型的缘由? - RednaxelaFX的回答 - 知乎
深刻理解 Java 泛型
java中,数组为何要设计为协变? - 胖君的回答 - 知乎
java泛型中的自限定类型有什么做用-CSDN问答