12-泛型
12—泛型
1. 为什么要有泛型
1.1 泛型(Generic)
泛型:标签
举例:
- 中药店,每个抽屉外面贴着标签
- 超市购物架上很多瓶子,每个瓶子装的是什么,有标签
泛型的设计背景
结合容器在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在JDK5.0之前只能把元素类型设计为Object,JDK1.5之后使用泛型解决。因为这个时候除了元素的类型不确定,其他的部分是确定的,例如这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成一个参数,这个类型参数叫做泛型。Collection
,List ,ArrayList 这个 就是类型参数,即泛型。
1.2 泛型的概念
- 所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值类型及参数类型。这个类型参数将在使用时(例如:继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型参数)
- 从JDK1.5以后,Java引入了”参数化类型(Parameteized type)“的概念,允许我们在创建集合时再指定集合元素的类型,正如:List
,这表明该List只能保存字符串类型的对象 - JDK1.5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参
为什么要有泛型呢,直接Object不是也可以存储数据吗?
解决元素存储的安全性问题,好比商品、药品标签,不会弄错
解决获取元素时,需要类型强制转换的问题,好比不用每次拿商品、药品都要辨别
在集合没有泛型时

在集合有泛型时

Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。同时,代码更加简洁、健壮
1.3 概述
1 | 泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。 |
1.4 一个例子
1 | List arrayList = new ArrayList(); |
程序崩溃
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。我们将第一行声明初始化list的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题。
1 | List<String> arrayList = new ArrayList<String>(); |
1.5 特性
泛型只在编译阶段有效
1 | List<String> stringArrayList = new ArrayList<String>(); |
D/泛型测试: 类型相同
通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
2. 自定义泛型结构
2.1 自定义泛型结构
泛型的声明
interface List
和calss GenTest<K,V>,其中,T,K,V不代表值,而是表示类型。这里使用任意字母都可以。常用T表示,是Type的缩写 泛型的实例化
一定要在类名后面指定类型参数的值(类型)。如:
List
strList = new ArrayList (); Iterator
iterator = customers.iterator(); - T只能是类,不能用基本数据类型填充。但可以使用包装类填充
- 把一个集合中的内容限制为一个特定的数据类型,这就是generics背后的核心思想

