java泛型(一)、泛型的基本介绍和使用

http://blog.csdn.net/lonelyroamer/article/details/7864531

   现在开始深入学习java的泛型了,以前一直只是在集合中简单的使用泛型,根本就不明白泛型的原理和作用。泛型在java中,是一个十分重要的特性,所以要好好的研究下。

一、泛型的基本概念

泛型的定义:泛型是JDK 1.5的一项新特性,它的本质是参数化类型(Parameterized Type)的应用,也就是说所操作的数据类型被指定为一个参数,在用到的时候在指定具体的类型。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。

  泛型思想早在C++语言的模板(Templates)中就开始生根发芽,在Java语言处于还没有出现泛型的版本时,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。例如在哈希表的存取中,JDK 1.5之前使用HashMap的get()方法,返回值就是一个Object对象,由于Java语言里面所有的类型都继承于java.lang.Object,那Object转型为任何对象成都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个Object到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多ClassCastException的风险就会被转嫁到程序运行期之中。

  泛型技术在C#和Java之中的使用方式看似相同,但实现上却有着根本性的分歧,C#里面泛型无论在程序源码中、编译后的IL中(Intermediate Language,中间语言,这时候泛型是一个占位符)或是运行期的CLR中都是切实存在的,List<int>与List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型被称为真实泛型。

  Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原始类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,ArrayList<int>与ArrayList<String>就是同一个类。所以说泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型。(类型擦除在后面在学习)

  使用泛型机制编写的程序代码要比那些杂乱的使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型对于集合类来说尤其有用。

  泛型程序设计(Generic Programming)意味着编写的代码可以被很多不同类型的对象所重用。

实例分析:

  在JDK1.5之前,Java泛型程序设计是用继承来实现的。因为Object类是所用类的基类,所以只需要维持一个Object类型的引用即可。就比如ArrayList只维护一个Object引用的数组:

[java] view plaincopy

  1. public class ArrayList//JDK1.5之前的
  2. {  
  3. public Object get(int i){......}  
  4. public void add(Object o){......}  
  5.     ......  
  6. private Object[] elementData;  
  7. }  

这样会有两个问题:

1、没有错误检查,可以向数组列表中添加类的对象

2、在取元素的时候,需要进行强制类型转换

这样,很容易发生错误,比如:

[java] view plaincopy

  1. /**jdk1.5之前的写法,容易出问题*/
  2. ArrayList arrayList1=new ArrayList();  
  3. arrayList1.add(1);  
  4. arrayList1.add(1L);  
  5. arrayList1.add("asa");  
  6. int i=(Integer) arrayList1.get(1);//因为不知道取出来的值的类型,类型转换的时候容易出错

这里的第一个元素是一个长整型,而你以为是整形,所以在强转的时候发生了错误。

所以。在JDK1.5之后,加入了泛型来解决类似的问题。例如在ArrayList中使用泛型:

[java] view plaincopy

  1. /** jdk1.5之后加入泛型*/
  2.         ArrayList<String> arrayList2=new ArrayList<String>();  //限定数组列表中的类型
  3. //      arrayList2.add(1); //因为限定了类型,所以不能添加整形
  4. //      arrayList2.add(1L);//因为限定了类型,所以不能添加整长形
  5.         arrayList2.add("asa");//只能添加字符串
  6.         String str=arrayList2.get(0);//因为知道取出来的值的类型,所以不需要进行强制类型转换

还要明白的是,泛型特性是向前兼容的。尽管 JDK 5.0 的标准类库中的许多类,比如集合框架,都已经泛型化了,但是使用集合类(比如 HashMap 和 ArrayList)的现有代码可以继续不加修改地在 JDK 1.5 中工作。当然,没有利用泛型的现有代码将不会赢得泛型的类型安全的好处。

在学习泛型之前,简单介绍下泛型的一些基本术语,以ArrayList<E>和ArrayList<Integer>做简要介绍:

整个成为ArrayList<E>泛型类型

ArrayList<E>中的 E称为类型变量或者类型参数

整个ArrayList<Integer> 称为参数化的类型

ArrayList<Integer>中的integer称为类型参数的实例或者实际类型参数

·ArrayList<Integer>中的<Integer>念为typeof   Integer

ArrayList称为原始类型

二、泛型的使用

泛型的参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。下面看看具体是如何定义的。

1、泛型类的定义和使用

一个泛型类(generic class)就是具有一个或多个类型变量的类。定义一个泛型类十分简单,只需要在类名后面加上<>,再在里面加上类型参数:

[java] view plaincopy

  1. class Pair<T> {  
  2. private T value;  
  3. public Pair(T value) {  
  4. this.value=value;  
  5.         }  
  6. public T getValue() {  
  7. return value;  
  8.     }  
  9. public void setValue(T value) {  
  10. this.value = value;  
  11.     }  
  12. }  

