Genericity - 泛型 - Java - 细节狂魔

x33g5p2x  于2022-02-21 转载在 Java  
字(7.5k)|赞(0)|评价(0)|浏览(296)

前言

对于泛型,我们的学习目的:看得懂就可以了。因为 泛型是 java 当中比较难的一块语法了。

什么是泛型?

一般的类和方法,只能使用具体的类型;基本类型,或者是自定义的类。如果要编写可以用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。 ------- 来自《Java编程思想》
泛型是在JDK1.5引入的新语法,通俗讲:泛型 ->就是适用于很多类型。从代码上讲,就死对类型实现了参数化(将类作为参数进行传递)。

引出泛型

实现一个类似顺序表的类,使得底层的数组可以存放任何类型的数据,也可以根据成员方法来返回数组中某个下标的值
思路:
  1.我们以前学过的数组,只能存放指定类型的元素。例如:
int[] array1 = new int[10];
String[] array2 =new String[10];

2、所有类的父类,默认为 Object 类。数组是否可以创建为 Object ?

代码示例

.>

此时,我们会发现:我们的数组由于类型 是 Object 的类,说明什么类型都可以往 数组里放。因此,我们 set 方法,接收的数据类型为 Object 类型,返回值也是Object 类型。
因为你不知道下一个返回的数据是什么类型的数据。
因此,导致了一个问题的出现,哪怕我们知道下一个数据类型,用相应的数据类型去接收也是不可以的。只能使用 Object 去接收。

只有 进行 强制类型转换,才能赋值。

结论

该代码,任何类型的数据都可以放入,但是取出需要强制类型转换。
虽然这个代码确定达到了通用的效果。但是! 取出数据很麻烦。

由结论 引出 我们的泛型,将存在缺陷弥补。

我们来讲修整好的代码执行一下,看一下效果。

结果表明,编译阶段没有问题。而且有了泛型指定类型之后,在编译阶段会帮我们检查一波输入的数据类型,如果和指定类型不符,则会有错误提示。

结果表明, 运行没有问题。
我们在来看看取出的数据的时候,有没有问题。

而且,不需要手动类型转换了。它会自动进行类型转换。
要向存储其他类型的数据也可以,只需要 再去new MyArray 类,利用泛型去指定你想输入的数据类型。

取出数据。

泛型初步结论:

1、类名后的 代表占位符,表示当前类时一个泛型类。
2、 泛型类型的数数组,不能new/实例化。
3、基本数据类型,不能作为泛型类型的参数。要封装类。
4、类型后加入<数据类类型>,该类型只能处理 规定的数据类型。
5、编译的时候,自动进行类型的检查
6、取出数据的时候,不需要类型转换。

泛型的主要目的:就是指定当前的“容器”,要持有什么类型的对象。让编译器去做检查。
此时,就把需要类型,作为参数传递。要什么类型,就传入什么类型。
以此达到灵活使用,却不会乱套的地步。

语法

语法一

  1. class 泛型类名称<类型实参列表>{
  2. // 这里可以使用类型参数
  3. }
  4. //示例
  5. class ClassName<T1,T2,.......Tn>{
  6. // 这里可以使用类型参数
  7. }

语法二

  1. class 泛型类名称<类型实参列表> extends 继承类/* 这里可以使用类型参数 */ {
  2. // 这里可以使用类型参数
  3. }
  4. class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
  5. // 可以只使用部分类型参数
  6. }

泛型类的使用

语法

  1. 泛型类<类型实参> 变量名; // 定义一个泛型类引用
  2. new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象
  1. MyArray<Integer> list = new MyArray<Integer>();

注意:泛型只能接受类,所有的基本数据类型必须使用包装类!

类型推导(Type inference)

编译器可以通过上下文推导出类型实参时,可以省略类型实参的填写。

  1. MyArray<Integer> list = new MyArray<>(); // 可以推导出实例化需要的实参类型为Integer

裸类型(Raw Type)

裸类型是一个泛型类但没有带着类型实参,例如 MyArrayList 就是一个裸类型

相当于 泛型没有其作用。又回到原点了。所以请记住:使用泛型类,记得加<类型实参>。

