聊聊Java的泛型及实现

发表于:2017-3-20 11:05  作者:suemi94   来源:51Testing软件测试网采编

字体: | 上一篇 | 下一篇 |我要投稿 | 推荐标签: 软件开发 java

  泛型基础
  泛型是对Java语言类型系统的一种扩展,有点类似于C++的模板,可以把类型参数看作是使用参数化类型时指定的类型的一个占位符。引入泛型,是对Java语言一个较大的功能增强,带来了很多的好处:
  1、类型安全。类型错误现在在编译期间就被捕获到了,而不是在运行时当作java.lang.ClassCastException展示出来,将类型检查从运行时挪到编译时有助于开发者更容易找到错误,并提高程序的可靠性
  2、消除了代码中许多的强制类型转换,增强了代码的可读性
  3、为较大的优化带来了可能
  泛型是什么并不会对一个对象实例是什么类型的造成影响,所以,通过改变泛型的方式试图定义不同的重载方法是不可以的。剩下的内容我不会对泛型的使用做过多的讲述,泛型的通配符等知识请自行查阅。
  在进入下面的论述之前我想先问几个问题:
  · 定义一个泛型类最后到底会生成几个类,比如ArrayList<T>到底有几个类
  · 定义一个泛型方法最终会有几个方法在class文件中
  · 为什么泛型参数不能是基本类型呢
  · ArrayList<Integer>是一个类吗
  · ArrayList<Integer>和List<Integer>和ArrayList<Number>和List<Number>是什么关系呢,这几个类型的引用能相互赋值吗
  类型擦除
  正确理解泛型概念的首要前提是理解类型擦除(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。
  泛型的实现原理
  因为种种原因,Java不能实现真正的泛型,只能使用类型擦除来实现伪泛型,这样虽然不会有类型膨胀(C++模板令人困扰的难题)的问题,但是也引起了许多新的问题。所以,Sun对这些问题作出了许多限制,避免我们犯各种错误。
  保证类型安全
  首先第一个是泛型所宣称的类型安全,既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?java编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,在进行编译的。那类型检查是针对谁的呢,让我们先看一个例子。
  ArrayList<String> arrayList1=new ArrayList(); // 正确,只能放入String
  ArrayList arrayList2=new ArrayList<String>(); // 可以放入任意Object
  这样是没有错误的,不过会有个编译时警告。不过在第一种情况,可以实现与 完全使用泛型参数一样的效果,第二种则完全没效果。因为,本来类型检查就是编译时完成的。new ArrayList()只是在内存中开辟一个存储空间,可以存储任何的类型对象。而真正涉及类型检查的是它的引用,因为我们是使用它引用arrayList1 来调用它的方法,比如说调用add()方法。所以arrayList1引用能完成泛型类型的检查。 而引用arrayList2没有使用泛型,所以不行。
  类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
  实现自动类型转换
  因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?
public class Test {
public static void main(String[] args) {
ArrayList<Date> list=new ArrayList<Date>();
list.add(new Date());
Date myDate=list.get(0);
}
}
  编译器生成的class文件中会在你调用泛型方法完成之后返回调用点之前加上类型转换的操作,比如上文的get函数,就是在get方法完成后,jump回原本的赋值操作的指令位置之前加入了强制转换,转换的类型由编译器推导。
  泛型中的继承关系
  先看一个例子:
class DateInter extends A<Date> {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
  先来分析setValue方法,父类的类型是Object,而子类的类型是Date,参数类型不一样,这如果实在普通的继承关系中,根本就不会是重写,而是重载。
public void setValue(java.util.Date);  //我们重写的setValue方法
Code:
0: aload_0
1: aload_1
2: invokespecial #16                // invoke A setValue
:(Ljava/lang/Object;)V
5: return
public java.util.Date getValue();    //我们重写的getValue方法
Code:
0: aload_0
1: invokespecial #23                 // A.getValue
:()Ljava/lang/Object;
4: checkcast     #26
7: areturn
public java.lang.Object getValue();     //编译时由编译器生成的方法
Code:
0: aload_0
1: invokevirtual #28                 // Method getValue:() 去调用我们重写的getValue方法
;
4: areturn
public void setValue(java.lang.Object);   //编译时由编译器生成的方法
Code:
0: aload_0
1: aload_1
2: checkcast     #26
5: invokevirtual #30                 // Method setValue;   去调用我们重写的setValue方法
)V
8: return

21/212>

评 论

论坛新帖



建议使用IE 6.0以上浏览器,800×600以上分辨率,法律顾问:上海信义律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2021, 沪ICP备05003035号
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪公网安备 31010102002173号

51Testing官方微信

51Testing官方微博

扫一扫 测试知识全知道