现在我们就可以使用这个泛型类了:

[java] view plaincopy

  1. public static void main(String[] args) throws ClassNotFoundException {  
  2.         Pair<String> pair=new Pair<String>("Hello");  
  3.         String str=pair.getValue();  
  4.         System.out.println(str);  
  5.         pair.setValue("World");  
  6.         str=pair.getValue();  
  7.         System.out.println(str);  
  8.     }  

Pair类引入了一个类型变量T,用尖括号<>括起来,并放在类名的后面。泛型类可以有多个类型变量。例如,可以定义Pair类,其中第一个域和第二个域使用不同的类型:

public class Pair<T,U>{......}

注意:类型变量使用大写形式,且比较短,这是很常见的。在Java库中,使用变量E表示集合的元素类型,K和V分别表示关键字与值的类型。(需要时还可以用临近的字母U和S)表示“任意类型”。

2、泛型接口的定义和使用

定义泛型接口和泛型类差不多,看下面简单的例子:

[java] view plaincopy

  1. interface Show<T,U>{  
  2. void show(T t,U u);  
  3. }  
  4. class ShowTest implements Show<String,Date>{  
  5. @Override
  6. public void show(String str,Date date) {  
  7.         System.out.println(str);  
  8.         System.out.println(date);  
  9.     }  
  10. }  

测试一下:

[java] view plaincopy

  1. public static void main(String[] args) throws ClassNotFoundException {  
  2.         ShowTest showTest=new ShowTest();  
  3.         showTest.show("Hello",new Date());  
  4.     }  

3、泛型方法的定义和使用

泛型类在多个方法签名间实施类型约束。在 List<V> 中,类型参数 V 出现在 get()、add()、contains() 等方法的签名中。当创建一个 Map<K, V> 类型的变量时,您就在方法之间宣称一个类型约束。您传递给 add() 的值将与 get() 返回的值的类型相同。

类似地,之所以声明泛型方法,一般是因为您想要在该方法的多个参数之间宣称一个类型约束。

举个简单的例子:

[java] view plaincopy

  1. public static void main(String[] args) throws ClassNotFoundException {  
  2.         String str=get("Hello", "World");  
  3.         System.out.println(str);  
  4.     }  
  5. public static <T, U> T get(T t, U u) {  
  6. if (u != null)  
  7. return t;  
  8. else
  9. return null;  
  10.     }  

三、泛型变量的类型限定

在上面,我们简单的学习了泛型类、泛型接口和泛型方法。我们都是直接使用<T>这样的形式来完成泛型类型的声明。

有的时候,类、接口或方法需要对类型变量加以约束。看下面的例子:

有这样一个简单的泛型方法:

[java] view plaincopy

  1. public static <T> T get(T t1,T t2) {  
  2. if(t1.compareTo(t2)>=0);//编译错误
  3. return t1;  
  4.     }  

因为,在编译之前,也就是我们还在定义这个泛型方法的时候,我们并不知道这个泛型类型T,到底是什么类型,所以,只能默认T为原始类型Object。所以它只能调用来自于Object的那几个方法,而不能调用compareTo方法。

可我的本意就是要比较t1和t2,怎么办呢?这个时候,就要使用类型限定,对类型变量T设置限定(bound)来做到这一点。

我们知道,所有实现Comparable接口的方法,都会有compareTo方法。所以,可以对<T>做如下限定:

[java] view plaincopy

  1. public static <T extends Comparable> T get(T t1,T t2) { //添加类型限定
  2. if(t1.compareTo(t2)>=0);  
  3. return t1;  
  4.     }  

类型限定在泛型类、泛型接口和泛型方法中都可以使用,不过要注意下面几点:

1、不管该限定是类还是接口,统一都使用关键字 extends

2、可以使用&符号给出多个限定,比如

[java] view plaincopy

  1. public static <T extends Comparable&Serializable> T get(T t1,T t2)  

3、如果限定既有接口也有类,那么类必须只有一个,并且放在首位置

[java] view plaincopy

  1. public static <T extends Object&Comparable&Serializable> T get(T t1,T t2)  

[运行时获取模板类类型] Java 反射机制 + 类型擦除机制

http://blog.csdn.net/tbwood/article/details/40739947

给定一个带模板参数的类

class A<T> 

{

}

如何在运行时获取 T的类型?

在C#中,这个很简单,CLR的反射机制是解释器支持的,大概代码为:

[csharp] view plaincopy

  1. namespace TestReflect  
  2. {  
  3. class Program<T>  
  4.     {  
  5. public Type getTClass()  
  6.         {  
  7.             Type type= this.GetType();  
  8.             Type[] tts = type.GetGenericArguments();  
  9. return tts[0];  
  10.         }  
  11.     }  
  12. }  

