1、泛型的引入
现在要求可以设计一个表示坐标的操作类(Point),此类中可以分别表示以下的三种坐标:
·第一种: x=10,y=30
·第二种: x=10.3,y=30.9
·第三种: x="东经110度",y="北纬200度"
问,此类该如何设计?
由于现在存在了三种数据类型的数据,所以为了保证接收,只能通过Object接收:
· intà Integerà Object
· floatà Floatà Object
· Stringà Object
class Point { private Object x ; private Object y ; public void setX(Object x){ this.x = x ; } public void setY(Object y){ this.y = y ; } public Object getX(){ return this.x ; } public Object getY(){ return this.y ; } };
现在已经解决了本题目的要求,按照之前所学,现在肯定是最合理的解决方案,但是,本程序又存在了一个安全隐患,因为所有的类型都使用Object进行接收,那么有没有一种可能,将X的坐标设置成了整型,而Y的坐标设置成了字符串?
public class PointDemo { public static void main(String args[]){ Point p = new Point() ; p.setX(100) ; p.setY("北纬100度") ; String x = (String) p.getX() ; String y = (String) p.getY() ; System.out.println("X的坐标:" + x) ; System.out.println("Y的坐标:" + y) ; } };
对于以上的问题,在JDK1.5之后就可以解决了,因为JDK 1.5有一个最大的特点,就是加入了泛型的操作。
class Point <T> { private T x ; private T y ; public void setX(T x){ this.x = x ; } public void setY(T y){ this.y = y ; } public T getX(){ return this.x ; } public T getY(){ return this.y ; } };
程序中的“<T>”就表示的是一种类型,只是这种类型现在属于未知的,在类使用的时候设置类型。
public class PointDemo { public static void main(String args[]){ Point<Integer> p = new Point<Integer>() ; p.setX(10) ; p.setY(20) ; int x = p.getX() ; int y = p.getY() ; System.out.println("X的坐标:" + x) ; System.out.println("Y的坐标:" + y) ; } };
泛型中的类型可以由外部决定,但是在设置基本数据类型的时候,只能使用包装类。设置泛型之后,程序更加安全了,可以避免掉类转换异常的出现。
二、泛型
JDK 1.5之后增加了很多的新特性,其中有三项是最重要的新特性:泛型、枚举、Annotation(注释)Java泛型(generics)是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter)。声明的类型参数在使用时用具体的类型来替换。泛型最主要的应用是在JDK 5中的新集合类框架中。对于泛型概念的引入,开发社区的观点是褒贬不一。从好的方面来说,泛型的引入可以解决之前的集合类框架在使用过程中通常会出现的运行时刻类型错误,因为编译器可以在编译时刻就发现很多明显的错误。而从不好的地方来说,为了保证与旧有版本的兼容性,Java泛型的实现上存在着一些不够优雅的地方。当然这也是任何有历史的编程语言所需要承担的历史包袱。后续的版本更新会为早期的设计缺陷所累。
2.1、泛型的作用
泛型的主要目的是为了解决在进行类转换过程中发生的类转换异常的问题,用于处理安全隐患的。所以为了避免掉ClassCastException在JDK 1.5之后增加了泛型的操作,泛型的具体含义就是一个类中的某些属性的操作类型由使用此类的时候决定。
在设置烦型的时候是通过“<T>”的形式设置的,这里只是设置了一个标记,以后会根据设置的情况换成不同的类型,但是需要注意的是,为了保证JDK 1.5之前代码的使用正常,所以在泛型中也可以不设置类型,如果不设置的话,称为擦除泛型,将全部使用Object进行接收。
当然,也可以同时设置多个泛型类型,例如:
class Point <T,K,V> {}
2、通配符与上下界
通配符一共有三种:
· ?:可以接收任意的泛型类型
· ? extends类:指定上限
· ? super类:指定下限
泛型确实可以保证程序避免安全隐患问题,但是程序中一旦使用了泛型之后对于引用传递上又会存在问题。
class Info<T> { private T content ; public void setContent(T content){ this.content = content ; } public T getContent(){ return this.content ; } }; public class GenDemo01 { public static void main(String args[]){ Info<String> info = new Info<String>() ; info.setContent("Hello World") ; fun(info) ; } public static void fun(Info<String> temp){ System.out.println(temp.getContent()) ; } };
以上的程序非常的容易,但是这个时候有一个问题出现了,如果现在设置的泛型类型不是String呢?
由于fun()方法上设置的Info只能接收String,所以肯定无法传递,那么如果现在在fun()方法中不写呢?
public class GenDemo01 { public static void main(String args[]){ Info<Integer> info = new Info<Integer>() ; info.setContent(30) ; fun(info) ; } public static void fun(Info temp){ System.out.println(temp.getContent()) ; } };
如果这样写的话,则意味着,以后可以通过fun()方法向Info的对象中设置任意样的内容。
public static void fun(Info temp){ temp.setContent("Hello") ; System.out.println(temp.getContent()) ; }
这种操作是存在安全隐患的。那么如果写成如下的形式?
public static void fun(Info<Object>temp){ System.out.println(temp.getContent()) ; }
这种做法肯定没意义,而且不能编译通过(string 和object不匹配),那么该如何修饰呢?此时关键性的问题,是如何解决调用设置的问题,而不是取得属性输出的问题,所以说在泛型中增加了一个“?”表示的是接收所有的泛型类型,而且一旦接收之后只能取得,不能设置(在没实际调用之前,?和任意类型都不匹配)。
public class GenDemo01 { public static void main(String args[]){ Info<Integer> info = new Info<Integer>() ; info.setContent(30) ; fun(info) ; } public static void fun(Info<?> temp){ System.out.println(temp.getContent()) ; } };
但是一个新的问题又出现了,如果现在的Info中的content属性只能是数字,不能是其他的任意一种类型呢?
所有的数字的包装类,都是Number的子类,所以这种条件下泛型只能设置Number或Number的子类,就可以使用如下的语法:? extends 类,表示可以设置指定类或指定类的子类。
class Info<T> { private T content ; public void setContent(T content){ this.content = content ; } public T getContent(){ return this.content ; } }; public class GenDemo01 { public static void main(String args[]){ Info<Number> info = new Info<Number>() ; info.setContent(30) ; fun(info) ; } public static void fun(Info<? extends Number> temp){ System.out.println(temp.getContent()) ; } };
现在还有一种情况,方法中接收Info类型的时候只能是String或其父类,语法:<? super 类>。
class Info<T> { private T content ; public void setContent(T content){ this.content = content ; } public T getContent(){ return this.content ; } }; public class GenDemo01 { public static void main(String args[]){ Info<Object> info = new Info<Object>() ; info.setContent("Hello World!!!") ; fun(info) ; } public static void fun(Info<? super String> temp){ System.out.println(temp.getContent()) ; } };
2.3、在方法上使用泛型
之前的所有泛型都是在一个类上使用的,那么泛型也可以在方法中使用,当然了,泛型方法可以不用编写在泛型类之中,而可以单独存在。
public class GenDemo02 { public static void main(String args[]){ fun("Hello","World") ; } public static <T> void fun(T t1,T t2){ System.out.println(t1 + " -- " + t2) ; } };
2.4、在接口上使用泛型
泛型的操作不光在类上使用,也可以在接口上应用。
interface Info<T>{ public void fun(T t) ; }
此时,对于这种接口就有两种实现方式了。
第一种:继续指定泛型
interface Info<T>{ public void fun(T t) ; } class InfoImpl<T> implements Info<T> { public void fun(T t){ System.out.println(t) ; } }; public class GenDemo03 { public static void main(String args[]){ Info<String> info = new InfoImpl<String>() ; info.fun("Hello") ; } };
第二种:直接设置好具体的类型
interface Info<T>{ public void fun(T t) ; } class InfoImpl implements Info<String> { public void fun(String t){ System.out.println(t) ; } }; public class GenDemo03 { public static void main(String args[]){ InfoImpl info = new InfoImpl() ; info.fun("Hello") ; } };
2.5开发自己的泛型类
泛型类与一般的Java类基本相同,只是在类和接口定义上多出来了用<>声明的类型参数。一个类可以有多个类型参数,如 MyClass<X, Y, Z>。每个类型参数在声明的时候可以指定上界。所声明的类型参数在Java类中可以像一般的类型一样作为方法的参数和返回值,或是作为域和局部变量的类型。但是由于类型擦除机制,类型参数并不能用来创建对象或是作为静态变量的类型。考虑下面的泛型类中的正确和错误的用法。
class ClassTest<X extends Number, Y, Z> { private X x; private static Y y; //编译错误,不能用在静态变量中 public X getFirst() { //正确用法 return x; } public void wrong() { Z z = new Z(); //编译错误,不能创建对象 } }
例子2
class Info<T extends Number> { private T content ; public void setContent(T content){ this.content = content ; } public T getContent(){ return this.content ; } }; public class GenDemo01 { public static void main(String args[]){ Info<Number> info = new Info<Number>() ; info.setContent(30) ; fun(info) ; } public static void fun(Info<?> temp){ System.out.println(temp.getContent()) ; } };
三、类型擦除
正确理解泛型概念的首要前提是理解类型擦除(type erasure)。 Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的List<Object>和List<String>等类型,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方式与C++模板机制实现方式之间的重要区别。
很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:
- 泛型类并没有自己独有的Class类对象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class。
- 静态变量是被泛型类的所有实例所共享的。对于声明为MyClass<T>的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象,都是共享一个静态变量。
- 泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException<String>和MyException<Integer>的。对于JVM来说,它们都是 MyException类型的。也就无法执行与异常对应的catch语句。
类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明,即去掉<>的内容。比如T get()方法声明就变成了Object get();List<String>就变成了List。接下来就可能需要生成一些桥接方法(bridge method)。这是由于擦除了类型之后的类可能缺少某些必须的方法。比如考虑下面的代码:
class MyString implements Comparable<String> { public int compareTo(String str) { return 0; } }
当类型信息被擦除之后,上述类的声明变成了class MyString implements Comparable。但是这样的话,类MyString就会有编译错误,因为没有实现接口Comparable声明的int compareTo(Object)方法。这个时候就由编译器来动态生成这个方法。
实例分析
了解了类型擦除机制之后,就会明白编译器承担了全部的类型检查工作。编译器禁止某些泛型的使用方式,正是为了确保类型的安全性。以上面提到的List<Object>和List<String>为例来具体分析:
public void inspect(List<Object> list) { for (Object obj : list) { System.out.println(obj); } list.add(1); //这个操作在当前方法的上下文是合法的。 } public void test() { List<String> strs = new ArrayList<String>(); inspect(strs); //编译错误 }
这段代码中,inspect方法接受List<Object>作为参数,当在test方法中试图传入List<String>的时候,会出现编译错误。The method inspect(List<Object>) in the type 类名 is not applicable for the arguments(List<String>)。假设这样的做法是允许的,那么在inspect方法就可以通过list.add(1)来向集合中添加一个数字。这样在test方法看来,其声明为List<String>的集合中却被添加了一个Integer类型的对象。这显然是违反类型安全的原则的,在某个时候肯定会抛出ClassCastException。因此,编译器禁止这样的行为。编译器会尽可能的检查可能存在的类型安全问题。对于确定是违反相关原则的地方,会给出编译错误。当编译器无法判断类型的使用是否正确的时候,会给出警告信息。