文章目录
前言
这几天重新回来复习泛型,顺便记录总结一下在学习过程中遇到的重要知识点。
一、为什么要使用泛型?
我们知道ArrayList是一个带有泛型参数的的类,但我们用不带泛型参数的原始类型去操作数据时,可以添加任何类型,编译跟运行都没有报错,但真正对外get值的时候,却出行类型转化异常:
static void test() {
ArrayList list = new ArrayList();
list.add("string");//添加字符串
list.add(1);//添加int类型
list.add(true);//添加booleanleixng
for (int i = 0; i < list.size(); i++) {
String item = (String) list.get(i);//强转成String
System.out.println(item);//输出
}
}
报错:
string
Exception in thread "main" java.lang.ClassCastException:
java.lang.Integer cannot be cast to java.lang.String
at threadDemo.ThreadOperation.test(ThreadOperation.java:44)
at threadDemo.ThreadOperation.main(ThreadOperation.java:35)
所以一般都会用限制类型的泛型,如下:
ArrayList<String> list = new ArrayList<>();
list.add(1);//编译器报错
list.add(true);//编译器报错
另外下面的泛型方法可以传入任何非null的类型,具有极大的代码复用效果:
//测试
public class Test {
//可以用一个方法代替下面3个方法
private static <T extends Number>void setNum( T t){
}
private static void set(int t){
}
private static void set(double t){
}
private static void set(long t){
}
}
我们得出两点结论:
1、泛型具有提前在编译期进行代码检查的作用,防止运行期出现类型转化异常。
2、泛型具有被很多不同类型的对象所重用的优点。
二、定义简单的泛型
1、泛型类
泛型类就是在类名后用< T >代表引入的类型,可以用多个字母,表示多个引用类型,如< T,U >等。 引入类型可以修饰成员变量、局部变量、参数、返回值。public class Test<T,U> {
//修饰成员变量
private T t;
private U u;
//修饰返回值
public T getT() {
return t;
}
//修饰参数
public void setU(U u) {
//修饰局部变量
U u1 = u;
this.t = u;
}
}
泛型类的使用:
Test<String,String> test = new Test<>();
test.setU("haha");
System.out.println(test.getT());
2、泛型方法
该方法可以在普通类、泛型类中使用,< T >在修饰符后,返回值前如下:
//泛型方法
public class Test<T>{
//注意这里的T与类
public <T> T getTestStr(T t) {
System.out.println("== do work ==");
return t;
}
}
泛型方法使用:
Test test = new Test();
//<String>在JDK 1.7后可以省略<String>,虚拟机会自动推断类型
System.out.println(test.<String>getTestStr("String"));
要注意泛型方法跟普通方法的区别。
我的记法是泛型方法在定义的时候有尖括号< T >
//泛型方法。 T 跟泛型类无关
public <T> T getSomeThing(T t) {
return t;
}
//普通方法。 T 为泛型类定义
public T getSomeThing1(T t) {
return t;
}
2、泛型接口
泛型接口与泛型类相似,只是在实现时指定泛型类型,也可以不指定类型。
java中的List< T > 的源码也是泛型接口:
public interface List<E> extends Collection<E> {
int size();
boolean isEmpty();
...//省略其他方法
}
其中List< T >在ArrayList< T >中使用:
public class ArrayList<E> extends AbstractList<E> implements List<E>,
RandomAccess, Cloneable, Serializable {
...//省略其他方法
public ArrayList(int var1) {
}
....//省略其他代码
}
三、泛型的限定与类型擦除
1、泛型的限定
对范型变量的范围进行限制,格式< T extends XXX>,其中T表示绑定类型的子类型,XXX为绑定类型,绑定类型XXX可以是类,可以是接口。绑定类型可以有多个,且用&符号隔开。
由于Java的单继承,限定类型中只能有一个类,且必须放在限定列表中的第一个,多个接口放在类后面:
//泛型的限定 其中HashMap 属于类必须放到第一位,接口排在它后面
public class Test<T extends HashMap & Serializable & Cloneable & Runnable> {
}
如果多个参数类型,则用逗号(,)分隔开:
//泛型的限定
public class Test<K extends Object & Serializable & Cloneable, V extends Object & Runnable> {
private HashMap<K,V> hashMap;
}
使用泛型的限定,注意传进去的类型必须为所限定类的本身或者子类,并且要满足全部限定的类,如下面的Test中传入的类型Date 必须同时满足为Serializable跟Cloneable的子类:
//java.util.Date 为Serializable与Cloneable子类编译通过
Test test = new Test<Date,Thread>();
//Double 不是Serializable的子类,编译不通过
Test test1 = new Test<Double,Thread>();
我们总结为:被限定的类型参数必须为限定类的子类或其本身,否则编译不会通过。
2、泛型的类型擦除
由于虚拟机不支持泛型类型对象,编译期会把所有泛型擦除成普通变量,规则为:
存在一个或多个限定类型,就擦除为第一受限类型。
无限定类型则擦除为Object。
我们可以分析下面的字节码文件来验证:
//定义一个类型参数为T,无限制参数类型
public class Test<T> {
private T t;
public void setT(T t) {
this.t = t;
}
}
让TestThread继承Test:
//继承Test,并且继承参数类型为自定义的Thread
//另外TestThread类又定义了K,V两个参数类型,其中K的限制参数类型为 ArrayList,V无限定类型。
public class TestThread<K extends ArrayList,V> extends Test<Thread> {
private K t;
private V v;
public void setT(Thread thread) {
super.setT(thread);
}
}
对TestThread编译后的字节码分析如下:
// class version 51.0 (51)
// access flags 0x21
==签名信息==
// signature <K:Ljava/util/ArrayList;V:Ljava/lang/Object;>LthreadDemo/Test<Ljava/lang/Thread;>;
// declaration: threadDemo/TestThread<K extends java.util.ArrayList, V> extends threadDemo.Test<java.lang.Thread>
public class threadDemo/TestThread extends threadDemo/Test {
// compiled from: TestThread.java
private Ljava/util/ArrayList; t ==t的第一受限参数类型为ArrayList==
private Ljava/lang/Object; v ==v无受限函数,所以擦除为Object==
// access flags 0x1
public <init>()V
/....省略
// access flags 0x1
public setT(Ljava/lang/Thread;)V
/....省略
// access flags 0x1041
public synthetic bridge setT(Ljava/lang/Object;)V ==由于java希望方法能够具有多态==
==则需要解决类型擦除与多态的冲突==
==这里就生成了桥方法==
}
另外对于程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("string");
//编译器将下面的调用分为两条虚拟机指令。
// 1.对原始方法ArrayList.get(int index)的调用。
// 2.将返回的Object类型,请强转成String类型。
String string = arrayList.get(0);
类型擦除的总结:
- 虚拟机中没有泛型,只有普通类和方法。
- 所有类型参数都用它们的限定类型替换。
- 桥方法被合成用来保持多态。
- 为了保持类型安全,必要时插入类型转化。
四、泛型的约束与限制
当然泛型也存在不足,表现为以下几点:
1、不能用基本类型实例化类型参数
泛型不能传入基本数据类型,因为类型擦除后,无限定参数类型只存在Object类,而Object不能存储基本数据类型的值。如:
//int 为基本数据类型,编译错误
List<int> lists1=new ArrayList<>();
//boolean 为基本数据类型,编译错误
List<boolean> lists2=new ArrayList<>();
//正确为使用包装类:
List<Integer> lists3= new ArrayList<>();
List<Boolean> lists4= new ArrayList<>();
2、运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型,因此,所有运行时类型查询只存在原始类型。即不能使用 instanceof 来进行判断运行时对象类型。如:
if (a instanceof ArrayList< String >)//error
if (a instanceof ArrayList< T >)//error
if (a instanceof ArrayList)//正确
ArrayList< String > list=(ArrayList< String >)a//warning
3、不能创建参数化类型的数组
由于数组是一个协变类型,比如说数组中的元素A继承于B,那么数组A也继承于数组B,但带有参数类型A与带有参数类型B的类之间没有任何关系的:
//下面两个类没有继承关系
new ArrayList<Son>();
new ArrayList<Father>();
//error 不允许实例化泛型数组
List<String>[] arrayList = new ArrayList<>()[10];
但是定义成员变量又是允许的:
public class Test{
//定义参数类型数组是允许的 我也不知道这样定义有啥用=。=
private ArrayList<String>[] arrayList;
}
4、可变参数警告
由于java不能创建泛型数组,但是如果我们想在可变参数的方法中传入泛型呢?
如下:
private <T> void setName(T... ts) {
for (T t : ts) {
//todo
}
}
答案是可以的,只是这里编译器会发出警告,需要用@SuppressWarnings或者java SE 7 中的@SafeVarargs来标注。
5、不能实例化类型变量
如:
T t =new T();//不允许 编译错误
6、 不能构造泛型数组
由于泛型在运行时具有类型擦除机制,虚拟机只知道存储的对象为Object而已,而对于数组Array来说,必须知道持有对象的具体类型,而泛型擦除机制却违反了数组的安全检查原则。
public static <T extends Serializable> T[] getT(){
return new T[2]; //不允许
}
7、 泛型类的静态上下文中类型变量无效
public class Test<T>{
//不允许
private static T instance;
//不允许
public static T getInstance(){
//...
return instance;
}
}
8、 不能抛出或捕获泛型类的异常实例
public static <T extends Throwable> void test(){
try {
//...
}catch (T t){//不允许
throw t;
}
}
9、 类型擦除后引发的方法冲突
public class Test<T> {
/**编译器报错:'equals(T)' in 'com.enjoy.entity.bag.Test'
* clashes with 'equals(Object)' in 'java.lang.Object';
* both methods have same erasure,
* yet neither overrides the other
*/
public boolean equals(T t){
}
}
意思是此处的equals方法与object中的equals具有相同的类型擦除,彼此都不能重写对方,产生冲突。解决方法只能修改方法名。
其实泛型的大多数限制都输由于类型擦除引起的。
五、通配符类型
1、通配符概念
泛型中,问号?符号被称为通配符,是用来解决固定泛型类型的约束性,它允许类型参数变化。
只能用在适用范围有:
1、参数类型
2、字段类型
3、局部变量类型
4、返回值类型
public class Test<T> {
//字段类型
private List<? extends Serializable> list;
//参数类型 、返回值类型
public List<? extends Serializable> test1(List<? extends Serializable> list){
//局部变量类型
List<? extends Serializable> list2=null;
return list2;
}
}
2、上限界定符
< ? extend X> 被称为上限界定符,只能匹配X及其子类。只能从从泛型类中读取数据,并且不能写入,(是PECS原则中的生产者,Producer Extends,往外输出东西,所以你只能获取(get),不能够提供它)
class Grandfather{
}
class Father extends Grandfather{
}
class Son extends Father{
}
public class Test {
private static void set(List<? extends Father> list){
//只提供安全的访问,不允许修改
list.get(0);//允许
list.set(new Son());//不允许
}
public static void main(String[] args) {
List<Grandfather> grandfathers =new ArrayList<>();
List<Father> fathers =new ArrayList<>();
List<Son> sons =new ArrayList<>();
//限制为Father及其子类Son ,Grandfather编译器报错
set(sons);//允许
set(fathers);//允许
set(grandfathers);//不允许
}
}
3、下限界定符
< ? superX> 被称为下限界定符,只能匹配X及其超类。只能从从泛型类中写入数据,并且不能读取,(是PECS原则中的消费者,Comsumer Super,它要消费东西,所以你只能提供(set),不能跟他抢)。
public class Test {
private static void set(List<? super Father> list){
//只提供安全的修改,不允许安全的访问
list.add(new Son());//允许
Father object = (Father) list.get(0);//不允许,需要强转,但存在强转异常
}
public static void main(String[] args) {
List<Grandfather> grandfathers =new ArrayList<>();
List<Father> fathers =new ArrayList<>();
List<Son> sons =new ArrayList<>();
//限制为Father及其父类Grandfather ,子类Son编译器报错
set(sons);//不允许
set(fathers);//允许
set(grandfathers);//不允许
}
}
4、无限定通配符
< ? > 为无限定通配符,表示任意通配符,不能Get/Set。
public class Test {
private static void set(List<?> list){
//既不提供安全的修改,也不允许安全的访问
list.add(new Son());//不允许
Father object = list.get(0);//不允许
}
public static void main(String[] args) {
List<Grandfather> grandfathers =new ArrayList<>();
List<Father> fathers =new ArrayList<>();
List<Son> sons =new ArrayList<>();
//匹配任意类型
set(sons);//允许
set(fathers);//允许
set(grandfathers);//允许
set(new ArrayList<>());//允许
}
}
总结
梳理学习要点还是挺不容易的,最后的总结想用一个问题来结束:
Java中的泛型是什么 ? 使用泛型的好处是什么?
答:泛型是一种参数化类型的机制。它可以使得代码适用于各种类型,从而编写更加通用的代码,例如集合框架。
泛型还是一种编译时类型确认机制。它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现ClassCastException。
参考《java核心技术卷1》第八章泛型技术设计