1. 泛型
面向对象的一个重要目标是:对代码重用的支持。
支持这个目标的个重要的机制就是泛型机制(generic mechanism):
如果除去对象的基本类型外,实现方法是相同的,那么我们就可以用泛型实现(generic implementation)来描述这种基本的功能。
1.1 实现方式一:使用Object表示泛型
Java中的基本思想就是可以通过使用像Object这样适当的超类来实现泛型类。
当我们使用这种策略时,有两个细节必须要考虑。
第一个细节:为了访问这种对象的一个特定方法,必须要强制转换成正确的类型。
第二个细节:是不能使用基本类型。只有引用类型能够与0bjct相容。(基本数据类型呢?延伸到包装类自动拆装箱)
1.2 实现方式二:使用接口类型表示泛型
方式一的缺点:只有使用Object类中已有的那些方法能够表示所执行的操作,才能使用Object为泛型类型来工作
当涉及到在一些项组成的数组中找最大项问题上时,基本的代码跟类型无关,但是它确实需要一种能力来比较任意两个对象,并确定大小,简单的想法是找出Comparable的数组中的最大元,确定顺序可以用compareTo方法
public class FindMaxDemo {
/**
* 获得数组中最大的元素
*/
public static Comparable findMax(Comparable[] arr) {
int maxIndex = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i].compareTo(arr[maxIndex]) > 0) {
maxIndex = i;
}
}
return arr[maxIndex];
}
public static void main(String[] args) {
Shape[] sh1 = {
new Rectangle(3, 4),
new Rectangle(9, 1),
new Rectangle(9, 3)
};
String[] st1 = {"zeng", "chuiyu", "yu"};
System.out.println(findMax(st1));
System.out.println(findMax(sh1));
}
}
**局限性:**当一个类是库中的类,而接口却是用户定义的接口;类是final类等,不可能扩展它以创建一个新的类
另一种接口实现泛型的解决方案:function object,函数对象
1.2.2 数组类型的兼容性
**继承问题:设Employee IS-A Person,那么是否Employee[] IS-A Person[]呢?**即:某例程接受Person[]作参数,那能不能把Employee[]作为参数传递呢?
答:能,Java数组是类型兼容的,这种兼容的现象叫做协变数组类型,当不兼容的类型插入到数组虚拟机将抛出ArrayStroreExcetion异常
//假设Student IS-A Person
Person[] arr = new Employee[5]; //编译通过
arr[0] = new Student(...); //编译通过,运行时ArrayStroreExcetion
1.3 实现方式三:Java5泛型实现泛型特性
1.3.1 泛型的声明
当声明泛型类时,类的声明包括一个或者多个类型参数,参数放在类名后边的一对尖括号中,在泛型类内部,可以声明泛型类型的域和使用泛型类型作为参数或者返回类型的方法
例如:
//声明泛型类
public class GenericMemoryCell<AnyType> {
private AnyType storeValue;
public AnyType read() {
return storeValue;
}
public void write(AnyType x) {
storeValue = x;
}
}
接口也可以是泛型的
//在Java5前,Comparable接口不是泛型的,它的compareTo方法需要一个Object作为参数,所以类型错误只会报运行时异常ClassCastExcetion
//在Java5后,引入了泛型,变成了编译时错误
public interface Comparable<T> {
public int compareTo(T o);
}
1.3.2 自动拆装箱
Java5
之前,在使用包装类时需要在调用write
之前创建Integer
对象,然后使用intValue
方法从Integer
中提取int
值
Java5
之后,
int
->Integer
: 编译器将在幕后插入一个对**Integer
构造方法**的调用,自动装箱Integer
->int
: 编译器将在幕后插入一个对**intValue
方法**的调用,自动拆箱
1.3.3 带有限制的通配符
背景:
由1.2.2可知,Java中数组是协变的,当Square IS-A Shape
于是Square[] IS-A Shape[]
,如果数组是协变的,那么集合也将是协变的(非泛型集合)
数组的协变性导致代码得以编译,但是将产生运行时异常,而使用泛型的全部原因就在于产生编译器错误而不是类型不匹配的运行时异常,所以,泛型集合不是协变的
问题:
泛型及泛型集合不是协变的,缺少代码的灵活性
解决方式:
Java5用**通配符(wildcard)**来弥补这个不足,通配符用来表示参数类型的子类或者超类
- Collection<? extend Shape> :Shape的子类
- Collection<? super Shape>: Shape的超类
- Collection<? extend Object>: 不带限制使用
例如:
public static double totalArea(Collection<? extends Shape> arr) {
double total = 0;
for (Shape shape : arr) {
if (shape != null) {
total += shape.area();
}
}
return total;
}
1.3.4 泛型static方法
上面的totalArea
方法是泛型方法,但是并没有特定类型的参数表。有时方法需要特定类型,有以下原因:
- 该特定类型用作返回类型
- 该类型用于多于一个的参数类型中
- 该类型用于声明一个局部变量
如果这样,就必须声明一种带有若干类型的显式泛型方法
泛型方法特别像是泛型类,因为类型参数表使用相同的语法,在泛型方法中的类型参数位于返回类型之前。
例1:
/**
* 代替使用Object作为参数的非泛型方法,当在Shape对象的数组中查找Apple对象时我们能得到编译时错误
*/
public <T> boolean contain(T[] arr, T t) {
for (T val : arr) {
if (t.equals(val)) {
return true;
}
}
return false;
}
例2:
/**
* JDK中的List接口
*/
public interface List<E> extends Collection<E> {
/**
* 返回一个数组,其中包含此列表中的所有元素(调用的List实例)
* 正确的序列(从第一个元素到最后一个元素)
* 返回的数组是指定数组的数组
*/
<T> T[] toArray(T[] a);
}
1.3.5 类型限界
概念: 在使用泛型时,泛型限界在尖括号内指定,它指定参数类型必须具有的性质
例:
版本1:泛型static方法查找一个数组中的最大元素
public static <T> T findMax(T[] arr) {
int maxIndex = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i].compareTo(arr[maxIndex]) > 0) { //报错,因为T需要有实现Comparable的特性
maxIndex = i;
}
}
return arr[maxIndex];
}
版本2:方法改成public static <T extends Comparable>
版本3:因为Comparable
接口是泛型的,所以更好的做法:public static <T extends Comparable<T>>
版本4:版本3存在以下问题,假设以下场景
Shape implements Comparable<Shape>
Square extend Shape
Square implements Comparable<Shape>
Square IS-A Comparable<Shape> 但 Square IS-NOT-A Comparable<Square>
考虑到以上存在的场景,最终版本为:
public static <T entends Comparable<? super T>>
1.3.6 类型擦除
泛型是Java语言中的成分而不是虚拟机中的结构
类型擦除:泛型类由编译器通过类型擦除(type erasure)过程转变成非泛型类,编译器生成一种与泛型类同名的原始类(raw class),但是类型参数都被删去,类型变量由他们的类型限界替代,当一个具有擦除返回类型的泛型方法被调用的时候,一些特性被自动地插入,如果使用一个泛型类而不带类型参数,那么使用的是原始类
类型擦除的推论:所生成的代码与程序员在泛型之前所写的代码并没有太多的差异,而且事实上运行的也并没有更快,其显著的优点在于,程序员不必把一些类型转换放到代码中,编译器将进行重要的类型检验
1.3.7 对于泛型的限制
- 基本类型
基本类型不能用做类型参数,必须使用包装类
- instanceof检测
instanceof检测和类型转换工作只对原始类型进行
GenericMemoryCell<Integer> cell1 = new GenericMemoryCell<>();
cell1.write(4);
Object cell = cell1;
/** 方式一,运行时错误 **/
GenericMemoryCell<String> cell2 = (GenericMemoryCell<String>) cell;
String s = cell2.read(); //java.lang.ClassCastException
/** 方式二,运行通过 **/
GenericMemoryCell cell2 = (GenericMemoryCell) cell; //不使用泛型
Integer read = (Integer) cell2.read();
System.out.println(read);
- static的语境
在一个泛型类中,static方法和static域均不可引用类的类型变量,因为在类型擦除后类型变量就不存在了。
另外,由于实际上只存在一个原始的类,因此static域在该类的诸泛型实例之间是共享的。(擦除后就一个类,相当于没有泛型,实例共享静态域)
public class GenericMemoryCell<T> {
private T storeValue;
static {
//error: cannot be referenced from a static context
T t;
}
}
- 泛型类型的实例化
不能创建一个泛型类型的实例。如果T是一个类型变量,则语句是非法的(意思就是T代表泛型的话)
T obj = new T(); //右边是非法的
T由它的限界替代,这可能是Object或者甚至是抽象类,因此对new的调用没有意义。
- 泛型数组对象 (不太理解)
不能创建一个泛型的数组。如果T是一个类型变量,则语句是非法的(意思就是T代表泛型的话)
T[] arr = new T[10]; //右边是非法的
T由它的限界代替,这很可能是Object T,于是(由类型擦除产生的)对T[ ] 的类型转换将无法进行,因为Object[ ] IS-NOT-A T[ ]。
由于我们不能创建泛型对象的数组,因此一般来说我们必须创建一个擦除类型的数组,然后使用类型转换。这种类型转换将产生一个关于未检验的类型转换的编译警告。
- 参数化类型的数组
略
1.4 函数对象
在1.3.5中的泛型方法findMax可以用于找出一个数组中的最大项,但是这种泛型方法有个重要的局限:它只对实现Comparable接口的对象有效,因为它使用compareTo方法作为所有比较决策的基础
其他的问题还有:
- 数组的元素要实现Comparable接口有点过分
- 即使实现了Comparable接口,它具有的compareTo方法有可能还不是想要的方法
解决方法:重写findMax方法,使其接受对象数组以及比较函数两个参数,在比较函数中解释如何决定两个对象中哪个大哪个小,如此以来,这些对象将不再知道如何比较它们自己,这些信息从数组中的对象中完全去除了。
函数对象(funtion object):一个函数通过将其放在一个对象内部而被传递,这样的对象通常叫做函数对象
例子:
/**
* 根据指定比较规则找出对象数组中的最大元素
*
* @param arr 对象数组
* @param cmp 比较规则
* @return 最大元素
*/
public static <T> T findMax(T[] arr, Comparator<? super T> cmp) {
int maxIndex = 0;
for (int i = 1; i < arr.length; i++) {
if (cmp.compare(arr[i], arr[maxIndex]) > 0) {
maxIndex = i;
}
}
return arr[maxIndex];
}
//实现规则
class CaseInsensitiveCompare implements Comparator<String> {
@Override
public int compare(String o1, String o2) {
return o1.compareToIgnoreCase(o2);
}
}
//Test
class TestProgram {
public static void main(String[] args) {
String[] arr = {"ZEEBRA", "alligator", "crocodile"};
System.out.println(GenericMemoryCell.findMax(arr, new CaseInsensitiveCompare()));
}
}