Java基础-泛型的约束和局限性
Java中的泛型是一个非常重要知识点,在这里,简单的介绍一下Java泛型的几个注意点。这里不会讲解Java中的泛型是怎么使用的,只会讲解在Java中使用的泛型的注意点
1. 不能使用基本数据类型实例化类型参数
不能使用类型参数代替基本数据。因此,没有Pair<double>,只有Pair<Double>(这里我们假设Pair是一个public class Pair<T> 类型的一个类)。这个非常的好理解,想一想我们在使用List集合时,不能这样子来定义一个集合:List<int> list = new ArrayList<>(),通常都是这样来定义一个int类型的集合:List<Integer> list = new ArrayList<>();
这个是什么原因呢?有人可能要问。我们这里需要讲一下Java中泛型的类型擦除
(1).Java泛型的类型擦除
在我们定义一个泛型类的时候,都会自动的给我们提供一个相应的原始类型(这个原始类型不是像Integer对应的是原始数据类型是int)。这里的原始数据类型就是删除类型参数之后的泛型类型名、擦除类型变量,并且替换为限定类型(如果没有限定类型,那么就用Object来代替)。
例如:
擦除类型之前的Pair类
public class Pair<T> {
private T first = null;
private T second = null;
public Pair() {
this.first = null;
this.second = null;
}
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public void setFirst(T first) {
this.first = first;
}
public void setSecond(T second) {
this.second = second;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
}
擦除类型之后的Pair类
public class Pair {
private Object first;
private Object second;
public Pair() {
this.first = null;
this.second = null;
}
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public void setFirst(Object first) {
this.first = first;
}
public void setSecond(Object second) {
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
}
我们会发现在在擦除之前,Pair里面的成员变量类型都是T类型,也就是泛型类型。但是在擦除之后,所有T类型都变成了Object类型。这个也就是我们之前说的,如果一个类型是无限定类型的话,会被替换成为Object类型。
如果泛型类型被限制了的,也就是T extends其他的类或者接口,就取extends关键字之后第一个类型来作为擦除之后的类型。为什么这里要强调是extends关键字之后第一个类型呢?因为extends关键字之后可以跟多个类或者接口,多个类或者接口使用&来连接。
例如,可以这样写:
public class Interval<T extends Comparable & Serializable>{
private T lower;
private T upper;
.....
}
擦除类型之后:
public class Interval{
private Comparable lower;
private Comparable upper;
......
}
(2).不用基本数据类型的原因
非常的明显,这里的原因肯定是类型擦除导致的。擦除之后,Pair类含有Object类型的成员变量,但是Object不能存储double类型的值。
2.运行时类型查询只适用于原始类型
在Java中,我们知道可以使用instanceof关键字来判断一个引用是否是一个类的对象。在泛型里面,这种代码是不支持的:
if(a instanceof Pair<String>)
或者是强制类型转换:
Pair<String> p = (Pair<String>)a;
同样的道理,使用getClass方法返回的原始类型:
Pair<String> stringPair = new Pair<>();
Pair<Integer> integerPair = new Pair<>();
if(stringPair.getClass() == integerPair.getClass()){ //true
}
他们的比较结果是true,因为两次调用getClass方法都将返回的是Pair.class对象,是同一个对象。
3. 不能创建泛型类型的数组
不能创建泛型类型的数组,例如:
Pair<String> pairs[] = new Pairs<String>[10];
这个是为什么呢?
假设,记住这里是假设,如果能够创建泛型类型的数组,也就是说,我们上面的pairs数组是定义成功了的,那么我们如此操作,编译器是会报错的:
pairs[0] = "Hello";
这个报错的原因是非常简单的,Pair<String>类型的数组,不能存储一个String类型的数据。
但是类型擦除会导致这个机制失效(类型匹配的机制)。因为如果定义泛型类型的数组成功的话,在擦除类型之后,数组的类型就从Pair<String>[]类型转换为Pair[],那么Pair[]类型可以变为Object[]类型:
Pair pairs[] = new Pairs[10];//这里重新定义一个Pair类型的数组,表示类型擦除
Object[] obejcts = pairs;//将Pair类型的数组转换为Object类型的数组
objects[0] = "Hello";//这里就不会报错,因为这里数组存储的是Object类型,所以不会报错。
由于这个原因--能够通过数组存储数组的类型检查,出于这个原因,不允许创建泛型类型的数组。
需要说明的是,只是不允许创建泛型类型的数组,而生命类型为Pair<String>[]的变量仍是合法的,只是不允许使用new Pair<String>[10]这种方式来初始化变量。
注意:可以声明通配类型的数组,然后进行强制类型转换:Pair<String> pairs = (Pair<String>[]) new Pair<?>[10]
4. Varagrs警告
在上一节中,我们已经了解到了Java中不支持泛型类型的数组。这一节中我们再来讨论一下相关的问题:向参数个数可变的方法传递一个泛型类型的对象。
例如:
public static <T> void addAll(Collection<T> coll, T...ts){
for(T t:ts){
coll.add(t);
}
}
我们知道,在addAll方法中的ts参数是一个数组。
现在我们这样调用这个方法:
Collection<Pair<String>> coll = new ArrayList<>();
Pair<String> pair1 = new Pair<>();
Pair<String> pair2 = new Pair<>();
Pair<String> pair3 = new Pair<>();
addAll(coll, pair1, pair2, pair3);
为了成功的调用addAll方法,Java虚拟机必须为我们创建一个Pair<String>类型的数组,这个就违反了前面的规则。不过,对于这种情况,规则有所放松,这里只是一个警告,而不是错误。
可以采用两种方法来抑制这个警告。一种方法是在addAll方法的前面增加注解@SuppressWarnings("unchecked");或者在Java7中,还可以使用@SafeVarargs直接标注addAll方法:
@SafeLVarargs
public static <T> void addAll(Collection<T> coll, T...ts)
注意:
这里我们可以使用@SafeVarargs注解来消除泛型数组的有关限制,方法如下:
@SafeVarargs
public static <E> E[] array(E...array){
return array;
}
现在可以调用:
Pair<String>[] pairs = array(pair1, pair2);
这个看起来非常的方便,不过隐藏着危险,以下代码:
Object[] oejcts = pairs;
objects[0] = new Pair<Integer>();
这里能够顺利运行而且不会出现ArrayStoreException异常(因为数组存储时,只会检查擦除之后的类型),但是在处理pairs[0]时,有可能会在别处得到一个异常。
5. 不能创建泛型类型的变量
不能使用像new T(...)、new T[...]或者T.class这样的表达式。例如,下面Pair<T>的构造方法是非法:
public Pair(){
this.first = new T();
this.second = new T();
}
类型擦除之后,将T变为了Object,而且本意上不是调用Object().在Java 8 出现之后,最好的解决办法是:让调用提供一个构造器的表达式,例如:
Pair<String> p = Pair.makePair(String::new);
makePair方法接收一个Supplier<T>类型的对象,这是一个函数式接口,表示一个无参数但是返回类型为T的函数:
public static <T> Pair<T> makePair(Supplier<T> constr){
return new Pair<>(constr.get(), constr.get());
}
这种方式在Java 8比较适用,如果各位读者对Java 8不是很熟悉的,可以先去看看Java 8中方法引用,这里其实就是将Lambda表达式简写成为了方法引用的形式,也就是所谓的语法糖。
但是在传统的想法中,我们比较倾向于通过反射调用Class.newInstance方法来创建泛型对象:
first = T.class.newInstance();
但是遗憾的是,细节比较复杂,而且不能调用。表达式T.class是不合法的,因为擦除之后,类型成为Object.class。所以必须通过以下方法来设计,以便得到一个Class对象:
public static <T> Pair<T> makePair(Class<T> clazz){
try {
return new Pair<>(clazz.newInstance(), clazz.newInstance());
}catch(Exception e) {
return null;
}
}
然后通过如下方法来调用:
Pair<String> pair = Pair.makePair(String.class);
注意,Class类本身是泛型。例如,String.class是一个Class<String>的对象。因此,makePair方法能够判断出pair的类型。
6. 不能构造泛型数组
就像不能创建一个泛型类型的对象,也不能创建泛型类型的数组。不过原因有所不同,毕竟数组会填充null值,构造是看上去是安全的。不过,数组本身也有类型,用来监控在虚拟机中的数组,这个类型会被擦除。例如:
public static <T extends Comparable> T[] minAndMax(T a[]){
T ts[] = new T[2];
......
return ts;
}
类型擦除会让这个方法永远构造Comparable类型的数组。
但是如果数组是一个类的私有成员变量,就可以使用Object类型的数组,并且在获取元素时,进行类型转化。例如,ArrayList类可以这样实现:
public class ArrayList<E>{
private Object[] elements;
......
@SuppressWarnings("unchecked")
public E get(int index){
return (E)elements[index];
}
public void set(E e, int index){
elements[index] = e;
}
}
实际上也可以这样写:
public class ArrayList<E> {
private E[] elements;
@SuppressWarnings("unchecked")
public ArrayList() {
this.elements = (E[])new Object[10];
}
}
在minAndMax方法中,由于该方法返回的是一个泛型类型的数组,所以像上面的操作不能进行,但是如果想要实现功能的话,可以如下实现:
public static<T extends > T[] minAndMax(T...ts){
Object[] objects = new Object[10];
......
return (T[]) objects;
}
然后调用代码:
String ss[] = ArrayAlg.minAndMax("pby", "pby123", "pby456");
上面这段代码在编译阶段是没有错误的,但是当我们调用这个方法会抛出一个ClassCastException异常。
在这种情况下,可以让用户提供一个数组的构造器表达式:
String [] ss = ArrayAlg.minAndMax(String[]::new, "pby", "pby123", "pby456");
然后在minAndMax方法中使用这个参数生成一个正确类型的数组:
public static <T extends Comparable> T[] minAndMax(IntFunction<T[]> constr, T...ts){
return constr.apply(2);
}
上面的写法是基于Java 8中的方法引用。如果使用老式的Java反射,调用Array.newInstance方法:
public static <T extends Comparable> T[] minAndMax(T...a){
return (T[])Array.newInstance(a.getClass().getComponentType(), 2);
}
7.泛型类的静态上下文在泛型类型中无效
静态变量不能定义泛型类型,静态方法的返回类型不能定义为泛型类型。例如,下面的写法是错误的:
public class Interval<T>{
private static T singleInstance; //错误,静态变量的类型不能为泛型类型
public static T getSingleInstance(){ //错误,静态方法的返回类型为泛型类型。
return singleInstance;
}
}
8.不能抛出或者捕获泛型类的异常
在Java中,不能对泛型类的异常对象进行抛出捕获。实际上,泛型类继承于Throwable类都是不合法的,例如,以下的代码是错误的:
public class Problem<T> extends Throwable{
}
同时不能再catch语句中使用泛型类型的异常对象。例如:
public static <T extends Throwable> void doWork(Class<T> clazz){
try{
}catch(T e){ //错误,不能抛出泛型类型的异常对象
}
}
不过,在异常规范中,使用泛型类型的对象是允许的:
public static <T extends Throwable> void doWork(T t){
throws T
try{
}catch(Throwable realCause){
t.initCause(realCause);
throw t;
}
}
9. 可以消除对受查异常的检查
Java异常处理的一个基本规则是:必须为所有受查异常提供一个处理器。不过我们可以利用这个泛型来取消这个限制。例如:
public abstract class Block {
public abstract void body() throws Exception;
public Thread toThread() {
return new Thread() {
@Override
public void run() {
try {
body();
}catch(Throwable t) {
Block.throwAs(t);
}
}
};
}
@SuppressWarnings("unchecked")
public static <T extends Throwable> void throwAs(Throwable t) throws T{
throw (T) t;
}
}
然后我们在main方法里面开启一个线程来调用我们的方法。
public class Demo {
public static void main(String []args) {
new Block() {
@Override
public void body() throws Exception{
}
}.toThread().start();
}
}
有人可能会问这个有什么意义上呢?正常情况下,我们必须捕获run方法里面所有受查异常,不能从run方法里面向外面抛出一个异常,因为在Thread类里面的run方法没有抛出任何的异常,所以我们这里向外抛出任何的异常,所有的受查异常都必须爱run方法里面进行捕获。但是我们这里的操作就是,将受查异常包装为非受查异常,然后在catch里面抛出