为什么会引入泛型

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

  • 适用于多种数据类型执行相同的代码(代码复用) ```java private static int add(int a, int b) { System.out.println(a + “+” + b + “=” + (a + b)); return a + b; }

private static float add(float a, float b) { System.out.println(a + “+” + b + “=” + (a + b)); return a + b; }

private static double add(double a, double b) { System.out.println(a + “+” + b + “=” + (a + b)); return a + b; }

  1. 下面这个,只要是进入数字类型的,我都可以得到我想要的**double**类型
  2. ```java
  3. public <T extends Number> double add(T a, T b){
  4. return a.doubleValue() + b.doubleValue();
  5. }
  6. // <T extends Number> 表示 T 这个参继承于 Number
  • 泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型) ```java
      List list = new ArrayList();
      list.add(123);
      list.add(new Person());
      list.add("野子");
      for (Object o : list) {
          System.out.println(o);
          // 123
          //Person{id=null, name='null'}
          //野子
      }
    

// 不指定泛型,加入的每个值都是Object类型,不可控。 // 如果需要循环相加,自己忘记了这个list 是什么类型的,就会非常危险。

引入泛型,它将提供类型的约束,提供编译前的检查:
```java
        List<Integer> list = new ArrayList();
        list.add(123);
        list.add(321);
        for (Object o : list) {
            System.out.println(o);
            // 123
            // 321
        }

泛型的基本使用

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。

泛型类

可以看到,加上泛型 内部的值可以由外部指定。指定后就可以限制后面的参数

public class Point<T> {
    private T var;// var的类型由T指定,即:由外部指定
    public T getVar() {
        return var;
    }
    public void setVar(T var) {  // 设置的类型也由外部决定
        this.var = var;
    }
}


    @Test
    void test28() {

        Point<String> stringPoint = new Point<>();
        stringPoint.setVar("helloWorld");
        System.out.println(stringPoint.getVar());

        Point<Integer> integerPoint = new Point<>();
        integerPoint.setVar(9527);
        System.out.println(integerPoint.getVar());

    }
public class NotePad<K, V> {
    private K key;     // 此变量的类型由外部决定
    private V value;   // 此变量的类型由外部决定
    public K getKey() {
        return this.key;
    }
    public V getValue() {
        return this.value;
    }
    public void setKey(K key) {
        this.key = key;
    }
    public void setValue(V value) {
        this.value = value;
    }
    @Override
    public String toString() {
        return "NotePad{" +
                "key=" + key +
                ", value=" + value +
                '}';
    }
}


        // 这种形式的经常 用 Map 的就很熟悉了
        NotePad<String, Integer> stringIntegerNotePad = new NotePad<>();
        stringIntegerNotePad.setKey("hello");
        stringIntegerNotePad.setValue(9527);
        System.out.println(stringIntegerNotePad);

泛型接口

这个使得接口适用性更加的广泛了


interface Info<T>{        // 在接口上定义泛型  
    public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型  
}  
class InfoImpl<T> implements Info<T>{   // 定义泛型接口的子类  
    private T var ;             // 定义属性  
    public InfoImpl(T var){     // 通过构造方法设置属性内容  
        this.setVar(var) ;    
    }  
    public void setVar(T var){  
        this.var = var ;  
    }  
    public T getVar(){  
        return this.var ;  
    }  
} 
public class GenericsDemo24{  
    public static void main(String arsg[]){  
        Info<String> i = null;        // 声明接口对象  
        i = new InfoImpl<String>("汤姆") ;  // 通过子类实例化对象  
        System.out.println("内容:" + i.getVar()) ;  
    }  
}

泛型方法

public class Generic<T> {
    private T key;
    public Generic(T key) {
        this.key = key;
    }
    public T getKey(){
        return key;
    }
}


        System.out.println("container key :" + container.getKey());
        //当然这个例子举的不太合适,只是为了说明泛型方法的特性。
        T test = container.getKey();
        return test;



        Generic<String> stringGeneric = new Generic<String>("helloWorld");
        String s = util.showKeyName(stringGeneric);
        System.out.println(s); // helloWorld