可是在Java中,答案是否定的,java中没有直接方法来获取T.Class. (至少JDK 1.7以前不可能)

其根本原因在于: java语言是支持反射的,但是JVM不支持,因为JVM具有“Type erasure”的特性,也就是“类型擦除”。

但是Java无绝人之路,我们依旧可以通过其他方法来达到我们的需求。

方法1. 通过构造函数传入实际类别. 【有人说这个方法不帅气,但是不可否认,这种方法没有用到反射,但是最为高效,反射本身就不是高效的。】

[java] view plaincopy

  1. public class A<T>  
  2. {  
  3. private final Class<T> clazz;  
  4. public A<T>(Class<T> clazz)  
  5.     {  
  6. this.clazz = clazz;  
  7.     }  
  8. // Use clazz in here
  9. }  

方法2. 通过继承这个类的时候传入模板T对应的实际类:(定义成抽象类也可以)

[java] view plaincopy

  1. class A<T>{  
  2. public Class getTClass(int index) {  
  3.       Type genType = getClass().getGenericSuperclass();  
  4. if (!(genType instanceof ParameterizedType))   
  5.           {  
  6. return Object.class;  
  7.           }  
  8.  <span style="white-space:pre">  </span>  Type[] params = ((ParameterizedType) genType).getActualTypeArguments();  
  9. <span style="white-space:pre">    </span>  if (index >= params.length || index < 0) {  
  10.     <span style="white-space:pre">  </span>  <span style="white-space:pre"> </span>throw new RuntimeException("Index out of bounds");  
  11.  <span style="white-space:pre">  </span>  }  
  12.  <span style="white-space:pre">  </span>  if (!(params[index] instanceof Class)) {  
  13.    <span style="white-space:pre">       </span> return Object.class;  
  14.   <span style="white-space:pre"> </span>  }  
  15.    <span style="white-space:pre">   </span> return (Class) params[index];  
  16.     }  
  17. }  

继承类:

[java] view plaincopy

  1. public class B extends A<String> {  
  2. public static void main(String[] args) {  
  3.           B bb =new B();  
  4.               bb.getTClass();//即答案
  5.      }  
  6. }  

我相信很多人用过这样类似的代码,但是并不是每个人都真正了解了,为什么可以这样做。

光是看代码:

我们可以

[java] view plaincopy

  1. getGenericSuperclass();  

却没有:

[java] view plaincopy

  1. getGenericclass();  

为什么呢?

stackoverflow上有个老外说:java 里如果 一个类继承了另外一个带模板参数的类,那么这个模板参数不会被“类型擦除”。而单一的一个类,其泛型参数会被擦除。

首先说明这种假设是错误的。我相信JCP一定会不会莫名其妙的有这种无厘头规定。如果这种说法成立,就等于:

[java] view plaincopy

  1. public class C<T> {  
  2. }  
  3. public class C1<T> extends C<T>{  
  4. public C1()  
  5.     {  
  6.     }  
  7. public Class getTClass(int index) {  
  8.           Type genType = getClass().getGenericSuperclass();  
  9. if (!(genType instanceof ParameterizedType))   
  10.           {  
  11. return Object.class;  
  12.           }  
  13.           Type[] params = ((ParameterizedType) genType).getActualTypeArguments();  
  14. if (index >= params.length || index < 0) {  
  15. throw new RuntimeException("Index outof bounds");  
  16.           }  
  17. if (!(params[index] instanceof Class)) {  
  18. return Object.class;  
  19.           }  
  20. return (Class) params[index];  
  21.      }  
  22. }  

直接调用C1来创建实体后:

[java] view plaincopy

  1. public static void main(String[]args)  
  2.     {  
  3.         C1<D> a= new C1<D>();  
  4.         System.out.println(a.getTClass(0));  
  5.     }  

能输出:D

但是实际情况是,没有输出D,输出的是 Object.

说明这跟是不是父类子类没关系。

那么究竟是什么原因,导致了,一个泛型类(含有模板参数的类),没有像C#中 GetGenericArguments()类似的getGenericClass()函数呢??

再来温习下java 中 “类型擦除” 的范畴。

我用自己的语言定义一下(未必精确,但求理同):

Java中所有除了【类声明区域】(区域划分如下)之外的代码中,所有的泛型参数都会在编译时被擦除。

[java] view plaincopy

  1. public class/interface [类声明区域] {  
  2.     [类定义区域]  
  3. }  

如下的2个类:

[java] view plaincopy

  1. public class E<T>{  
  2. public static void main(String []args)  
  3.     {  
  4.         List<String> a = new ArrayList<String>();  
  5.         System.out.println("Done");  
  6.     }  
  7. }  
  8. class SubE extends E<String>{  
  9. }  

在编译后的class文件代码为:(By Java De-compiler)

