MENU

Java-58 泛型

November 24, 2023 • Read: 81 • Java阅读设置

泛型

为什么使用泛型

泛型,简单地说,就是在定义类、接口和方法时,使得类型(类或接口)作为参数。
可以类比于形参,定义形参,就是定义一个接收值的参数,可以在要操作该值的地方使用该形参。泛型,相当于定义形式类型参数,就是定义一个接收类型的参数,可以在要使用该类型的地方使用该形式类型参数。

注意,泛型对应的形式类型参数所接收的实际类型参数不能是基本数据类型,只能是类或接口对应的类型。

使用泛型的好处:

  • 使得编译时具有更强的类型检查。编译器会对泛型代码进行强类型检查,如果代码违反了类型安全,则会报编译错误;
  • 不再需要类型强转。泛型使得数据的类型确定为指定的类型,使用泛型的数据就作为指定类型的数据。
  • 使用泛型的代码具有类型更安全、更简单易读的优点。

泛型的使用

1 泛型的定义

1) 泛型类或接口的定义
一个泛型类或接口的定义形式为:class name<T1, T2, ..., Tn> { /* ... */ }

  • 类型参数限定在一对尖括号<>中,多个类型参数用逗号,分隔;
  • 类型参数定义在类或接口名后面;
  • 定义类型参数后,类型参数可以用在对应类或接口中的任何使用类型的地方。
    2023-11-24T02:34:51.png

注意点:

  • 定义泛型类的构造器时,不需要带类型参数,与非泛型类的构造器一致。实例化泛型类时,使用对应构造器需要传入对应的类型参数,即要带有类型参数。
    2023-11-26T06:35:07.png
    2023-11-26T06:35:58.png
    2023-11-26T06:37:35.png
  • 不同实际类型对应的泛型类的实例化不能相互赋值。反向思维理解:如果能够相互赋值,比如将一个String赋给一个int,那么int对应的泛型类实例具有String的数据,显然与泛型的作用就矛盾。
    2023-11-26T06:41:24.png
  • 使用泛型类或接口时,却不传入对应的实际类型参数,即不进行参数化类型,则对应泛型类型按照Object处理。
    不建议对泛型类或接口不使用其泛型。
  • 泛型类或接口,其声明的泛型不能用在对应的静态方法上。
    根据static特性来理解:static结构只有一个,不依赖于特定的实例。但泛型类或接口对应的形式类型参数的确定,是在对应泛型类或接口实例化时才确定的,即不同的实例化对象对应不同的实际类型,因此不止一个。
  • 异常类不能是泛型。
    2023-11-26T06:49:26.png
  • 不能new泛型对应的形式类型参数。可以理解为,new是构建确定的(包括确定的类型)结构,而泛型对应的形式类型参数本质上是形参,不包含具体的类型,因此不能在new时使用。有类似场景需求时,可以利用泛型对应的形式类型参数来强转new出来的Object类型以实现对应需求。
    2023-11-26T06:54:08.png
    2023-11-26T06:54:44.png
  • 泛型类的继承,除了以下情况外,也可能父类有多个形式类型参数,而子类部分保留或全部保留的情况,与以下情况本质相同:

    • 子类继承泛型类父类时,忽略其父类对应的形式类型参数或者传入实际类型参数。此时,相当于将其父类对应的泛型按照Object类型或指定实际类型处理,子类不属于泛型类。
      2023-11-26T07:16:50.png
      2023-11-26T07:17:04.png
      2023-11-26T07:17:37.png
    • 子类继承泛型类父类时,忽略其父类对应的形式类型参数或者传入实际类型参数,且声明自己的形式类型参数。此时,仍时将其父类对应的泛型按照Object类型或指定实际类型处理,子类属于泛型类。
      2023-11-26T07:19:28.png
    • 子类继承泛型类父类时,保留其父类对应的形式类型参数。此时,相当于在实例化对应子类时,要传入对应父类的实际类型参数,子类属于泛型类。
      子类保留其父类对应的形式类型参数,就是在继承父类时,保留父类的泛型类型,同时自己要声明父类的泛型。
      2023-11-26T07:20:33.png
    • 子类继承泛型类父类时,保留其父类对应的形式类型参数,且声明自己的形式类型参数。此时,在实例化对应子类时,既要传入对应父类的实际类型参数,还要传入其本身的实际类型参数,子类属于泛型类。
      2023-11-26T07:20:08.png

