1. 泛型的作用
我们设计了一个栈用来容纳对象,但是我们设计的是可以容纳任何对象,在开始的时候我们并不知道这些对象具体是什么类型,所以我们只能使用Object
来表示这些类型
public class MyStack{
public void push(Object object){
//省略
}
public Object pop(){
//省略
}
}
上述代码已经形成了一个简单的容器,能够放入并且取出对象
Teacher teacher=new Teacher();
MyStack stack =new MyStack();
stack.push(teacher);
我们已经容器里面放入了一个Teacher对象,一切看起来很美好,但是取出来的时候出现了一些问题
Teacher teacher=stack.pop();//编译不通过
Teacher teacher=(Teacher) stack.pop();//编译通过
上述第一句代码无法通过编译,原因也很好理解,MyStack中我们是把teacher作为Object类型往里面放的,所以取出来也必定是Object类,无法直接转成Teacher类。
但是我们在平常的使用过程中通常会使用容器只装一种类型,怎么办呢?我们在使用这个容器的时候能不能声明我们就只装特定类型呢?于是泛型就出现了
public class MyStack<T>{
public void push(T t){
//省略
}
public T pop(){
//省略
}
}
上述代码中MyStack<T>
就是使用了泛型,其中<T>
的T就代表了一种类型,至于这个类型到底是啥类型,只有在初次声明的才知道
MyStack<Teacher> stack =new MyStack<>();//这里声明了stack是一个只能装Teacher的容器
stack.push(new Teacher());
Teacher teacher=stack.pop();//编译成功
现在最后一行代码已经能够正常编译了,原因在于前面已经对stack所装的类型进行了声明
如果我们要实现一个函数,其功能就是传入一个对象,返回一个相同类型的对象
//没有泛型的情况
public Object function(Object o){
//进行一些处理
}
因为我们不知道要传入的参数是什么类型的,所以只能用Object代表对象,但是这样我们无法保证调用这个函数的时候无法返回的只能是Object,无法跟传入的参数类型一致
Teacher teacher=function(new Teacher());//这里无法通过编译,因为返回的是Object参数,无法确定直接转成Teacher类型
Teacher teache=(Teacher)function(new Teacher());//通过编译,但是需要强制转型
但是如果使用泛型我们则可以避免这个情况
//使用泛型的情况
public <T> void function(T t){
//进行一些处理
}
我们再用错误的方式调用这个函数
Teacher teacher=function(new Teacher());//编译成功,编译器根据传入的Teacher类型判定返回的也是Teacher类型,所以无需强制转型
2. 类型声明与推断
在上面的例子中泛型的作用就是声明类型与类型检查
public class MyStack<T>{//这里声明了类型T,但是T到底是什么类型需要在使用的进行确定
}
MyStack<Teacher> stack =new MyStack<>();//这里确定了T是Teacher类型,这意味着MyStack只能存储Teacher类型,如果存储了其他类型在编译器会报错误。
//声明了此函数要对一个T类型进行处理,并且返回一个T类型参数,至于T类型是什么类型得在使用的时候确定
public <T> T function(T t){
//进行一些处理
}
Teacher teacher=function(new Teacher());//编译器根据传入的参数推断T类型是Teacher类型
3. 泛型擦除
我们通过泛型能够在使用函数或者类的时候声明一些当时尚未确定的类,但是当我们查看编译完成的class文件却发现了奇怪的事情
//这是.java文件里面的源代码,能够通过编译
MyStack<Teacher> stack=new MyStack<>();
stack.push(new Teacher());
Teacher teacher=stack.pop();
//这是编译过后.class文件里面的代码
MyStack stack=new MyStack();
stack.push(new Teacher());
Teacher teacher=(Teacher)stack.pop();
我们发现编译过后,原来泛型已经消失了,重新变成了强制转型,这是为什么呢?
我们先想想泛型的作用是什么类型声明
与类型检查
,泛型就是为了帮助我们进行类型检查减少代码编写繁琐的过程和强制约束代码编写规范的一个东东,既然通过编译了,我们也就不再需要类型声明和类型检查了,于是乎,所有在源代码中的泛型在编译后都消失了
换句话说,其实Java的后台里面根本没有泛型,泛型只是在编译器的一个东东
我们的源代码里面MyStack<Teacher> stack
在真正的程序里面其实就是MyStack<Object>
,只不过编译器搞了一个泛型来帮助程序员来约束代码编写。
之前需要强制转型的程序部分通过泛型你可以不用写了,但是实际上是编译器在编译过程中自己加上了强制转型
又换句话说,其实MyStack<Teacher> teacherStack
和MyStack<Student> studentStack
在编译器编译后都是MyStack<Object>
了,两者并没有什么区别,但是在源代码中编译器能限制你teacherStack
只能装Teacher
而studentStack
只能装Student
。
总结来说,泛型就是编译器的一个功能,是一个语法糖,所以在编译结束后程序里面所有的泛型都没有了,这就是泛型擦除。
既然
MyStack<Teacher>
和MyStack<Student>
实际上都是MyStack<Object>
public void function(MyStack<Teacher> stack){} public void function(MyStack<Student> stack){}
所以上述代码会编译不成功,因为两个函数的名称与传入的参数都完全相同
另外由于泛型在编译后会擦除,所以我们无法获取泛型的具体类型信息,所以也无法实例化一个泛型对象,也无法创建一个泛型数组
public class MyStack<T>{ public void test(){ T t=new T();//编译错误 T[] array=new T[]{};//编译错误 } }
4. 通配符,extends,super
这三个作为泛型的比较深入的部分,后面会详细讲解