[java] view plaincopy

  1. public class E<T>  
  2. {  
  3. public static void main(String[] args)  
  4.   {  
  5.     List a = new ArrayList(); //区别在这一行
  6.     System.out.println("Done");  
  7.   }  
  8. }  

[java] view plaincopy

  1. class SubE extends E<String>  
  2. {  
  3. }  

可以看到,【类声明区域】和java文件中的一模一样。而【类定义区域】中所有的泛型参数都被去掉了。

那么为啥这样呢?一个类,在编程中宿命的只有两大类:要么被继承,要么自己创建实例。直接用于创建实例时必在【类定义区域】,从而必定被擦除。只有被继承时,子类的实例信息中会存在一个夫类的泛型信息。

为何要有类型擦除?这涉及到Java语言的特性,JDK 从1.5(应该是)开始支持泛型,但是只能说是Java语法支持泛型了,JVM并不支持泛型,不少人笑称其为 “假泛型”。

虽然会被擦除,但是《Effective Java》 2th Edtion 还是建议大家在编程中,明确限定原型类,这样可以更好的约束代码,在编译期间提示。如果确实不在乎列表元素的类型是否一致,请使用 List<?>。

所以当我们使用 

List<String>的时候,编译器看到的不是String,而是一个Object(java中所有类型都继承于Object)。

一旦【类定义区域】中的泛型参数被擦除了。那么使用这个模板类创建实例,运行时,JVM反射是无法获取此模板具体的类型的。

因此

像C#中 GetGenericArguments()类似的getGenericClass()函数,在java中毫无意义。

这里的毫无意义是指在上面所说的java和jvm的特性的基础上。(若jvm支持真泛型,那么这一切就有意义了)

为什么说他无意义,反证法:

假设这个函数存在,有意义,那么下面代码可以获取T.Class

[java] view plaincopy

  1. class A<T>{  
  2. public Class getTclass()   
  3.   {  
  4. return this.getGenericClasses()[0];//这里只有一个模板参数
  5.   }  
  6. public static void main(String []args)  
  7.   {  
  8.       A<Integer> a= new A<Integer>();  
  9.       System.out.print(a.getTclass());  
  10.   }  
  11. }  

这样的一段代码会被编译成:

[java] view plaincopy

  1. class A<T>{  
  2. public Class getTclass()   
  3.   {  
  4. return this.getGenericClasses()[0];//这里只有一个模板参数
  5.   }  
  6. public static void main(String []args)  
  7.   {  
  8.       A a= new A();  
  9.       System.out.print(a.getTclass());  
  10.   }  
  11. }  

这时候问题就来了,JVM执行的是.class文件,而不是.java文件,在JVM看来,这个class文件里没有说任何关于T的信息,现在你问我要T.class 我该拿什么给你?

这样一来,

[java] view plaincopy

  1. getGenericClasses()  

这个函数永远都返回 java.lang.Object. 等于没返回。

所以这个函数没有必要存在了。

回头想想,之所以

getGenericSuperclass();有效,其本质在于

在子类的java文件中的【类声明区域】指定了T的真正类。

如:

public class B extends A<Integer>{

//...

}

这样一个小悬案就明朗了。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏有趣的Python

3-Linux C语言结构体-学习笔记

将#include <stdio.h>中stdio.h展开,将未注释的内容直接写入.i文件。

2631
来自专栏个人随笔

深入理解Java异常处理机制 (笼统篇)

801
来自专栏机器学习算法与Python学习

python基础-字符串与编码

转载于:廖雪峰的官方网站-python教程 字符编码 我们已经讲过了,字符串也是一种数据类型,但是,字符串比较特殊的是还有一个编码问题。 因为计算机只能处理数字...

47611
来自专栏Linyb极客之路

JVM 方法内联

调用某个函数实际上将程序执行顺序转移到该函数所存放在内存中某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方。

2004
来自专栏九彩拼盘的叨叨叨

学习纲要:JavaScript 数据类型

701
来自专栏Java3y

泛型就这么简单

1634
来自专栏测试开发架构之路

总结了一些指针易出错的常见问题(四)

指针与结构体 简介:我们可以使用C的结构体来表示数据结构元素,比如链表或树的节点,指针是把这些元素联系到一起的纽带。 typedef struct _pers...

2947
来自专栏chenjx85的技术专栏

leetcode-697-Degree of an Array

2504
来自专栏Kevin-ZhangCG

[ Java学习基础 ] Java异常处理

3046
来自专栏青青天空树

C语言中把数字转换为字符串 【转】

在将各种类型的数据构造成字符串时,sprintf 的强大功能很少会让你失望。由于sprintf 跟printf 在用法上几乎一样,只是打印的目的地不同而已,前者...

3.5K5

扫码关注云+社区

领取腾讯云代金券