前半部分的概括小结

1、泛型是将数据类型参数化,进行传递。
2、使用表示当前类时一个泛型类。
3、泛型目前为止的优点:数据类型参数化,编译时自动进行类型的检查和转换。

泛型是如何编译的?

擦除机制

泛型到底是怎么编译的?这个问题 是 曾经 的一个面试问题
泛型本身就是一个非常难的语法,需要理解好它,还是需要时间的!

下面我们就用通过实战来了解 它 是 如何编译的。

通过命令: javap -c 查看字节码文件,发现所有的 T 都换成 Object。
即:在编译的过程当中,将所有的 T 替换 Object 这种机制,我们称为 擦除机制。
Java 的泛型机制是在编译阶段实现的。
编译器生成的字节码在运行期间并不包含泛型的类型信息。

擦除机制的介绍

有关泛型擦除机制的文章截介绍:链接
这篇文章讲得非常清楚的。
我们摘取部分重要内容讨论

什么意思呢?我们在这里再解读一下。

如果这么去写,我们又会回到最初的问题。因为我们刚开始就是这么写的。
放入数据没有限制,太灵活了。拿出数据需要强制类型准换,太麻烦了。
现在我们就是使用反证法,来简单说明一下。
假设泛型类型的数组可以实例化:public T[] objects = new T[10];

有的人可能会有疑问:不是说泛型在编译期间,会检查数据类型吗?为什么一开始不报错?
其实很简单:类型检查嘛,你指定 类型参数是 String,它就认为 getArray 返回的数组类型就是String[] 类型,所以它不会报错。
还有一个因素: 因为 public T[] objects = new T[10]; 能成立的话,根据擦除机制:T = Object,那么就是说:这个数组它可以存储任何类型的元素。故:getArray 返回的数组元素类型可以五花八门的,我们凭什么认为 String[] 能接收?答案是不能的!
其实我们的这种写法: public T[] objects = (T[])new Object[10];也是不安全的!
存在的问题也是一样的,数组的元素 也有可能存储不同类型的元素。
有的人可能就会说:那搞了半天,这泛型不跟没有一样? 答案:不是的! 它起着一定提示作用,指定我们输入某种类型的数据。能够程度上避免我们犯错。
当然,并不说泛型就这点作用,这是因为此时使用泛型的方法,不是正确的。

那么,正确的方法又是怎么样?不知道大家有没有注意到 上面那篇文章的部分截图最下面划线的地方。

通过Array.newInstance() 方法,向它转入 参数,才能真正创建一个 T[] 类型的数组。
‘如果要创建一个泛型数组,记住一定是使用 反射 来创建的。
记住!不要傻里傻气说:原码里 ArrayList 底层的数组就是 Object类型。原码有它自己的处理方式,绝大部分人又不会涉及到 jdk 的开发,所以无足轻重。有兴趣的,自己去研究。

泛型的上界

在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。
泛型只有上界,没有下界。另外,当泛型类没有指定边界时(class 泛型类名),默认是Object。

语法

  1. class 泛型类名称<类型形参 extends 类型边界> {
  2. ...
  3. }

示例

  1. public class MyArray<E extends Number> {
  2. ...
  3. }

只接受 Number 的子类型作为E的类型实参(E 可以是 Number 或者 Number的子类)。这就叫做 泛型上界。

实例

复杂示例 - 比较接口

  1. public class MyArray<E extends Comparable<E>> {
  2. ...
  3. }

&ensp;

实例 - 写一个泛型类,找出数组中的最大值

所以,此时我们需要实现 Comparator 或者 Comparable 接口,来比较大小。
通过 compareTo 方法 来比较。

但是,你会发现 没有 与 Comparable 和 Comparator 相关的功能。
这是因为你怎么确定 类型参数 T 它实现了 Comparable 或者 Comparator接口的方法?
没有办法确定!
来看怎么解决!

来看看 实际效果

基本数据类型的包装类 和 String 类 都实现了 Comparable 接口

泛型方法

定义语法

  1. 方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { ... }
  2. // <类型形参列表> 一般只有在静态方法中,写在static后面

