Java泛型
1 为什么使用泛型程序设计
泛型程序设计(generic programming)意味者编写多种不同类型的对象重用。
举例:
收集String和File对象无需编写不同的类,只需要一个ArrayList就可以收集。
1.1类型参数的好处
在Java中增加泛型类之前,泛型程序设计是用继承实现的。
ArrayList类只维护一个Object应用的数组:
public class ArrayList //before generic classes
{
private Object[] elementData;
...
public Object get(int i) {...}
public void add(Object o) {...}
}
缺点:
1.首先取值需要强制转换
ArrayList files = new Arraylist();
String filename = (String) files.get(0);
2.无错误检查,可以想数组列表中添加任何类的值
files.add(new File("..."));
编译和运行都不会出错,但若将get的即通过强制类型转换为String类型,会出错
泛型提供的解决方案 :类型参数 (指示元素的类型)
ArrayList<String> files = new ArrayList();
优点:
1.可读性好
2.编译器充分利用
String filename = files.get(0);//返回类型为String, 而不是Object
files.add(new File("..."));//error 无法通过编译
2 泛型类
泛型类(generc class)就是有一个或多个类型变量的类。
举例:
public class Pair <T>{
private T first;
private T second;
public T getFirst() { return first; } // 返回类型为T的变量
public T getSecond() { return second; }
public void setFirst(T first) { this.first = first; } // 局部类型变量的类型为T
public void setSecond(T second) { this.second = second; }
public Pair(T first, T second) { this.first = first; this.second = second; }
}
类型变量T 用<>括起来放在类命的后面,泛型类可以有多个类型变量。
public class Pair<T, U> {...}
注:
类型变量一般使用大写字母,而且简短。Java中E表示集合的元素类型,K和V分别表示键和值的类型。T(U和S)表示“任意类型”。
可以用具体的类型替换类型变量来实例化(instantiate)泛型类型。
举例:
Pair<String>
3 泛型方法
类型变量放在修饰符之后,返回值之前。
泛型方法既可以在普通方法中定义,也可以在泛型类中定义。
举例:
class ArrayAlg {
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
}
调用泛型方法,可以将具体类型放在尖括号中,放在方法名前面。
举例:
String middle = ArrayAlg.<String>getMiddle("1","2","3");
大部分下情况下可以省略,编译器会自动推断。
不可省略的情况举例:
double middle = ArrayAlg.getMiddle(3.14, 1, 2);
编译器将参数自动装箱为1个Double对象和2个Integer对象,然后寻找这些类的共同超类型。事实上找到了2个超类型Number和Comparable接口。编译工具会隐晦的提示:
Static member 'ArrayAlg.getMiddle(java.lang.Number & java.lang.Comparable<? extends java.lang.Number & java.lang.Comparable<?>>...)' accessed via instance reference
4类型变量的限定
有时候, 类或方法需要对类型变量需要加以约束。
举例:
public class ArrayAlg<T>{
public static <T> T min(T[] a) {
if (a == null || a.length == 0) return null;
T smallest = a[0];
for (int i = 0; i < a.length; i++) {
if (smallest.compareTo(a[i]) > 0) smallest = a[i];
}
}
}
这段代码有一定的问题,min方法中,smallest的类型为T,意味它可以是任何一个类的对象,但不是每一个类都有一个compareTo方法。
如何改正:只能限制T为实现了Comparable接口(包含一个compareTo的方法)的类。
举例:
public static <T extends Comparable> T min(T[] a)...
// 为什么Comparable是接口却用extends ?
// 仅仅因为它更加接近子类型的概念
一个类型变量或通配符可以有多个限定
举例:
ArrayAlg<T extends Comparable & Serializable, E extends Collection>
5 泛型代码
1类型擦除
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型(raw type)。这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量都会被擦除(erased),并且替换为限定类型(或者,对于无限定的变量则替换为Object)。
原始类型用第一个限定来替换类型变量,或者没有给限定就替换为Object。
举例:
Pair<T> //原始类型为Pair
Interval<T extends Comparable & Serializable> //第一个限定类型为Comparable
2转换泛型表达式
编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。
举例:
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
编译器擦除类型后返回类型是Object,调用getFirst会自动插入转换到Employee的强制类型转换。
即:1.对原始方法Pair.getFirst的调用 2.将返回的Object类型强制转换为Employee类型
3转换为泛型方法
类型擦除也会出现在泛型方法
举例:
public static <T extends Comparable> T min(T[] a);
// 泛型擦除后
public static Comparable min(Comparable[] a);
6限制性与局限性
1不能使用基本类型实例化类型参数
举例:
不能使用Pair 只有Pair
原因在于类型擦除,擦除之后Pair类含有Object类型的字段,而Object类型不能存储double值。
2运行时类型查询只适用于原始类型
虚拟机对象中总有一个特定类型的非泛型类型。因此,所有类型查询只产生原始类型。
举例:
if (a instanceof Pair<String>) //error
if (a instanceof Pair<T>) //error
Pair<String> p = (Pair<String>) a; // warning--can only test that a is a Pair
为提醒这一风险,如果试图查询一个对象是否属于某个泛型类型,会得到一个错误或警告。
同样道理,由于泛型擦除会有以下结果出现:
Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if(stringPair.getClass() == employeePair.getClass()) //true
3不能创建参数化类型的数组
举例:
var table = new Pair<String>[10]; // error
**原因:**一般来说数组会记住自己的元素类型,如果试图存储其他类型的元素,会抛出ArrayStore-Exception异常,而泛型擦除后 var table = new Pair[10];会使这种机制无效。
Object[] arr = table;
arr[0] = new Pair<Employee>(); //编译通过, 但是会导致类型错误
5不能实例化类型变量
不能再类似new T(…)的表达式中使用类型变量
举例:
public Pair() {first = new T(); second = new T();} //error
类型擦除将T变成Object
6不能构造泛型数组
举例:
public static <T extends Comparable> T[] minmax(T... a) {
T[] mm = new T[2]; // error
}
类型擦除会使得构造Comparable[2]数组
String[] names = ArrayAlg.minmax("Tom", "Dick");
// 在Comparable[]引用强制转换为String[]时候,会出现ClassCastException
7泛型类的静态上下文中类型变量无效
不能在静态字段或方法中引用类型变量。
举例:
public class Singlenton<T> {
private static T singleInstance; //error
private static T getSingleInstance() {//error
if(singleInstance == null)
return singleInstance;
}
}
8不能抛出或捕获泛型类的实例
不能抛出或捕获泛型类的的对象。事实上泛型类扩展Throwable都是不合法的。
举例:
public class Problem<T> extends Exception {...} // error
public static <T extends Throwable> void doWork(Class<T> t) {
try {
// do work
} catch (T e) { // error
}
}
7泛型类型的继承规则
无论S与T有什么关系,通常Pair与Pair都没有关系。
举例:
Manager[] topHonchos = ...;
Pair<Employee> result = ArrayAlg.minmax(topHonchos); //error
minmax返回Pair ,而不是Pair,这样的赋值不合法。
这一点与数组不同
Employee[] employeeBuddies = new Manager[]{ceo, cfo}; // ture
将一个参数化类型转换为原始类型的时。也可能产生类型错误。
举例:
Pair rawBuddies = new Pair<Manager>;
rawBuddies.serFirst(new File("...")); // error
最后,泛型类也可以扩展或实现其他泛型类。
**例如:**ArrayList类实现List接口
8通配符类型
1通配符的概念
在通配符类型中允许类型参数发生变化。
举例:
Pair< ? extends Employee>
public static void printBuddies(Pair<Employee> p) {
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName() + " and " + secong.getName() + " are buddies.");
}
这个方法的局限性就是不能将Pair 传给这个方法。但是利用通配符就可以解决。
Pair是Pair< ? extends Employee>的子类型
Pair< ? extends Employee>
? extends Employee getFirst()
void setFirst(? extends Employee)
以上代码不可能调用setFirst方法,编译器只需要知道Employee的某个子类型,但不知道具体是什么类型,导致直接拒绝传递任何特定的类型。毕竟?不能匹配。
使用getFirst不存在这个问题,将getFirst的返回值赋予一个Employee引用完全合法
2通配符的超类型限定
举例:
Pair< ? super Manager>
? super Manager getFirst()
void setFirst(? super Manager)
编译器无法知道setFirst方法的具体类型,所以不能接受参数类型为Employee 或Object的方法调用,只能传递Manager类型的对象,或某个子类型的对象。而调用getFirst,不能保证返回对象的类型,只能把它赋值给一个Object。
Pair是Pair< ? super Manager>的子类型
某些情况下使用通配符可以帮程序员去除对调用参数的不必要限制
举例:
LocalDate extends ChronLocalDate
ChronLocalDate extends Comparable<ChronLocalDate>
// 所以 LocalDate 实现的是Comparable<ChronLocalDate> 而不是Comparable<LocalDate>
// 这种情况下可以利用超类型解决
public static <T extends Comparable<? super T>> T min(T[] a)...
3无限定通配符
举例:
Pair<?>
? getFirst();
void setFirst(?)
getFirst的返回值只能赋给一个Object。setFirst方法不能被调用。