Java泛型详细分析
什么是泛型
Java泛型( generics) 是JDK 5中引⼊的⼀个新特性, 允许在定义类和接口的时候使⽤类型参数( type parameter) 。
声明的类型参数在使⽤时⽤具体的类型来替换。 泛型最主要的应⽤是在JDK 5中的新集合类框架中。
泛型最⼤的好处是可以提⾼代码的复⽤性。 以List接⼜为例,我们可以将String、 Integer等类型放⼊List中, 如不⽤泛型, 存放String类型要写⼀个List接口, 存放Integer要写另外⼀个List接口, 泛型可以很好的解决这个问题。
1 | List arrayList = new ArrayList(); |
程序运行出现异常java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
。这是因为我们第二个存放的元素是Integer
类型不能被强转为String
。
ArrayList可以看成是一个什么都能存的容器。假设我们每次取相应的类型数据都需要相对应的工具。ArrayList中存了String,Integer…..。我们可以使用String相对应的工具取出String类型数据。但是我们却不能用它来取出Integer型的数据。(强取报错)
而且我们很多时候从这个容器中拿东西是不知道每次要取出的数据的类型。所以我们就需要一个专门存放特定一种类型的容器。
List<String> list = new ArrayList<>();
只存String类型的容器
List<Integer> list = new ArrayList<>();
只存Integer类型的容器
1 | List<String> arrayList = new ArrayList<String>(); |
这就是泛型的最常用的使用。
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法
泛型的使用
泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
泛型类
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。
1 | class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{ |
一个最普通的泛型类
1 | //此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 |
1 | //泛型的类型参数只能是类类型(包括自定义类),不能是简单类型 |
1 | 泛型测试: key is 123456 |
定义的泛型类,就一定要传入泛型类型实参么?
并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。
1 | //并未传人泛型实参 例如 Generic<String> generic = new Generic<>(); |
1 | 泛型测试: key is 111111 |
不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。
1 | List<Integer> list = new ArrayList<>(); |
泛型接口
泛型接口与泛型类的定义及使用基本相同。
1 | //定义一个泛型接口 |
当实现泛型接口的类,未传入泛型实参时
1 | //未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中 |
当实现泛型接口的类,传入泛型实参时
1 | //在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型(Idea我帮我们自动替换) |
泛型方法
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
下面是定义泛型方法的规则:
- 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的
<E>
)。 - 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
- 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
- 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像int,double,char的等)。
具体看这篇文章中写的。https://blog.csdn.net/s10461/article/details/53941091。太多了,分多种情况。
泛型通配符
无边界的通配符,固定上边界的通配符,固定下边界的通配符
无边界通配符
一般是使用?代替具体的类型参数。例如 List<?> 在逻辑上是List<String>,List<Integer>
等所有List<具体类型实参>的父类。
类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 。重要说三遍!此处’?’是类型实参,而不是类型形参 ! 此处’?’是类型实参,而不是类型形参 !再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。
可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。
1 | class Base{} |
上面代码显示,Base 是 Sub 的父类,它们之间是继承关系,所以 Sub 的实例可以给一个 Base 引用赋值,那么
1 | List<Sub> lsub = new ArrayList<>(); |
最后一行代码成立吗?编译会通过吗?答案是否定的。
编译器不会让它通过的。Sub 是 Base 的子类,不代表 List<Sub>
和 List<Base>
有继承关系。
但是,在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。
无限定通配符经常与容器类配合使用,它其中的 ? 其实代表的是未知类型,所以涉及到 ? 时的操作,一定与具体类型无关。
方法内的参数是被无限定通配符修饰的 Collection 对象,它隐略地表达了一个意图或者可以说是限定,那就是 testWidlCards() 这个方法内部无需关注 Collection 中的真实类型,因为它是未知的。所以,你只能调用 Collection 中与类型无关的方法
1 | public class TestWildCards { |
有人说,<?>
提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空?我想这种需求还是很常见的吧。
有同学可能会想,<?>
既然作用这么渺小,那么为什么还要引用它呢?
个人认为,提高了代码的可读性,程序员看到这段代码时,就能够迅速对此建立极简洁的印象,能够快速推断源码作者的意图。(????这或许就是高手这样写代码吧)
上下界限定符
<? extends T>
和<? super T>
是Java泛型中的“通配符(Wildcards)”和“边界(Bounds)”的概念。
<? extends T>
:是指 “上界通配符(Upper Bounds Wildcards)”,即泛型中的类必须为当前类的子类或当前类。
<? super T>
:是指 “下界通配符(Lower Bounds Wildcards)”,即泛型中的类必须为当前类或者其父类。
1 | public class Food {} |
extends为上界通配符,只能取值,不能放.
super为下界通配符,可以存放元素,但是也只能存放当前类或者子类的实例,以当前的例子来讲。
在testExtends方法中,因为泛型中用的是extends,在向list中存放元素的时候,我们并不能确定List中的元素的具体类型,即可能是Apple也可能是Banana。因此调用add方法时,不论传入new Apple()还是new Banana(),都会出现编译错误。
理解了extends之后,再看super就很容易理解了,即我们不能确定testSuper方法的参数中的泛型是Fruit的哪个父类,因此在调用get方法时只能返回Object类型。结合extends可见,在获取泛型元素时,使用extends获取到的是泛型中的上边界的类型(本例子中为Fruit),范围更小。
在使用泛型时,存取元素时用super,获取元素时,用extends。
频繁往外读取内容的,适合用上界Extends。经常往里插入的,适合用下界Super。
K T V E ? object等的含义
- T - Type(Java 类)
- K - Key(键)
- V - Value(值)
- N - Number(数值类型)
- ? - 表示不确定的java类型(无限制通配符类型)
- Object - 是所有类的根类,任何类的对象都可以设置给该Object引用变量,使用的时候可能需要类型强制转换,但是用使用了泛型T、E等这些标识符后,在实际用之前类型就已经确定了,不需要再进行类型强制转换。
类型擦除
类型擦除(type erasue)指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。
类型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。
先给大家奉上一道经典的测试题。
1 | List<String> l1 = new ArrayList<String>(); |
面的代码中涉及到了泛型,而输出的结果缘由是类型擦除。
类型擦除的主要过程如下:
- 将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
- 移除所有的类型参数。
1 | public static void main(String[] args) { |
反编译后
1 | public static void main(String[] args) { |
我们发现泛型都不见了,程序又变回了Java泛型出现之前的写法,泛型类型都变回了原生类型。
1 | interface Comparable<A> { |
反编译后
1 | interface Comparable { |
1 | public class Collections { |
反编译后
1 | public class Collections |
第2个泛型类Comparable <A>
擦除后 A被替换为最左边界Object
。Comparable<NumericValue>
的类型参数NumericValue
被擦除掉,但是这直 接导致NumericValue
没有实现接口Comparable的compareTo(Object that)
方法,于是编译器充当好人,添加了一个桥接方法。
第3个示例中限定了类型参数的边界<A extends Comparable<A>>A
,A必须为Comparable<A>
的子类,按照类型擦除的过程,先讲所有的类型参数 ti换为最左边界Comparable<A>
,然后去掉参数类型A
,得到最终的擦除后结果。
移除所有的类型参数。将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
泛型带来的问题
重载
1 | public class GenericTypes { |
上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是List<String>
另一个是List<Integer>
,但是,这段代码是编译通不过的。因为我们前面讲过,参数List<Integer>
和List<String>
编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模一样。
异常
1.不能抛出也不能捕获泛型类的对象。事实上,泛型类扩展Throwable都不合法。例如:下面的定义将不会通过编译:
public class Problem<T> extends Exception{......}
为什么不能扩展Throwable,因为异常都是在运行时捕获和抛出的,而在编译的时候,泛型信息全都会被擦除掉,那么,假设上面的编译可行,那么,在看下面的定义:
1 | try{ |
类型信息被擦除后,那么两个地方的catch都变为原始类型Object,那么也就是说,这两个地方的catch变的一模一样,就相当于下面的这样
1 | try{ |
这个当然就是不行的。就好比,catch两个一模一样的普通异常,不能通过编译一样
1 | try{ |
2.不能再catch子句中使用泛型变量
1 | public static <T extends Throwable> void doWork(Class<T> t){ |
Cannot catch type parameters
不能捕获类型参数
那么如果可以再catch子句中使用泛型变量,那么,下面的定义呢:
1 | public static <T extends Throwable> void doWork(Class<T> t){ |
根据异常捕获的原则,一定是子类在前面,父类在后面,那么上面就违背了这个原则。即使你在使用该静态方法的使用T是ArrayIndexOutofBounds,在编译之后还是会变成Throwable,ArrayIndexOutofBounds是IndexOutofBounds的子类,违背了异常捕获的原则。所以java为了避免这样的情况,禁止在catch子句中使用泛型变量。
但是在异常声明中可以使用类型变量。下面方法是合法的.
1 | public static<T extends Throwable> void doWork(T t) throws T{ |
静态变量
1 | public class StaticTest{ |
答案是——2!由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的所有静态变量是共享的。
区别
List<Object>
和原始类型List之间的区别?
原始类型List和带参数类型List<Object>
之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查。
通过使用Object作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String或Integer。
它们之间的第二点区别是,你可以把任何带参数的类型传递给原始类型List,但却不能把List<String>
传递给接受 List<Object>
的方法,因为会产生编译错误。
List<?>
和List<Object>
之间的区别?
List<?>
是一个未知类型的List,而List<Object>
其实是任意类型的List。你可以把List<String>
, List<Integer>
赋值给List<?>
,却不能把List<String>
赋值给 List<Object>
。
?和T有什么区别 ?
T 代表一种类型。
?是通配符,泛指所有类型。
1 | T t = (T) new ArrayList(); |
泛型数组
sun的说明文档,在java中是”不能创建一个确切的泛型类型的数组”的。
也就是说这个例子是不可以的:List<String>[] ls = new ArrayList<String>[10];
而使用通配符创建泛型数组是可以的:List<?>[] ls = new ArrayList<?>[10];
这样也是可以的:List<String>[] ls = new ArrayList[10];
后面的自己看吧。
https://blog.csdn.net/s10461/article/details/53941091
参考文章
https://blog.csdn.net/s10461/article/details/53941091