实例 - 非静态

实例 - 静态

细心的朋友发现,你在通过类名调用 静态方法的时候,并没有指定类型啊。
为什么就可以通过呢?
其实,是省略了。来看下面的图

泛型中的父子类关系

  1. public class MyArrayList<E> { ... }
  2. // MyArrayList<Object> 不是 MyArrayList<Number> 的父类型
  3. // MyArrayList<Number> 也不是 MyArrayList<Integer> 的父类型

理论上来说:Object 是 所有类的父类。
至于 为什么说: MyArrayList 不是 MyArrayList 的父类型, MyArrayList 也不是 MyArrayList 的父类型。 这是因为 这些 类名后面的 尖括号 和 里面的类型参数 都会被擦除掉。

findMax方法 里面的 T 会被擦成 Object,而<类型参数>会被完全擦除。
也就说:在JVM当中是没有泛型的概念的。
所以 上面的 4个类时构成不了父子关系的。

通配符

? 用于在泛型的使用,即为通配符。

通配符解决什么问题

通配符是用来解决泛型无法协变的问题。
协变 指的就是如果 Student 是 Person 的子类,那么List 也应该是 List的子类。
但是泛型是不支持这样样子父子类关系的。
1、泛型 T 是指定的类型,一旦你传给了我就定下了。而通配符则更为灵活或者说是不确定,更多的是用于扩充参数的范围。
2、或者我们可以这样理解:泛型 T 就是一个变量,等着你将来传给它一个具体的类型;而通配符则是一种规定:规定你只传某一个范围的参数类型。【比如说整形 short、int 都是整形范围里的类型】

实例:假设现有一个 list,输出list当中的数据

代码1

  1. 泛型指定了某种类型,也就是说:类型参数 T 一定是将来指定的一个泛型参数。
  2. public static<T> void printList1(ArrayList<T> list) {
  3. for (T x:list) {
  4. System.out.println(x);
  5. }
  6. }

代码二

  1. //代码2中使用了通配符,和代码1相比,此时传入printList2的数据类型,
  2. //具体是什么数据类型,我们是不清楚的。这就是通配符。
  3. // 这也是为什么 foreach 循环中,x 是 Object 类型,就是因为不确定数据类型,
  4. public static void printList2(ArrayList<?> list) {
  5. for (Object x:list) {
  6. System.out.println(x);
  7. }
  8. }

代码比较

效果图

从效果上来看:两者效果是一样的。
唯一的区别即使程序上的区别,print1 的T 一定是指定了某种数据类型的。接收还是用 T 去接收。
而 print2 使用了通配符 ?,指定了一个类型范围,虽然扩充了参数的范围,但同时也意味着无法 确定具体类型,所以使用 Object 去接收读取的数据。

通配符上界

语法

  1. <? extends 上界>
  2. <? extends Number>//可以传入的实参类型是Number或者Number的子类

示例 1

  1. // 可以传入类型实参是 Number 子类的任意类型的 MyArrayList
  2. public static void printAll(MyArrayList<? extends Number> list) {
  3. ...
  4. }
  5. // 以下调用都是正确的
  6. printAll(new MyArrayList<Integer>());
  7. printAll(new MyArrayList<Double>());
  8. printAll(new MyArrayList<Number>());
  9. // 以下调用是编译错误的
  10. printAll(new MyArrayList<String>());
  11. printAll(new MyArrayList<Object>());

示例 2

  1. Animal
  2. Cat extends Animal
  3. Dog extends Anima
  4. Cat Dog 都继承了 Animal,也就是说 Animal Cat Dog 父类,即 Cat Dog Animal的子类。

根据上述关旭,写一个方法,打印一个存储了Animal 或者 Animal 子类的 list、

代码1

  1. public static void print(List<Animal> list) {
  2. }

但是这样写不可以解决这个问题,因为 print 的 参数类型是 List list,就不能接收 List list。.
因为 List 是一个泛型,根据前面所讲 Cat 和 Dog 跟 泛型的父类 是不构成父子关系的。
所以说 通配符的出现就是为解决这一类的问题(协变类型:父子类关系)。