2.2 泛型类、泛型接口
泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>
泛型类的构造器如下:public GenericClass(){}
而下面是错误的:public GenericClass
(){} 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致
泛型不同的引用不能相互赋值
尽管在编译时ArrayList
和ArrayList 是两种类型,但是,在运行时只有一个ArrayList被加载到JVM 泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,且不等价于Object。经验:泛型要使用一路都用。要不用,一路都不要用
如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象
JDK1.7,泛型的简化操作:ArrayList
firt = new ArrayList<>(); 泛型的指定中不能使用基本数据类型,可以使用包装类替换
1 | class GenericTest { |
在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态 属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法 中不能使用类的泛型。
异常类不能是泛型的
不能使用new E[]。但是可以:E[] elements = (E[])new Object[capacity]; 参考:ArrayList源码中声明:Object[] elementData,而非泛型参数类型数组。
父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:
- 子类不保留父类的泛型:按需实现
- 没有类型 擦除
- 具体类型
- 子类保留父类的泛型:泛型子类
- 全部保留
- 部分保留
结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自 己的泛型
- 子类不保留父类的泛型:按需实现
1 | class Father<T1, T2> { |
1 | class Father<T1, T2> { |
1 | class Person<T> { |
方法,也可以被泛型化,不管此时定义在其中的类是不是泛型类。在泛型 方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型。
泛型方法的格式:
[访问权限] <泛型> 返回类型 方法名([泛型标识 参数名称]) 抛出的异常
泛型方法声明泛型时也可以指定上限
1
2
3
4
5
6
7public class DAO {
public <E> E get(int id, E e) {
E result = null;
return result;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o);
}
}
public static void main(String[] args) {
Object[] ao = new Object[100];
Collection<Object> co = new ArrayList<Object>();
fromArrayToCollection(ao, co);
String[] sa = new String[20];
Collection<String> cs = new ArrayList<>();
fromArrayToCollection(sa, cs);
Collection<Double> cd = new ArrayList<>();
// 下面代码中T是Double类,但sa是String类型,编译错误。
// fromArrayToCollection(sa, cd);
// 下面代码中T是Object类型,sa是String类型,可以赋值成功。
fromArrayToCollection(sa, co);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Creature{}
class Person extends Creature{}
class Man extends Person{}
class PersonTest {
public static <T extends Person> void test(T t){
System.out.println(t);
}
public static void main(String[] args) {
test(new Person());
test(new Man());
//The method test(T) in the type PersonTest is not
//applicable for the arguments (Creature)
test(new Creature());
}
}
3. 泛型的使用
3.1 泛型在集合中的使用
1 | package 集合中使用泛型; |
3.2 泛型类
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。泛型类定义的泛型,在整个类中有效。如果被方法使用,那么泛型类的对象明确要操作的具体类型后,所有要操作的类型就已经固定了。为了让不同的方法可以操作不同类型,而且类型还不确定。那么可以将泛型定义在方法上。
泛型类
1 | class Demo<T> |
1 | class GenericDemo4 |
show: 4
show: haha
3.3 泛型方法
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
1 | class Demo |
1 | class GenericDemo4 |
show: hello boy!
print:Alex i love you !
同时定义泛型类和泛型方法
1 | class Demo<T> |
show: hello boy!
print:Alex i love you !
print:5
print:heiei
特殊之处:
静态方法不可以访问类上定义的泛型
如果静态方法操作的应用数据类型不确定,可以将泛型定义在方法上。
1 | class Demo<T> |
show: hello boy!
print:Alex i love you !
print:5
print:heiei
method: hihi
3.4 泛型定义在接口上
1 | interface Inter<T> |
show :haha
静态泛型
1 | import java.util.ArrayList; |
上述代码是 编译通过,运行异常,为什么会出现这种现象呢?这是因为Java的泛型方法属于伪泛型,在编译的时候将进行类型擦除。普通的泛型方法在类构建时已经明确制定了对应的类型,而在静态泛型方法中,类型是无法直接推测的,缺少了明确的类型,最终造成类型转化异常。
convert函数最终转化后对应的字节码为 Method convert:(Ljava/lang/Object;)Ljava/lang/Object; 参数为Object类型,返回也为Object类型,而在接下来的 checkcast 操作中,由于 List 和 String 类型的不同,所以抛出了异常。
静态泛型相关问题
1 | class Father<E>{ |
静态关键字修饰的是静态方法,然后我们给他加了泛型,至于为什么静态关键字可以修饰泛型方法,这就是Java的规定。
为什么第一个能编译通过,第二个不通过?
首先是静态方法都是通过 类名 来调用。前面的泛型T是声明了一个泛型,在这里可以通过extends等对泛型具体类型进行一个限定。T的具体类型由使用show1传入具体参数类型为准。
1 | //编译通过 |
类泛型是在 创建对象 的时候才指定,静态方法是 通过类名 直接调用,当使用类名来调用show2方法时,引发Father类的初始化,静态变量/方法的声明是在类的初始化之前的,所以静态属性不能使用类泛型
第二个方法改成非静态方法就编译通过了,因为成员方法需要通过new对象来调用.
1 | // 通过 |
3.5 泛型数组
查看sun的说明文档,在java中是”不能创建一个确切的泛型类型的数组”的。也就是说下面的这个例子是不可以的:
1 | List<String>[] ls = new ArrayList<String>[10]; |
而使用通配符创建泛型数组是可以的,如下面这个例子:
1 | List<?>[] ls = new ArrayList<?>[10]; |
这样也是可以的:
1 | List<String>[] ls = new ArrayList[10]; |
1 | List<String>[] lsa = new List<String>[10]; // Not really allowed. |
1 | 这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。 |
下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。
1 | List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type. |
4. 泛型在继承上的体现
如果B是A的一个子类型(子类或者子接口),而G是具有泛型声明的 类或接口,G并不是G的子类型! 比如:String是Object的子类,但是List并不是List 的子类。

1 | package 泛型在继承中的体现; |
5. 通配符的使用
1.使用类型通配符:? 比如:List> ,Map,?>
List<?>是List
2.读取List<?>的对象list中的元素时,永远是安全的,因为不管list的真实类型 是什么,它包含的都是Object。
3.写入list中的元素时,不行。因为我们不知道c的元素类型,我们不能向其中添加对象。
唯一的例外是null,它是所有类型的成员。
将任意元素加入到其中不是类型安全的:
Collection<?> c = new ArrayList();
c.add(new Object()); // 编译时错误
因为我们不知道c的元素类型,我们不能向其中添加对象。add方法有类型参数E作为集 合的元素类型。我们传给add的任何参数都必须是一个未知类型的子类。因为我们不知 道那是什么类型,所以我们无法传任何东西进去。
唯一的例外的是null,它是所有类型的成员。
另一方面,我们可以调用get()方法并使用其返回值。返回值是一个未知的类型,但是我们知道,它总是一个Object。
1 | package 通配符的使用; |
注意点
1 | //注意点1:编译错误:不能用在泛型方法声明上,返回值类型前面<>不能使用? |
1 | //注意点2:编译错误:不能用在泛型类的声明上 |
1 | //注意点3:编译错误:不能用在创建对象上,右边属于创建集合对象 |
有限的通配符
- > 允许所有泛型的引用调用
通配符指定上限
上限extends:使用时指定的类型必须是继承某个类,或者实现某个接口,即<=
通配符指定下限
下限super:使用时指定的类型不能小于操作的类,即>=
举例:
- extends Number> (无穷小 , Number] 只允许泛型为Number及Number子类的引用调用
- super Number> [Number , 无穷大) 只允许泛型为Number及Number父类的引用调用
- extends Comparable> 只允许泛型为实现Comparable接口的实现类的引用调用
6. 泛型应用实例
6.1 泛型嵌套
1 | public static void main(String[] args) { |
1 | package 自定义泛型结构; |