泛型的上下限

  • 上限

    // 上限   值被限制住到何种地步
    class Info<T extends Number>{    // 此处泛型只能是数字类型
      private T var ;        // 定义泛型变量
      public void setVar(T var){
          this.var = var ;
      }
      public T getVar(){
          return this.var ;
      }
      public String toString(){    // 直接打印
          return this.var.toString() ;
      }
    }
    public class demo1{
      public static void main(String args[]){
          Info<Integer> i1 = new Info<Integer>() ;        // 声明Integer的泛型对象
      }
    }
    
  • 下限

    // 下限    值的下限有多少
    class Info<T>{
      private T var ;        // 定义泛型变量
      public void setVar(T var){
          this.var = var ;
      }
      public T getVar(){
          return this.var ;
      }
      public String toString(){    // 直接打印
          return this.var.toString() ;
      }
    }
    public class GenericsDemo21{
      public static void main(String args[]){
          Info<String> i1 = new Info<String>() ;        // 声明String的泛型对象
          Info<Object> i2 = new Info<Object>() ;        // 声明Object的泛型对象
          i1.setVar("hello") ;
          i2.setVar(new Object()) ;
          fun(i1) ;
          fun(i2) ;
      }
      public static void fun(Info<? super String> temp){    // 只能接收String或Object类型的泛型,String类的父类只有Object类
          System.out.print(temp + ", ") ;
      }
    }
    

    ```java <?> 无限制通配符 <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类 <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类

// 使用原则《Effictive Java》 // 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符 //使用的规则就是:生产者有上限、消费者有下限 // 上限就是限制的条件 比如:只有人能进餐厅,如果该值继承的猫类,就不能进入。 // 下限条件更广 可能是此对象及其父类 比如:公园 <? super person> 不单单的允许人进,也可以允许人的父类进入 如:动物

  1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
  2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
  3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。 ```
  • 使用&符号 ```java

public class Client { //工资低于2500元的上斑族并且站立的乘客车票打8折 public static void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println(“恭喜你!您的车票打八折!”); } } public static void main(String[] args) { discount(new Me()); } }


---


<a name="9Kdv0"></a>
## 深入理解泛型

<a name="Lzbqj"></a>
### 如何理解Java中的泛型是伪泛型?泛型中类型擦除
> Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“**伪泛型**”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“**类型擦除**”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。理解类型擦除对于用好泛型是很有帮助的,尤其是一些看起来“疑难杂症”的问题,弄明白了类型擦除也就迎刃而解了。


**泛型的类型擦除原则**是:

- 消除类型参数声明,即删除`<>`及其包围的部分。
- 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
- 为了保证类型安全,必要时插入强制类型转换代码。
- 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。

**那么如何进行擦除的呢**?

- 擦除类定义中的类型参数 - 无限制类型擦除

当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如`<T>`和`<?>`的类型参数都被替换为Object。<br />![](https://cdn.nlark.com/yuque/0/2020/png/1145940/1600102486556-773e0caf-3db7-4989-bc12-7d70c554e788.png#align=left&display=inline&height=223&margin=%5Bobject%20Object%5D&originHeight=223&originWidth=776&size=0&status=done&style=none&width=776)

- 擦除类定义中的类型参数 - 有限制类型擦除

当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如`<T extends Number>`和`<? extends Number>`的类型参数被替换为`Number`,`<? super Number>`被替换为Object。![](https://cdn.nlark.com/yuque/0/2020/png/1145940/1600102509517-45ae3f91-12ea-40f0-a35b-9dd98d9f15cb.png#align=left&display=inline&height=236&margin=%5Bobject%20Object%5D&originHeight=236&originWidth=822&size=0&status=done&style=none&width=822)


- 擦除方法定义中的类型参数

擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例。<br />![](https://cdn.nlark.com/yuque/0/2020/png/1145940/1600102527812-eb420cb6-9886-4533-a10f-bf0c3df98232.png#align=left&display=inline&height=144&margin=%5Bobject%20Object%5D&originHeight=144&originWidth=982&size=0&status=done&style=none&width=982)


<a name="vapqv"></a>
### 如何证明类型的擦除呢?

- 原始类型相等
```java
        List<String> strings = new ArrayList<>();
        strings.add("hello");

        ArrayList<Integer> integers = new ArrayList<>();
        integers.add(123);

        System.out.println(strings.getClass()); // class java.util.ArrayList
        System.out.println(integers.getClass()); // class java.util.ArrayList
        System.out.println(strings.getClass() == integers.getClass()); // true