代码2

  1. public static <T extends Animal> void print2(List<T> list) {
  2. for (T animal : list) {
  3. System.out.println(animal);
  4. }
  5. }

此时T类型是Animal的子类或者自己。该方法可以实现.
因为 我们通过 extends Animal 确保了 T 是 Animal 的 子类,或者本身。
这里是 泛型上界

代码3 - 通配符实现

  1. public static void print3(List<? extends Animal> list) {
  2. for (Animal ani : list) {
  3. System.out.println(ani);//调用谁的toString 方法?
  4. }
  5. }

这方法比上一个方法更好。
通过利用 通配符的上界,将 类型参数 限制为 Animal 或者 Animal 的子类。
所以 在 foreach 循环,我们可以是使用 Animal 来接收 读取的数据。
也就是 只是 传入 Cat 或者 Dog 都是可以的。
但是输出的时候,存入的如果是Cat ,那么输出的时候就会调用 Cat 的 toString 方法、
同理:Dog 的话,那么sout的时候就会调用 Dog 的toString 方法。
Animal 就调用 Animal 的 toString 方法。
注意!如果没有 在 通配符后面添加上界,那么foreach就需要使用 Object 类型去接收。

三个代码的区别

1、对于实现泛型的 print2 方法, 对 T 进行了限制,只能是Animal 或者Animal 的子类(泛型上界),当传入Cat时,类型也就定下来了,就是Cat。
'2、对于通配符实现的print3方法,首先不用在static后面使用尖括号,其次相当于对Animal 进行了规定,允许你传入animal的子类。具体哪个子类,此时并不清楚。
【比如:传入了Cat,实际上声明的类型是 Animal,使用多态才能调用Cat的toString方法】

通配符的上界 - 父子类关系

  1. // 需要使用通配符来确定父子类型
  2. MyArrayList<? extends Number> MyArrayList <Integer>或者 MyArrayList<Double>的父类类型
  3. MyArrayList<?> MyArrayList<? extends Number> 的父类型

对于 MyArrayList<? extends Number>,我们只能传入 Number 或者 Number的子类。
而 后面的 MyArrayList 或者 MyArrayList中的 和 ,但是属于<? extends Number>的子类。
即:MyArrayList<? extends Number> 是 MyArrayList 或者 MyArrayList的父类类型

对于 MyArrayList<?> ,<?> 就相同与 Object了,Objet 是所有类的父类。
那么,MyArrayList<? extends Number>中的 <? extends Number> 就是 它的子类了。
即:MyArrayList<?> 是 MyArrayList<? extends Number> 的父类型

通配符的上界 - 特点

对于这个代码,我们思考:是否可以对这个List 进行写入?

答案是不可以!【具体原因注释写很清楚】
但是请记住: 通配符的上界 适合读取数据,不适合输入数据。’

通配符的下界

语法:

  1. <? super 下界>
  2. <? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型

注意!通配符有下界,但是泛型没有下界。
通配符的上界 与 下界的区别 就在于 extends + (上界) 与 super + (下界)
xtends + (上界) : 传参的类型只能是 上界,或者 上界的子类。
super + (下界):传参的类型只能是 下界,或者 下界的父类。

示例

  1. // 可以传入类型实参是 Integer 父类的任意类型的 MyArrayList
  2. public static void printAll(MyArrayList<? super Integer> list) {
  3. ...
  4. }
  5. // 以下调用都是正确的
  6. printAll(new MyArrayList<Integer>());
  7. printAll(new MyArrayList<Number>());
  8. printAll(new MyArrayList<Object>());
  9. // 以下调用是编译错误的
  10. printAll(new MyArrayList<String>());
  11. printAll(new MyArrayList<Double>());

实例

附图 - 来自java 黑皮书

还是一开始说的那句话,不要较真,以看得懂代码为母的就行了。
能用到通配符,除非你被邀请参加开发 jdk,这个可能非常小。

包装类

这里我就不再多讲,看着我的这篇文章就行了!
List 接口相关知识 - ArrayList数据结构 - Java - 细节狂魔开头就讲了 泛型 和 包装类。自行学习。

相关文章

最新文章

更多