2) 泛型方法的定义
泛型方法并不等同于使用泛型的方法,泛型方法是指具有自己的形式类型参数的方法,而使用泛型的方法可能是使用泛型类的形式类型参数。

泛型方法可以是static的,或者非static的,也可以是泛型构造器

泛型方法的定义形式为:权限修饰符 [关键字] <T1, T2, ...> 返回值类型 方法名(形参){},即定义泛型方法就是将对应的形式类型参数声明在该方法的返回值类型之前,声明形式类型参数后,就可以在方法的任何可以使用类型的地方使用该形式类型参数。

示例:
2023-11-28T01:37:18.png

2 类型参数命名规范

一般来说,类型参数变量的名称为单个大写字母,这与Java中变量的命名规范有显著不同,也是为了能够区分开变量名。

常用的类型参数变量名:
2023-11-24T02:36:55.png

3 调用和实例化泛型

调用和实例化泛型可以理解为给带有泛型的类或接口或方法,传递对应实际类型,来使用它们,相当于传实参

泛型类型的调用通常称为参数化类型。

比如,调用一个泛型类:
2023-11-24T02:40:57.png

实例化一个泛型类:
2023-11-24T02:41:27.png

可以看到,实例化一个泛型类时,由于声明一个类的对象要使用对应的类名,对于泛型类就是要使用传了实际类型参数的类名,同时在构造对应类的对象时,要使用对应构造器,而类的构造器本身也是其类名加上一对小括号(可能还有参数),对于泛型类就是要再次使用传了实际参数的类名来调用对应构造器。

在Java7时,声明一个类的对象同时构造对应对象时,可以在构造时不传实际类型参数,也就是直接用一对空的尖括号表示泛型类<>,因为这种情况,编译器能够直接推断出对应构造器的实际类型参数,这就是java7时引入的类型推断机制。
2023-11-24T02:46:26.png

对于带有泛型的方法的调用,与调用和实例化泛型类或接口类似,就是在调用时传递对应形式类型参数的实际类型参数即可。
一般来说,在调用泛型方法时往往在传对应方法形参对应的实参时就相当于确定了对应的形式类型参数的实际类型,因为泛型方法通常会在形参上使用其泛型,此时就无需显式的在调用泛型方法时在其方法名前显式传入实际类型参数,编译器能够通过类型推断机制通过形参推断出实际类型参数。

示例:
显式传入实际类型参数:
2023-11-28T01:42:44.png

使用类型推断:
2023-11-28T01:42:54.png

4 多类型参数和嵌套泛型

如上面的泛型的定义形式,一个泛型类或接口,可以有多个类型参数,定义和使用多个类型参数的方式和定义和使用单个类型参数的方式没有区别,可以类比于定义多个形参一样,使用起来也是一样的。

示例:
2023-11-24T02:49:29.png
2023-11-24T02:49:39.png
2023-11-24T02:49:52.png

泛型嵌套是指,有的泛型类或接口,传的实际类型参数也是泛型类或接口。
2023-11-24T02:50:45.png

示例:
2023-12-16T02:47:37.png

5 泛型使用的注意点

对于有继承关系的两个类,根据多态性可知,子类对象可以作为父类类型的对象,即Super sup = new Subclass(),此时虽然子类对象具有独特于子类的内容,但是由于它作为父类类型的对象,因此此时sup相当于被赋予了其子类对象中继承自父类的那块内容。