在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList<String>泛型类型的,只能存储字符串;一个是ArrayList<Integer>泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。

  • 通过反射添加其它类型元素

          ArrayList<Integer> integers = new ArrayList<>();
    
          integers.add(134);
    
          integers.getClass().getMethod("add", Object.class).invoke(integers, "hello");
    
          int size = integers.size();
          System.out.println(size); // 2
    
          for (Integer integer : integers) {
              System.out.println(integer); // java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
          }
    
          for (Object integer : integers) {
              System.out.println(integer); // 2 , 134
          }
    

    上面的这个例子就可以表示,Integer 在编译之后类型被擦除了。可以通过反射赋值,最后也是可以输出的。真的奇怪呀😮

如何理解类型擦除后保留的原始类型?

原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。

  • 原始类型Object

    class Pair<T> {  
      private T value;  
      public T getValue() {  
          return value;  
      }  
      public void setValue(T  value) {  
          this.value = value;  
      }  
    }
    
  • Pair的原始类型为:

在 Pair 中泛型 T 可以为任何值,所以 原型类型为 Object

class Pair {  
    private Object value;  
    public Object getValue() {  
        return value;  
    }  
    public void setValue(Object  value) {  
        this.value = value;  
    }  
}

如果类型变量有限定,那么原始类型就用第一个边界的类型变量类替换。所以上面的 原始类型为 Comparable

public class Pair<T extends Comparable> {}

在调用泛型方法时,可以指定泛型,也可以不指定泛型:

  • 在不指定泛型的情况下,泛型变量的类型为该方法中的几种类型的同一父类的最小级,直到Object
  • 在指定泛型的情况下,该方法的几种类型必须是该泛型的实例的类型或者其子类 ```java

      // 不指定泛型的时候
      Integer add = util.add(1, 2);
      Number add1 = util.add(1, 1.2);
      Object hello = util.add(1, "hello");
    
      // 指定泛型
      Integer add2 = util.<Integer>add(1, 2);
      util.<Integer>add(1, 1.1); //编译错误,指定了Integer,不能为Float  
      Number add3 = util.<Number>add(1, 1.1);
    
public static <T> T add(T a, T b){

    return b;

}

<a name="VeeZ3"></a>
### 如何理解泛型的编译期检查?
> 比如: List<String>  如果假如的不是String类型的就会报错,这就是编译期检查。
> Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。

```java
    ArrayList<String> list = new ArrayList<String>();  
    list.add("123");  
    list.add(123);//编译错误
        ArrayList<String> list1 = new ArrayList(); //第一种 情况
        ArrayList list2 = new ArrayList<String>(); //第二种 情况

        list1.add("heloo");
        list2.add(123);
  • 第一种情况 泛型 能够起到作用,第二种情况 却不能
  • new ArrayList()只是在内存中开辟了一个存储空间,可以存储任何类型对象。并不能起到泛型的作用。最主要是看前半部分的引用


public class Test {  

    public static void main(String[] args) {  

        ArrayList<String> list1 = new ArrayList();  
        list1.add("1"); //编译通过  
        list1.add(1); //编译错误  
        String str1 = list1.get(0); //返回类型就是String  

        ArrayList list2 = new ArrayList<String>();  
        list2.add("1"); //编译通过  
        list2.add(1); //编译通过  
        Object object = list2.get(0); //返回类型就是Object  

        new ArrayList<String>().add("11"); //编译通过  
        new ArrayList<String>().add(22); //编译错误  

        String str2 = new ArrayList<String>().get(0); //返回类型就是String  
    }  
}
  • new ArrayList() 你如果前面不指定泛型,就会根据后面的 指定泛型
ArrayList<Object> list1 = new ArrayList<Object>();  
list1.add(new Object());  
list1.add(new Object());  
ArrayList<String> list2 = list1; //编译错误

