文章目录
1.什么是泛型?
在我看来,我们可以将泛型编程机制看作一个工具,通过这个工具我们可以在代码编译时检测出某些运行错误,并写出更为通用(抽象),可复用的代码。
从JDK1.5开始,Java允许定义泛型类,泛型接口,泛型方法。这样我们就可以在Java中使用泛型机制写出更为通用的程序。
1.1. 如何定义泛型类,接口,方法
1.1.1.泛型类
泛型相当于一些类型数据的抽象,泛型程序设计意味着编写的泛型代码可以对多种不同类型的对象重用。(体现代码的复用原则)
首先我们要注意泛型类型必须是引用类型(不能是int double …的基本类型)
例子
class GenericStack<E>{
//定义一个链表,链表中存储的元素设定为E
private ArrayList<E> list=new ArrayList<>();
public int getSize() {
return list.size();
}
//泛型方法peek
public E peek(){
return list.get(getSize()-1);
}
public void push(E o) {
list.add(o);
}
//泛型方法pop
public E pop(){
E o = list.get(getSize()-1);
list.remove(getSize()-1);
return o;
}
public boolean isEmpty(){
return list.isEmpty();
}
}
对于上面的类,我们就可以称之为泛型类了,因为它的定义使用了泛型机制。
在定义类的时候在类名后面添加 类似于 的代码:
class GenericStack < E >
时,我们就为这个类定义了泛型E,在类中,我们可以将E当作一个类进行使用,这个类是在使用GenericStack时定义的。
例如:
public static void main(String[] args) {
GenericStack<String> stack = new GenericStack<>();
stack.push("number1");
stack.push("number2");
System.out.println(stack.pop());
}
当我们尝试弹入非String的数据时,在编译期就会报错: 如果没有使用泛型,在程序运行到这一段时才会报错。
比如我们把上面的泛型删掉用Object类型来编写一个相同作用的类,再看看:
class GenericStack1{
//定义一个链表,链表中存储的元素设定为E
private ArrayList list=new ArrayList();
public int getSize() {
return list.size();
}
public Object peek(){
return list.get(getSize()-1);
}
public void push(Object o) {
list.add(o);
}
public Object pop(){
Object o = list.get(getSize()-1);
list.remove(getSize()-1);
return o;
}
public boolean isEmpty(){
return list.isEmpty();
}
}
测试之:
public static void main(String[] args) {
GenericStack1 stack1 = new GenericStack1();
stack1.push(11);
System.out.println((String)stack1.pop());
}
编译没有报错,下面运行的时候报错了:
结论:使用泛型可以有效避免强转所造成的运行时错误。
1.1.2 泛型接口
泛型接口的定义和泛型类一样,在合适的位置加上E即可。
例如:
public interface Maximum<T> {
T getMax(T[] array);
}
操作实例:
public class GenericInterface {
public static void main(String[] args) {
MaximumImp maximumImp = new MaximumImp();
System.out.println(maximumImp.getMax(null));
}
}
interface Maximum<T> {
T getMax(T[] array);
}
class MaximumImp implements Maximum<String>{
@Override
public String getMax(String[] array) {
return "null";
}
}
1.1.3 泛型方法
泛型方法除了在泛型类中定义,也可以在普通类中定义
例子
class ArrayAlg{
public static <T> T getMiddle(T... a){
return a[a.length/2];
}
}
注意:类型变量放在修饰符后,返回类型的前面。
当我们调用一个泛型方法时,我们可以把具体类型包围在尖括号中,放在方法名前面,例如:
public class GenericMethod {
public static void main(String[] args) {
String middle =GenericMethod.<String>getMiddle("join","R","fasd");
System.out.println(middle);
}
public static <T> T getMiddle(T... a){
return a[a.length/2];
}
}
其实在实际的编程中我们可以省略掉< String >而不对结果产生影响,原因是编译器会根据已有信息来对泛型类型进行推导,这也是一种编程技巧。
2.使用泛型编程的好处
前面学习了一些泛型的基础,我们知道了如何使用泛型机制进行编程,那么问题来了,泛型编程适合编写什么样的程序?
泛型比较适合编写一些通用的程序出来,用来提高编程效率,简化编程工作。
使代码具有更好的可读性和安全性,复用性,通用性,提高工作效率。
3.谁想成为合格的泛型程序员?
3.1 作为一个泛型程序员,我们的任务就是要预计到我们的泛型类所有可能的用法。
这个任务会有多难呢?
我们可以看一个典型的问题:ArrayList类中有一个方法addAll用来添加另一个集合的全部元素。
现在:Manager extends Emplyee
一个程序员可能想要将一个ArrayList< Manager >中的所有元素添加到一个ArrayList< Emplyee >中去,这是可以的,但是反过来就不行了
ArrayList<Manager> list = new ArrayList();
ArrayList<Emplyee> list1 = new ArrayLsit();
list1.addAll(list);//这个是可以的,因为Manager继承了Emplyee
list.add(list1);//这个就不行了
但是目前我们所学的知识,泛型编程也无法避免这个问题:
public class GenericWildcard {
public static void main(String[] args) {
ArrayList<Manager> list = new ArrayList();
list.add(new Manager());
ArrayList<Emplyee> list1 = new ArrayList();
list1.add(new Emplyee());
//list1.addAll(list);//这个是可以的,因为Manager继承了Emplyee
list.addAll(list1);//这个就不行了
list.value[0].sysoutI();//这个就不行了
}
}
class ArrayList<E>{
E[] value;
int size;
public ArrayList(){
value = (E[]) new Object[10];
}
public void add(E e){
value[size++] = e;
}
public ArrayList<E> addAll(ArrayList<?> list){
for(int i=0;i<size;i++){
value[i] = (E)list.value[i];
}
return this;
}
}
class Emplyee{
private final int i = 100;
public void sysoutI(){
System.out.println(i);
}
}
class Manager extends Emplyee{
}
那么,如何在编译期间允许前一个调用,而不允许后一个调用方式呢?也就是如何让编译器在程序运行前发现这个错误?
java语言的设计者发明了一个具有独创性的概念来解决这个问题: 通配符类型(通配泛型)
通过通配泛型符,Java代码可以在编译期间发现本来可以避免的错误,并让程序员及时修复。
下面的4,5,6节将对这一机制进行详细学习。
3.2 泛型程序设计的三个水平
基本水平就是仅仅使用泛型类,而不考虑它们如何工作以及为什么这样做。
中等水平就是可以在混合使用不同的泛型类时能系统的解决各种问题,而不是胡乱猜测。
高等水平就是能自己设计出能够应用于多种场景的泛型类。
注意:Java库中使用变量E表示集合的元素类型,K和V表示键和值的类型。 T(必要时还可以用U,S)表示其他任意类型。
4.原始类型和向后兼容
4.1.我们也可以使用泛型类而无需指定具体类型:(这时泛型默认为Object)
GenericStack stack = new GenericStack();
上面语句等价于:
GenericStack<Object> stack = new GenericStack<Object>();
像这样的不带参数的泛型类称为原始类型,使用原始类型可以向后兼容Java的早期版本的代码。
示例:
public static void main(String[] args) {
List list = new LinkedList<>();
list.add(new Object());
list.add(new Integer(100));
System.out.println(list.get(0));
System.out.println(list.get(1));
}
4.2.类型擦除
在java虚拟机中,对于泛型程序其实会做一个类型消除的操作的,例如下面泛型接口:
public interface Maximum<T extends Comparable<T>> {
T getMax(T[] array);
}
在虚拟机中的class字节码为:
public interface Maximum{
Comparable getMax(Comparable[] array);
}
上面进行类型擦除的接口我们称之为这个泛型类的原始类型,这点很重要,因为我们获取的泛型类的Class对象其实就是原始类型的Class。
从这里我们大概可以知道泛型机制的实现原理,就是编译器通过解析泛型程序,根据泛型程序的定义将程序编译为具体类型的字节码JVM解释。
5.通配泛型
- ?称为非受限通配泛型,它和?extends Object是一样的。
- ?extends T称为受限通配泛型,表示T或T的一个子类型。
- ?super T称为下限通配泛型,它表示T或T的一个父类型。
通过通配泛型,我们便可以在编译期发现3.1节出现的错误。
如下图所示:
通配泛型符可以让我们在定义泛型类时限制泛型的使用场景,将错误的场景排除在外,避免在运行时出现事故。
6.消除泛型和对泛型的限制
泛型是使用类型消除机制的方法来实现的。
编译器使用泛型类型信息来编译代码,但是随后会消除它。
因此泛型信息在运行时是不可用的,这种方法可以使泛型代码向后兼容使用原始类型的遗留代码。
在这过程中 注意:
- 如果一个泛型类型是受限的,那么编译器就用该受限类型替换它。
- 不管实际的具体类型是什么,泛型类是被它的所有实例所共享的。
由于泛型类型在运行时被消除,因此对如何使用泛型类型有如下限制:
- 不能使用 new E();不能使用泛型类型参数创建实例。
- 不能使用new E[];不能使用泛型类型参数创建数组
- 在静态上下文中不允许类的参数是泛型类型。
- 异常类不能是泛型的。
代码地址:
Java基础学习/src/main/java/Progress/exa26 · 严家豆/Study - 码云 - 开源中国 (gitee.com)
如果通过上面的学习,你了解了如何使用泛型,不妨学习一下下面的泛型编程Demo专栏巩固一下:
Java动手做一做之泛型编程