子父类之间能够类型转换的本质是,子类的一部分就是父类,且多态性使得即使子、父类指向同一个对象,父类只能“看见”对象中父类的那块内容,而子类可以“看见”全部内容,即使父类进行改变也改变不了子类独有的内容。
2023-12-16T00:52:42.png

能够进行类型转换的两个类,要么是子父类,要么是同一个类。

对于带有泛型的两个同一个类但泛型对应的实际类型不同的类,它们之间仅存在并列关系,或者说,它们之间不存在关系,即既不是子父类,也不是同一个类:
1)由于泛型的限定,导致即使是同一个类,但已经变为两个不同的类;
2)即使它们的泛型对应的实例类型为子父类关系,例如List<Object>, List<String>,它们也不存在关系。可以理解为,泛型是一种限定,要判断两个泛型类是否具有关系,先看的不是泛型而是被限定的类是否存在关系,例如List和List不是子父类,属于同一类关系,再看泛型对应的实际类型,例子的情况就是如果泛型对应的类型相同,它们就是同一类关系,如果不同就是不同类关系,不存在子父类关系,因为第一步就已经不满足了。
2023-12-16T01:05:20.png

对于带有泛型的两个子父类,如果泛型对应的实际类型相同,则这两个类就是子父类关系,如果泛型对应的实际类型不同,则这两个类不存在关系。
2023-12-16T01:12:14.png
2023-12-16T01:12:27.png
2023-12-16T01:12:48.png
2023-12-16T01:13:36.png

通配符

1 什么是通配符

使用具有泛型的类或接口时,一般要传入实际类型来对具有泛型的类或接口进行限定。

多个具有不同实际类型的相同泛型类或接口是没有任何关系的,或者说是并列关系。

但有时我们希望能对泛型进行操作而不限定其泛型对应的实际类型,即不论这个泛型类是哪种实际类型,都能进行操作。这时就可以使用通配符来实现。

通配符,顾名思义,通用匹配符号,是一种能与各种泛型对应的实际类型适配的符号,用?表示。
带有通配符的泛型类可以理解为一种可以是任何实际类型对应的该泛型类的表示。

以下是使用通配符实现对不同泛型对应的实际类型的泛型类进行操作的示例:
2023-12-16T01:32:52.png
2023-12-16T01:33:22.png

2 带有通配符的泛型类的使用注意点

带有通配符的泛型类可以理解为一个可以作为任何实际类型对应的该泛型类的类,换句话说,带有通配符的泛型类代表着任何实际类型对应的泛型类的类。

因此,当改变带有通配符的泛型类时,要保证这种改变是适合任何实际类型的该泛型类的,即保证带有通配符的泛型类对任何实际类型的通配性。

例如,在对带有通配符的泛型类进行添加操作时,添加的元素类型要同时适配所有类型,这样的元素只有null
因此,一般不能对带有通配符的泛型类进行添加操作。

可以对带有通配符的泛型类进行读取操作,类似地,如果要接收读取的元素,只能用Object类型变量来接收。
2023-12-16T02:24:47.png

3 有限制条件的通配符

前面所学习的是没有限制条件的通配符,它可以同时是泛型对应的任何实际类型。

有限制条件的通配符是限制为它可以同时是泛型对应的某一范围的实际类型,有以下三类:

  • <? extends C>: 表示限制为类型C或类C的子类。这里的extends可以理解为<=
  • <? extends I>: 这里I表示接口,表示限制为接口I的实现类;
  • <? super C>: 表示限制为类C或类C的父类。这里的super可以理解为>=

同样地,对带有限制条件的通配符的泛型类进行改变时,这种改变要保证这种改变是适合对应适配的所有实际类型对应的该泛型类,一般没有,也就不能进行改变操作。

对带有限制条件的通配符的泛型类可以进行读取,如果要接收读取的元素,那么接收的变量的类型应该能够同时接收对应的可适配的所有实际类型。因此,Object一定可以。
另外,对于<? super C>,只能用Object来接收,对于<? extends C>,还可以用CC的父类来接收。
2023-12-16T02:38:45.png

Last Modified: December 16, 2023