实际上,在第4行代码的时候,就会有编译错误。那么,我们先假设它编译没错。那么当我们使用list2引用用get()方法取值的时候,返回的都是String类型的对象(上面提到了,类型检测是根据引用来决定的),可是它里面实际上已经被我们存放了Object类型的对象,这样就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷)。

ArrayList<String> list1 = new ArrayList<String>();  
list1.add(new String());  
list1.add(new String());

ArrayList<Object> list2 = list1; //编译错误

没错,这样的情况比第一种情况好的多,最起码,在我们用list2取值的时候不会出现ClassCastException,因为是从String转换为Object。可是,这样做有什么意义呢,泛型出现的原因,就是为了解决类型转换的问题。
我们使用了泛型,到头来,还是要自己强转,违背了泛型设计的初衷。所以java不允许这么干。再说,你如果又用list2往里面add()新的对象,那么到时候取得时候,我怎么知道我取出来的到底是String类型的,还是Object类型的呢?
所以,要格外注意,泛型中的引用传递的问题。

如何理解泛型的多态?泛型的桥接方法

类型擦除会造成多态的冲突,而JVM解决方法就是桥接方法。

现在有这样一个泛型类:

class Pair<T> {  

    private T value;  

    public T getValue() {  
        return value;  
    }  

    public void setValue(T value) {  
        this.value = value;  
    }  
}

然后我们想要一个子类继承它。

class DateInter extends Pair<Date> {  

    @Override  
    public void setValue(Date value) {  
        super.setValue(value);  
    }  

    @Override  
    public Date getValue() {  
        return super.getValue();  
    }  
}

在这个子类中,我们设定父类的泛型类型为Pair<Date>,在子类中,我们覆盖了父类的两个方法,我们的原意是这样的:将父类的泛型类型限定为Date,那么父类里面的两个方法的参数都为Date类型。

// 都被限制住了类型

public Date getValue() {  
    return value;  
}  

public void setValue(Date value) {  
    this.value = value;  
}

所以,我们在子类中重写这两个方法一点问题也没有,实际上,从他们的@Override标签中也可以看到,一点问题也没有,实际上是这样的吗?
分析:实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:

class Pair {  
    private Object value;  

    public Object getValue() {  
        return value;  
    }  

    public void setValue(Object  value) {  
        this.value = value;  
    }  
}

父类的类型是Object,而子类的类型是Date,参数类型不一样,这如果实在普通的继承关系中,根本就不会是重写,而是重载。 我们在一个main方法测试一下:

// 既然
public static void main(String[] args) throws ClassNotFoundException {  
        DateInter dateInter = new DateInter();  
        dateInter.setValue(new Date());                  
        dateInter.setValue(new Object()); //编译错误  
}

如果是重载,那么子类中两个setValue方法,一个是参数Object类型,一个是Date类型,可是我们发现,根本就没有这样的一个子类继承自父类的Object类型参数的方法。所以说,却是是重写了,而不是重载了。
为什么会这样呢
原因是这样的,我们传入父类的泛型类型是Date,Pair<Date>,我们的本意是将泛型类变为如下:

class Pair {  
    private Date value;  
    public Date getValue() {  
        return value;  
    }  
    public void setValue(Date value) {  
        this.value = value;  
    }  
}

然后再子类中重写参数类型为Date的那两个方法,实现继承中的多态。
可是由于种种原因,虚拟机并不能将泛型类型变为Date,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。这样,类型擦除就和多态有了冲突。JVM知道你的本意吗?知道!!!可是它能直接实现吗,不能!!!如果真的不能的话,那我们怎么去重写我们想要的Date类型参数的方法啊。

于是JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。

首先,我们用javap -c className的方式反编译下DateInter子类的字节码,结果如下:

class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {  
  com.tao.test.DateInter();  
    Code:  
       0: aload_0  
       1: invokespecial #8                  // Method com/tao/test/Pair."<init>":()V  
       4: return  

  public void setValue(java.util.Date);  //我们重写的setValue方法  
    Code:  
       0: aload_0  
       1: aload_1  
       2: invokespecial #16                 // Method com/tao/test/Pair.setValue:(Ljava/lang/Object;)V  
       5: return  

