死磕Java之泛型(一)
一般的类和方法,只能使用具体的类型;要么是基本类型,要么是自定义的类,如果需要编写可以应用于多种类型的代码,这种限制就降低了代码的可用性,当然你会想到重载,但是对于类呢,这就需要引入泛型了。
01
泛型的基本概念
泛型,从字面上理解就是适用于很多很多的类型,即参数化类型。从Java SE5开始,Sun公司就引入了泛型的概念。引入泛型的初衷是,希望类和方法具有更广泛的表达性!最直观的体现,就是我们现在所使用的容器类,例如List、Map、Set这些类;这些容器类的参数化类型表明了容器持有什么类型的对象,而且这些由编译器保证正确性。
在Java SE5之前,如果我们需要定义表某个类持有Object对象,我们可能编写如下的代码:
代码一:
class Apple{}; public class Hold2 { private Object obj; public Hold2(Object obj) { this.obj = obj; } public Object getObj() { return obj; } public void setObj(Object obj) { this.obj = obj; } //程序猿技术 public static void main(String[] args){ Hold2 h=new Hold2(new Apple()); Apple apple=(Apple)h.getObj(); h.setObj("HelloWorld"); String s=(String)h.getObj(); h.setObj(1); } }
这段代码可能存在的问题时类型转换异常,也许你对于这段简单的代码可保证不会出错,一旦逻辑复杂,并不能避免类型转换错误。
引入泛型之后,我们可以使用如下代码替换上面不安全的代码:
代码二:
public class Hold3<T> { private T obj; public Hold3(T obj) { this.obj = obj; } public T getObj() { return obj; } public void setObj(T obj) { this.obj = obj; } //程序猿技术 public static void main(String[] args) { Hold3<Apple> hold3=new Hold3<Apple>(new Apple()); Apple apple=hold3.getObj(); //不能这样使用,因为固定了类型 //hold3.setObj("ewdrf"); } }
这里在类后面使用尖括号将参数类型包住,在使用的时候用实际参数替代T,而且也避免了类型转换异常。
02
泛型的实现机制
当你深入专研泛型时会发现,泛型是一种编译时多态。更加详细的来说,泛型的实现是在编译期间编译器对于参数类型的擦除。
我们现在回到上一节的代码二中,在Hold3类的编译期间,编译器会将泛型参数T擦除,取而代之的是Object插入到被擦除的地方。那么,是不是意味着如果我们不声明泛型,Hold3<T>和Hold3类是相同呢?
如果泛型参数没有限制,在大多数方面确实如此。我们可以在上一节main方法中给出如下的代码:
代码三:
/程序猿技术 public static void main(String[] args) { Hold3<Apple> hold3=new Hold3<Apple>(new Apple()); Hold3<Integer> hold=new Hold3<Integer>(new Integer(0)); //答案是true System.out.println(hold.getClass()==hold3.getClass()); }
这样看起来会非常奇怪,持有Apple的Hold3的Class对象居然和持有Integer的Hold3的Class对象相同。然而,如果你理解,擦除的含义,那么对于两者的Class相同并不会奇怪。因为在编译期间,对于实例化的参数,这里分别是Apple和Integer,编译器都是使用Object来替换,显然对于两个持有相同参数的Class表示为同一个Class。
如果你理解了擦除的真谛,那么下面的代码并不吃惊:
代码四:
//程序猿技术 public static void main(String[] args) { List<String> stringList=new ArrayList<String>(); List<Integer> integerList=new ArrayList<Integer>(); System.out.println(stringList.getClass()==integerList.getClass()); }
这里依然是true。
03
擦除的缺陷
尽管擦除带来了很多便利之处,例如不需要强制转换、多态等,这也给程序员带来了困扰。比如在运行时,程序需要获取泛型声明的类型参数,具体代码如下:
代码五:
public static void main(String[] args) { List<String> stringList=new ArrayList<String>(); Map<String,Integer> map=new HashMap<String,Integer>(); //程序猿技术 System.out.println(Arrays.toString(stringList.getClass().getTypeParameters())); System.out.println(Arrays.toString(map.getClass().getTypeParameters())); }
与一般想法不一样的是,程序运行的结果如下:
可以看出仅仅只是输出了参数占位符的标识,这里有个残酷的现实:
在泛型代码内部,无法获得任何有关泛型参数类型的信息。
同样地,任何在运行时需要知道确切类型信息的操作都将无法工作。例如:
代码六:
public class Hold3<T> { private T obj; //编译错误 //private T[] array=new T[1024]; //private T t=new T(); public Hold3(T obj) { this.obj = obj; } public T getObj() { return obj; } public void setObj(T obj) { this.obj = obj; } //程序猿技术 public static void main(String[] args) { } }
上面代码中,我们无法使用T来创建数组和对象。显然,你无法确定用于替换T后的类型是否可以被实例化,或者有这样的构造器。如果代码中真的想要使用泛型数组,内部代码可以使用ArrayList替换一般的数组。同样,很多人喜欢使用在代码内部使用Class<T>,然后使用Class对象的方法newInstance()用于获取实例,需要注意的是,你无法确定替换泛型参数的类是否具有无参构造函数。在使用泛型时,你需要时刻替换自己,这个无界泛型参数在编译时用Object替换。
04
边界与通配符
原生类型即T,在编译期间被编译器被擦除后,被替换为Object。接下来我们将会看到一段令人费解的代码:
代码七:
public static void main(String[] args) { //程序猿技术 List<Number> numbers=new ArrayList<Number>(); List<Integer> integers=new ArrayList<Integer>(); //编译出错 //numbers=integers; //numbers=new ArrayList<Integer>(); }
Number类是Integer类基类,按照一般的理解,numbers应该可以指向Integer的ArrayList。我们需要这样理解,numbers表示持有Number对象的容器,integers表示持有Integer对象的容器;numbers可以指向Number和其子类的容器,integers只想Integer及其子类的容器。诚然,numbers包含integers在内,但是它不是一个Integer的List,它仍然是Number的List。
其实,真正的问题是我们谈论的是容器,并不是容器持有的类型。编译时和运行时系统并不知道你想什么,以及类型的转换规则是怎么样的,所以如果让上面的代码合法,这里需要引入规则,告诉编译时和运行时系统应该遵守什么样的规则。
代码八:
public static void main(String[] args) { //程序猿技术 List<Number> numbers=new ArrayList<Number>(); List<Integer> integers=new ArrayList<Integer>(); //编译出错 //numbers=integers; //numbers=new ArrayList<Integer>();
List<? extends Number> arrays=new ArrayList<Integer>();
/** arrays.add(10); arrays.add(1L); arrays.add(23.45f); arrays.add('h');**/
}
这里,我们使用通配符?同时限制了arrays持有对象的上界,在擦除时,表示替换的必须是Number的子类。然而,新的问题以后来了,尽管这段代码是没有问题的,但是在向容器中添加元素时,你无法添加Number子类实例。这时你才发现并不如我们想的那样,即使添加Integer类,也无法添加。其实,我们无法确定arrays指向那个持有Number子类的容器,因为有可能我们指向持有Long的容器也有可能指向持有Double的容器,尽管在这两种类型可能不会产生类型转换异常,但是如果程序员不规范的使用,可能就会产生安全性问题。
那么,假设我们需要使容器添加Integer类型,我们应该如何做呢?这里就引入了超类型通配符,定义了下边界。
代码九:
List<? super Integer> arrays=new ArrayList<Integer>(); arrays.add(10); /** arrays.add(1L); arrays.add(23.45f); arrays.add('h');**/
在更改之后,arrays容器中至少能添加Integer类型。但是对于Long,Doubler,Character等类型,依然不能添加,因为它们均不是Integer的类型。
点击上方二维码,关注我们
15