Java基础知识学习:泛型
目录,更新ing,学习Java的点滴记录
目录放在这里太长了,附目录链接大家可以自由选择查看--------Java学习目录
为什么要使用泛型
一般的类和方法,只能使用具体的类型:要么是基本类型要么是自定义类.如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大
- 在面向对象编程语言中,
多态
算是一种泛化机制.例如,你可以将方法的形参类型设为基类
,那么该方法就可以接受从这个基类导入的任何类作为参数.这样的方法更加通用,应用场合也比较多.在类的内部也是如此,凡是需要说明类型的地方,如果都使用基类,确实能够具备更好地灵活性. - 此外,拘泥于单继承体系,也使得程序受限很多.如果方法的形参为一个
接口
,而不是具体的类,这种限制又放松了很多.因为任何实现了该接口的类都能够满足该方法. - 可是有时候,即便使用了接口,对程序的约束还是太强.因为一旦指明了接口,他就要求你的代码必须使用特定的接口.而我们希望编写更加通用的代码,要使得代码能够应用于"某种不具体的类型",而不是一个具体的接口或类.
- 这就是JavaSE5的重大变化之一------
泛型
.泛型实现了参数化类型
的概念,使代码可以应用于多种类型.“泛型"这个术语的意思是"适用于许许多多的类型”.并且在你创建一个参数化类型的实例时,编译器会为你负责转型操作,并且保证类型的正确性
. - 泛型正是我们需要的程序设计手段.使用泛型机制编写的程序代码要比那些杂乱地使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性.
泛型对于集合类尤其有用
- 简单总结一句:
泛型意味着编写的代码可以被很多不同类型的对象所重用
简单泛型的使用
- 先看一个持有单个对象的类,该类可以明确指定其持有对象的类型
但是该类的重用性就很差了,只能持有一种类型的对象
- 在Java5之前,可以让这个类直接持有Object类型的对象
现在的Genericity可以存储任何类型的对象.但是获取值的时候必须强制类型转换 - 有些情况下,我们确实希望容器能够同时持有多种类型的对象.但是,通常来说,我们会使用容器来存储一种类型的对象.泛型的主要目的之一就是
用来指定容器要持有什么类型的对象
,而且由编译器来保证类型的正确性. - 因此,我们采用
类型参数
,用尖括号扩住,放在类名后面.在使用这个类的时候,再用实际的类型替换此类型参数.类型参数的魅力在于:使得程序具有更好的可读性和安全性
这样以后创建Genericity对象,必须指明想要持有什么类型的对象,将其置于尖括号内.以后,你就只能在Genericity中存入该类型机及其子类(因为多态和泛型不冲突)
,并且在取出对象时,会自动进行类型转换.
Java7之后的版本就可以在构造函数中省略泛型参数了,如上图main中第一行,编译器也可以很好的利用这个信息,调用get时,不需要类型转换,编译器就知道返回值类型为Car. - 这就是泛型的
核心概念
:告诉编译器想使用什么类型,然后编译器帮你处理一切细节
定义泛型类
- 一个泛型类就是
具有一个或多个类型参数的类
. - 引入一个Pig类
- Pig类引入了一个类型参数T,用尖括号<>括起来,并放在类名后面.泛型类可以有多个类型参数.例如,可以定义Pig类,其中第一个域和第一个域使用不同的类型
- 类定义中还可以用类型参数
指定方法的返回值类型以及域和局部变量的类型
类型参数使用大写形式,且比较短.
在Java类库中,使用变量E表示集合的元素类型,K和V分别表示键与值类型.T(需要时可以选择U和S)表示"任意类型"
- 用具体类型替换类型参数就可以实例化泛型类型
- 扩展:表面上看,Java的泛型类类似于C++的模板类.但是Java中没有template关键字,但是二者有着本质的区别.
定义泛型方法
- 前面介绍的都是应用在类上,但是同样可以在类中包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是泛型类.总之,
是否拥有泛型方法,与其所在的类是否是泛型没有关系
- 泛型方法能够使方法独立于类而变化.基本指导原则:
无论何时,就应该尽量使用泛型方法,也就是说,如果使用泛型方法可以取代将整个类泛型化,就应该只用泛型方法
. - 定义泛型方法:
只需要将类型参数放在方法返回值之前即可
在该例子中,类并不是泛型化的,只有方法test具有类型参数,这也是由
该方法的返回类型前面的类型参数指明的
- 泛型类和泛型方法指定类型参数的不同
当使用泛型类的时候,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指定参数类型,因为比那一起会为我们找出具体的类型,这称为类型参数推断
.因此,我们可以像调用普通方法一样调用test(),看上去test()被无限次重载过.并且调用test(0时传入基本类型
,自动打包机制就会介入其中,将基本类型的值包装为对应的对象.
定义泛型接口
- 泛型也可以应用于接口,开发中可能用到的生成器就是一个样例
- 定义格式:和泛型类很相似,直接在接口后面添加就可以
- 泛型接口中T的类型决定因素
1) 该接口的实现类在定义时指定T类型
2) 实现类在定义时,如果不指定泛型T的类型,那么该实现类也必须是泛型类,最终类型决定取决于创建该实现类对象的时候去指定
类型参数的限定
- 案例—查找一个数组中最小值
分析一下代码,方法接收的形参是一个T类型的数组,首先对数组进行判断,看是否为null或者空数组,其次将数组第一个元素赋值给T类型的small,最后通过一个for循环遍历所有元素,在调用small的compareTo方法去比较大小,每次将最小值赋值给small,再将small返回,看似没有什么问题?仔细一想,T类型是不固定的,那么它一定持有compareTo方法吗?当然是不一定,这里就是问题点
解决方法就是:对类型参数加以限定,使之必须要满足一定条件
,这里将T限制为实现了Comparable接口的类
- 上面示例中有一点很有意思,可能也让你感到迷惑,既然要对T类型限制为是一个实现Comparable接口的类,为啥不是用
T implements Comparable
却使用了extends关键字,在之前的学习中显然extends是用来表示继承关系的.在下面这种记法中(T extends BoundingType),表示T应该是绑定类型的子类型.T和绑定类型可以是类也可以是接口.选择extends关键字的原因是更接近与子类的概念. - 对类型参数的限定可以有多个
类型擦除
- 无论何时定义一个泛型类型,都自动提供了一个相应的
原始类型
.
原始类型的类名字就是删去类型参数后的泛型类的类名
.
擦除类型参数,如果有进行类型限定
的话,就替换为限定类型中的第一个(因此可以对一个T进行多个类型限定),无限定的变量用Object,看文字可能不明白,下面给出两个例子,一个未进行类型限定,一个进行了类型限定 - 未进行类型限定的原始类型
泛型类
对应原始类
- 进行类型限定之后的原始类型
泛型类
原始类型
- 继续看下面这个例子,你觉得二者输出结果一样吗?
一开始我个人理解也是ArrayList与ArrayList很容易被认为是不同的类型,因为传递的泛型都不一样,但是上面的程序会认为他们是相同的类型
一个残酷的现实就是:在泛型代码内部,无法获得任何有关泛型参数类型的信息---无法知道用来创建某个特定实例的实际的类型参数
. - 总结:Java泛型使用
擦除
来实现的,这意味着当你使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象,因此List和List在运行时事实上都是相同的类型,这两种类型都被擦除
为他们的原生类型—List.
约束与局限性(两个常见的)
- 不能用类型参数代替基本类型
不能用类型参数代替基本类型.因此,没有Pig,只有Pig.原因就是类型擦除.擦除之后,Pig类含有Object类型的域,而Object不能存储double值. - 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型.因此,所有的类型查询只产生原始类型.比如:
通配符类型
- 固定的泛型类型系统使用起来还不够灵活,解决方案就是:通配符类型
- 个人理解通配符就是一些约定俗称的符号,之前我们用到的T,已经表示键值的KV等
T表示某个具体的类型,?表示不确定的Java类型 - 无界通配符,
无界通配符在方法中声明形参时比较重要
从代码中可以发现,runOne方法只能接受泛型为Animal类型的参数,而runTwo使用了无界通配符,可以接收所有类型作为参数,更加灵活
所以,对于不确定的类型或者某个类的继承结构中的子类的类型,可以使用无界通配符 ,表示可以持有任何类型。在runTwo方法中,使用了?之后就不会关心具体传入的参数了 - 上界
extends:参数化的类型可能是所指定的类型,或者是此类型的子类
使用了extends的位置,可以传递Animal或者其任何子类的参数,并且,extends指定的上界可以有多个,中间用逗号分开
- 下界
super
:表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object