泛型
一. 泛型程序设计(Generic programming)
意味着编写的代码可以被很多不同类型的对象重用。
- 例如实现一个简单的栈:若想要这个栈可重用,可以尝试将其所存放元素定义为Object类型
class ObjectStack {
private Object[] elem;
private int top;
public ObjectStack() {
this(10);
}
public ObjectStack(int size) {
this.elem = new Object[size];
this.top = 0;
}
public void push(Object value) {
this.elem[this.top++] = value;
}
public void pop() {
--this.top;
}
public Object getTop() {
return this.elem[this.top-1];
}
}
public class ObjectStackDemo {
public static void main(String[] args) {
ObjectStack objectStack = new ObjectStack();
objectStack.push(10); //自动装包
objectStack.push(5.8);
objectStack.push("Daria");
String str = (String)objectStack.getTop();
}
}
但在实际运行过程中遇到这样的问题:
- 想要获取一个值时,要将Object类型根据栈顶所存放数据的类型进行强制类型转换;
- 且当获取一个值就要进行一次强转,此外,没有错误检查,即可以压栈任何类型的数据以及类的对象。
- 泛型提供了一个更好的解决方案:类型参数(type parameters)。
对上面的代码做出修改,这使代码具有很好的可读性,当定义一个Integer类型的栈,可以看到当压栈double类型的元素时会提示报错,且要读出元素时不用进行强转。
class GenericStack<T> {
private T[] elem;
private int top;
public GenericStack() {
this(10);
}
public GenericStack(int size) {
this.elem = (T[])new Object[size];
this.top = 0;
}
public void push(T value) {
this.elem[this.top++] = value;
}
public void pop() {
--this.top;
}
public T getTop() {
return this.elem[this.top-1];
}
}
public class GenericStackDemo {
public static void main(String[] args) {
GenericStack<Integer> genericStack = new GenericStack<Integer>();
genericStack.push(10);
//genericStack.push(10.5); 只能入栈 Integer
int data = genericStack.getTop();
GenericStack<String> genericStack2 = new GenericStack<String>();
genericStack2.push("Daria");
genericStack2.push("Student");
String str = genericStack2.getTop();
}
}
- 泛型的意义:
- 在编译期间对类型进行自动检查,并不是替换;
- 自动对类型进行转换。
二、泛型类
- 一个泛型类(generic class)就是具有一个或多个类型变量的类。
- GenericAlg类引入了一个类型变量T,用尖括号括起来放在类名的后面。
- 泛型类可以有许多个类型的变量,例如可以定义GenericAlg类有两个域,使用不同的类型
class GenericAlg<T,U> {...}
- 用具体的类型替换类型变量就可以实例化泛型类型,例如:
GenericAlg<Integer>
GenericAlg<String>
- 可以将结果想象成带有构造器的普通类和方法,例如:
public Integer findMaxValue(Integer[] array) {...}
public String findMaxValue(String[] array) {...}
- 例如写出一个通用算法找到数组的最大值
- 在类型比较大小时需要实现Comparable接口,利用接口中定义的CompareTo方法进行比较;
- 例如在main方法里分别定义Integer和Double类型的数组,求它们中元素的最大值。
class GenericAlg<T extends Comparable<T>> { //泛型类
public T findMaxValue(T[] array) {
T max = array[0];
for (int i = 0;i < array.length;i++) {
if (array[i].compareTo(max) > 0) {
max = array[i];
}
}
return max;
}
}
public class GenericDemo2 {
public static void main(String[] args) {
Integer[] array = {1,2,3};
GenericAlg<Integer> genericAlg = new GenericAlg<Integer>();
System.out.println(genericAlg.findMaxValue(array)); //通过类的对象调用类的成员方法
Double[] array2 = {1.0,2.0,3.0};
GenericAlg<Double> genericAlg2 = new GenericAlg<Double>();
System.out.println(genericAlg2.findMaxValue(array2));
}
}
运行结果:
三、泛型方法
- 定义带有类型参数的泛型方法
- 泛型方法是在普通类中定义也可以定义在泛型类中;
- 类型变量放在修饰符
public static
的后面,返回类型的前面; - 当调用泛型方法时,方法名前的尖括号内放入具体的类型,但编译器实际上可以通过实参的类型推演出泛型类型。
- 写出一个泛型方法找到数组的最大值:
- 同样需要实现Comparable接口来进行类型的比较;
class GenericAlg2 { //泛型方法
public static <T extends Comparable<T>> T findMaxValue(T[] array) {
T max = array[0];
for (int i = 0;i < array.length;i++) {
if (array[i].compareTo(max) > 0) {
max = array[i];
}
}
return max;
}
}
public class GenericDemo2 {
public static void main(String[] args) {
Integer[] array = {1,2,3};
Double[] array2 = {1.0,2.0,3.0};
System.out.println(GenericAlg2.findMaxValue(array));//编译器会通过实参的类型推演出泛型类型
System.out.println(GenericAlg2.<Double>findMaxValue(array2));//指明泛型类型
}
运行结果:
四、类型变量的限定
- 在上面的例子中将T限制为实现了Comparable接口(只包含了CompareTo一个抽象方法)的类,通过对类型变量T设置限定来实现:
- T只是一个类型占位符,表示当前类是一个泛型类;
public static <T extends Comparable<T>> T findMaxValue(T[] array)...
- Comparable接口本身就是一个泛型类型
- 在实现Comparable接口时使用的是关键字
extends
而不是implements
:
<T extends 限定类型>
表示T应该是绑定类型的子类型,T和绑定类型可以是类也可以是接口,选择extends
的原因是这样更接近子类的概念,且Java中一个类型变量或通配符可以有多个限定,限定类型用“&”隔开;
五、泛型的类型擦除机制
- 泛型的编译
- 类型向上(父类)擦除到Object类;
- 在编译期间,编译器把泛型全部擦除为Object类;
- 若有限定类型,则其为泛型类型擦除的上界,例如:
extends Comparable<T>
六、内存泄漏
- 用上面实现的通用栈存放Student类对象(仅作实例,只写一个空类未实现其属性以及方法)
- 压栈两个Student对象再出栈一个;
class Student {
}
public class GenericStackDemo {
public static void main(String[] args) throws InterruptedException {
GenericStack<Student> genericStack = new GenericStack<Student>();
genericStack.push(new Student()); //放入自定义类对象
genericStack.push(new Student());
genericStack.pop();
System.gc();
Thread.sleep(100000);
//在当前对象没有被引用时垃圾回收器自动回收
}
- 可以看到在出栈之后栈中依然有两个Student对象,说明出栈只是下移了栈顶指针Top的位置,而依然有引用指向被出栈的那个Student对象;内存没有及时释放;
- 修改通用栈中出栈的pop方法:
public void pop() {
this.elem[top-1] = null;
--this.top;
}
- 可以看到出栈之后只剩下一个Student对象;
七、约束与局限
- 不能用基本类型实例化类型参数
- 运行时类型查询只适用于原始类型
- 不能创建参数化类型的数组
- 不能实例化类型变量
- 不能构造泛型数组