  public java.util.Date getValue();    //我们重写的getValue方法  
    Code:  
       0: aload_0  
       1: invokespecial #23                 // Method com/tao/test/Pair.getValue:()Ljava/lang/Object;  
       4: checkcast     #26                 // class java/util/Date  
       7: areturn  

  public java.lang.Object getValue();     //编译时由编译器生成的巧方法  
    Code:  
       0: aload_0  
       1: invokevirtual #28                 // Method getValue:()Ljava/util/Date 去调用我们重写的getValue方法;  
       4: areturn  

  public void setValue(java.lang.Object);   //编译时由编译器生成的巧方法  
    Code:  
       0: aload_0  
       1: aload_1  
       2: checkcast     #26                 // class java/util/Date  
       5: invokevirtual #30                 // Method setValue:(Ljava/util/Date; 去调用我们重写的setValue方法)V  
       8: return  
}

从编译的结果来看,我们本意重写setValue和getValue方法的子类,竟然有4个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。
所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。
不过,要提到一点,这里面的setValue和getValue这两个桥方法的意义又有不同。
setValue方法是为了解决类型擦除与多态之间的冲突。
而getValue却有普遍的意义,怎么说呢,如果这是一个普通的继承关系:
那么父类的setValue方法如下:

public ObjectgetValue() {  
    return super.getValue();  
}

而子类重写的方法是:

public Date getValue() {  
    return super.getValue();  
}

其实这在普通的类继承中也是普遍存在的重写,这就是协变。
并且,还有一点也许会有疑问,子类中的巧方法Object getValue()Date getValue()是同时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。

如何理解基本类型不能作为泛型类型?

比如,我们没有ArrayList,只有ArrayList, 为何?

image.png
因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储int值,只能引用Integer的值。
另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作

如何理解泛型类型不能实例化?

不能实例化泛型类型, 这本质上是由于类型擦除决定的:

image.png

因为在 Java 编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了,此外由于T 被擦除为 Object,如果可以 new T() 则就变成了 new Object(),失去了本意。 如果我们确实需要实例化一个泛型,应该如何做呢?可以通过反射实现:

static <T> T newTclass (Class < T > clazz) throws InstantiationException, IllegalAccessException {
    T obj = clazz.newInstance();
    return obj;
}

泛型数组:能不能采用具体的泛型类型进行初始化?

List<String>[] list11 = new ArrayList<String>[10]; //编译错误,非法创建 
List<String>[] list12 = new ArrayList<?>[10]; //编译错误,需要强转类型 
List<String>[] list13 = (List<String>[]) new ArrayList<?>[10]; //OK,但是会有警告 
List<?>[] list14 = new ArrayList<String>[10]; //编译错误,非法创建 
List<?>[] list15 = new ArrayList<?>[10]; //OK 
List<String>[] list6 = new ArrayList[10]; //OK,但是会有警告

采用通配符的方式初始化泛型数组是允许的,因为对于通配符的方式最后取出数据是要做显式类型转换的,符合预期逻辑。综述就是说Java 的泛型数组初始化时数组类型不能是具体的泛型类型,只能是通配符的形式,因为具体类型会导致可存入任意类型对象,在取出时会发生类型转换异常,会与泛型的设计思想冲突,而通配符形式本来就需要自己强转,符合预期。

泛型数组:如何正确的初始化泛型数组实例?

这个无论我们通过new ArrayList[10] 的形式还是通过泛型通配符的形式初始化泛型数组实例都是存在警告的,也就是说仅仅语法合格,运行时潜在的风险需要我们自己来承担,因此那些方式初始化泛型数组都不是最优雅的方式。

// 这代码,有点秀啊

public class ArrayWithTypeToken<T> {
    private T[] array;

    public ArrayWithTypeToken(Class<T> type, int size) {
        array = (T[]) Array.newInstance(type, size);
    }

    public void put(int index, T item) {
        array[index] = item;
    }

    public T get(int index) {
        return array[index];
    }

    public T[] create() {
        return array;
    }
}
//...

ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100);
Integer[] array = arrayToken.create();

所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。

如何理解泛型类中的静态方法和静态变量?

泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数