什么是泛型,有什么用?

先运行下面的代码:

  1. public class Test {
  2. public static void main(String[] args) {
  3. Comparable c=new Date(); //Date实现了Comparable接口的
  4. System.out.println(c.compareTo("red")); //这里可以通过编译,但在运行的时候会抛出异常。ClassCastException: java.lang.String cannot be cast to java.util.Date
  5. }
  6. }

上面的代码稍微修改下:

  1. public class Test {
  2. public static void main(String[] args) {
  3. Comparable<Date> c=new Date(); //这行修改了,加入了泛型
  4. System.out.println(c.compareTo("red")); //这行会提示错误:compareTo (java.util.Date) in Comparable cannot be applied to (java.lang.String)
  5. }
  6. }

对比上面的代码,没加入泛型的时候,在程序运行期才发现问题,而加入了泛型则在程序编译期就发现了,这就是泛型的优势所在。

在第二段代码中,泛型就好象是在告诉编译器:这里声明的变量c只跟Date类型进行比较,如果跟别的类型比较,那么就不能通过编译。

再用ArrayList的例子看下

  1. public class Test {
  2. public static void main(String[] args) {
  3. List list=new ArrayList(); //没有泛型
  4. list.add("HTTP"); //添加的是String类型
  5. list.add("FTP"); //添加的是String类型
  6. list.add("SMTP"); //添加的是String类型
  7. list.add(1024); //不小心添加了个Integer类型,编译和运行期都不会报错
  8. Object http = list.get(0); //取出第一个String元素,类型变成了Object
  9. String ftp = list.get(1); //编译错误:取出第二个String元素,却不能直接赋值给String类型变量
  10. String smtp = (String) list.get(2); //取出第三个String元素,经强制类型转换赋值给String类型
  11. }
  12. }

上面的代码就是在Java1.5泛型加入前的办法,list中可以加入的是Object类型,同时加入String和Integer都没问题,然后不管加进去的是什么类型,取出来的时候都成了Object,还得强制转换成自己的类型。但在实际编码中往往只是添加同类型的元素,得小心翼翼的保证不会加入其他类型,否则在取出来的时候会出意外。泛型加入后,如果不小心添加了非指定类型元素,那根本不能通过编译,在取出来的时候,直接就是指定的类型,而不再是Object。

也就是说对类型的保证由程序员转到了编译器,将可能的错误从运行期转到了编译期。其实泛型只是一层皮,功能的实现还是Object+强制转换。

泛型就是Java的语法糖,所谓语法糖就是:这种语法对功能没有影响,但更方便程序员使用,虚拟机并不支持这些语法,在编译阶段就被还原成了简单语法结构。Java中的其他语法糖还有变长参数、自动拆装箱、内部类等。

自定义泛型类

设想有这样的一个类:用来盛装两个对象,但其类型不确定,可能是String,可能是File,可能是自定义的。我们引入一个类型参数T来代替它可能盛装的类型:

  1. public class Pair<T> { //这里的<T>就是类型参数,写在类名的后面
  2. private T first;
  3. private T second;
  4. public Pair() {
  5. first = null;
  6. second = null;
  7. }
  8. public Pair(T first, T second) {
  9. this.first = first;
  10. this.second = second;
  11. }
  12. public T getFirst() {
  13. return first;
  14. }
  15. public T getSecond() {
  16. return second;
  17. }
  18. public void setFirst(T first) {
  19. this.first = first;
  20. }
  21. public void setSecond(T second) {
  22. this.second=second;
  23. }
  24. public String toString(){
  25. return first.toString()+" "+second.toString(); //甚至可以调用T的方法
  26. }
  27. }

类型参数:

写在类名后面

可以有多个,比如:<K,V>

T一般代表任意类型,E一般用于集合类型中,KV一般用于键值类型

类型参数T可以用在实例变量、局部变量、方法返回值中

可以调用类型参数T的方法。既然不知道T具体是什么类型,那咋能调用其方法呢?后面再说

使用Pair类:

  1. public class PairTest1 {
  2. public static void main(String[] args) {
  3. String[] words = {"Mary", "had", "a", "little", "las"};
  4. Pair<String> mm = ArrayAlg.minmax(words); //指定mm中装的是String类型
  5. System.out.println("min= " + mm.getFirst());
  6. System.out.println("max= " + mm.getSecond());
  7. }
  8. }
  9. class ArrayAlg{
  10. public static Pair<String> minmax(String[] a) { //计算一个字符串数组中的最大、最小值
  11. if (a == null || a.length == 0) {
  12. return null;
  13. }
  14. String min = a[0];
  15. String max = a[0];
  16. int len = a.length;
  17. for (int i = 1; i < len; i++) {
  18. if (min.compareTo(a[i]) > 0) {
  19. min = a[i];
  20. }
  21. if (max.compareTo(a[i]) < 0) {
  22. max = a[i];
  23. }
  24. }
  25. return new Pair(min, max);
  26. }
  27. }

自定义泛型方法

定义泛型接口跟定义泛型类是一样,另外还可以在一个非泛型类中定义泛型方法,当然也可以定义在泛型类中

看代码:

  1. public class ArrayAlg { //类中没有泛型参数,这不是个泛型类
  2. public static <T> T getMiddle(T[] a) { //类型参数在方法中,这是个泛型方法,类型参数放在返回值前修饰符后。该方法返回一个数组中间那个元素
  3. return a[a.length/2];
  4. }
  5. }

使用该泛型方法

  1. public class Test {
  2. public static void main(String[] args) {
  3. Integer[] a = {54, 42, 94, 23, 34};
  4. Integer middle = ArrayAlg.<Integer>getMiddle(a); //使用的时候在方法调用前指定类型实参,其实也可以省略,编译器能通过方法中的实参类型自动判定类型实参
  5. System.out.println(middle);
  6. }
  7. }

对类型参数进行限定

看下面的代码,计算一个数组中的最小元素:

  1. public class ArrayAlg {
  2. public static <T> T min(T[] a) {
  3. if (a == null || a.length == 0) {
  4. return null;
  5. }
  6. T smallest = a[0];
  7. for (int i=1;i<a.length;i++) {
  8. if (smallest.compareTo(a[i]) > 0) { //这里会有编译错误,提示smallest没有compareTo()方法
  9. smallest = a[i];
  10. }
  11. }
  12. return smallest;
  13. }
  14. }

上面代码中,需要调用类型参数T的compareTo()方法,但T有没有这个方法呢?不知道。但是如果能明确告知T实现了Comparable接口,那T类型就一定是具有compareTo()方法的,这就是类型限定。

在声明类型参数处改为这样: public static <T extends Comparable> T min(T[] a) {

类型限定:

形式: <T extends BoundingType> ,关键字就是extends,没有别的比如implements

可以进行多个限定:比如:<T extends Comparable & Serializable> .注意这是且的关系

可以限定多个接口,但最多只能限定一个类,且这个类得排在第一位

<T extends Comparable>,这里指定了T的上限是Comparable,那么<T>的上限是什么呢?是Object

正是因为有了类型限定,才能调用T的方法。

泛型的擦除

前面说了,Java的泛型只是语法糖,仅仅存在于源码层面。

编译后的字节码中是没有泛型的。

虚拟机中更没有泛型类,所有的类都是普通类。

编译器在编译的时候,会将泛型信息擦除,转换为原始类,用限定类型替换泛型T,再插入必要的强制类型转换或者桥方法。

如果有多个限定类型,那就用第一个替换,因此标记式接口要放到泛型列表的最后。

比如:

  1. public class Pair<T>{
  2. private T first;
  3. public T getFirst(){
  4. return first;
  5. }
  6. }

擦除泛型变成这样了:

  1. public class Pair{
  2. private Object first;
  3. public Object getFirst(){
  4. return first;
  5. }
  6. }

下面使用这个getFirst()方法,代码片段:

  1. Pair<Employee> buddies=....;
  2. Employee buddy=buddies.getFirst();

上面这个代码片段中,buddies.getFirst()取出来的为啥直接就是Employee呢,不是Object呢?因为在编译的时候,编译器自动插入了强制类型转换,大概是这样子Employee buddy=(Employee) buddies.getFirst();

桥方法--与多态的冲突

前面的Pair类类型擦除后,大概是这样的:

  1. public class Pair{
  2. ...
  3. public void setSecond(Object second){
  4. this.second=second;
  5. }
  6. }

下面用一个DateInterval类继承Pair,并重写setSecond()方法,确保seconde一定大于first:

  1. public class DateInterval extends Pair<Date>{
  2. ...
  3. @Override //注意这个注解,没有报错,说明重写成功了的
  4. public void setSecond(Date second){ //该方法确保了第二个日期一定大于第一个
  5. if(second.compareTo(getFirst())>=0){
  6. super.setSecond(second);
  7. }
  8. }
  9. }

问题来了,setSecond(Date second)重写了setSecond(Object second),这不科学啊。

实际上,DateInterval编译后新增加了一个桥方法

  1. public void setSecond(Object second){ //实际完成重写的是这个桥方法
  2. setSecond((Date)Object); //调用DateInterval的setSecond(Date second)方法
  3. }

除了set外,还有get也存在问题:

DateInterval中会存在这样的两个方法:

  1. public Object getSecond(){}
  2. public Date getSecond(){}

这里两个get方法的方法签名是相同的,不能共存于一个类中。

但其实它们就是能共存,因为虚拟机辨别方法的时候还加上了返回值类型。

所以啊,泛型遇到编译和虚拟机有点特别了:

虚拟机没有泛型,只有普通的类和方法

类型参数要被其限定类型替换

插入桥方法来保持多态

插入了强制类型转换

总结

  • 泛型就是语法糖,仅存在于源代码中,功能的实现还是Object(限定类型)+强制类型转换实现
  • 泛型能保证程序员少犯错误,将发现错误的时间点从运行期提到编译期
  • 可以定义泛型类、泛型接口、泛型方法。泛型类和泛型接口,把类型参数写在类名或接口名前面;泛型方法将类型参数写到方法名前面
  • 类型参数可以进行限定,且能限定多个,可以是[0,1]个类+[0,n]个接口,如果有类,那得写在第一位,标记式接口要写在最后
  • 泛型在编译后,都会被擦除成原始类型,用限定类型替代类型参数,字节码和虚拟机中是没有泛型的
  • 编译的时候,还会根据需要,加入桥方法,以保持多态

0072 Java中的泛型--泛型是什么--泛型类--泛型方法--擦除--桥方法的更多相关文章

  1. JAVA 中两种判断输入的是否是数字的方法__正则化_

    JAVA 中两种判断输入的是否是数字的方法 package t0806; import java.io.*; import java.util.regex.*; public class zhengz ...

  2. 转:Java中子类是否可以继承父类的static变量和方法而呈现多态特性

    原文地址:Java中子类是否可以继承父类的static变量和方法而呈现多态特性 静态方法 通常,在一个类中定义一个方法为static,那就是说,无需本类的对象即可调用此方法,关于static方法,声明 ...

  3. java中多线程执行时,为何调用的是start()方法而不是run()方法

    Thead类中start()方法和run()方法的区别 1,start()用来启动一个线程,当调用start()方法时,系统才会开启一个线程,通过Thead类中start()方法来启动的线程处于就绪状 ...

  4. Java中HashMap(泛型嵌套)的遍历

    //Studnet package yzhou.gen03; public class Student<T> { private T score; public T getScore() ...

  5. Java中url传递中文参数取值乱码的解决方法

    java中URL参数中有中文值,传到服务端,在用request.getParameter()方法,得到的常常会是乱码,这将涉及到字符解码操作. 方法一: http://xxx.do?ptname=’我 ...

  6. java中从含反斜杠路径截取文件名的方法

    例如:获取到的文件路径为C:\Documents and Settings\Leeo\My Documents\logo.gif现在想要取得图片的名称logo.gif,我们知道反斜杠“\”是转义字符, ...

  7. Eclipse中的快捷键快速生成常用代码(例如无参、带参构造,set、get方法),以及Java中重要的内存分析(栈、堆、方法区、常量池)

    (一)Eclipse中的快捷键:  ctrl+shift+f自动整理选择的java代码 alt+/ 生成无参构造器或者提升信息 alt+shift+s+o 生成带参构造 ctrl+shift+o快速导 ...

  8. JAVA中利用反射机制进行对象和Map相互转换的方法

    JAVA的反射机制主要作用是用来访问对象的属性.方法等等.所以,JAVA中对象和Map相互转换可以利用JAVA的反射机制来实现.例子如下: 一.对象转Map的方法 public static Map& ...

  9. Java中子类是否可以继承父类的static变量和方法而呈现多态特性

    静态方法 通常,在一个类中定义一个方法为static,那就是说,无需本类的对象即可调用此方法,关于static方法,声明为static的方法有以下几条限制: 它们仅能调用其他的static 方法. 它 ...

随机推荐

  1. Could not connect to Redis at 192.168.0.129:6379: Connection refused

    在虚拟机上(CentOS 6.7)本机连接自己的redis [root@localhost bin]# ./redis-cli -h Could not connect to Redis at : C ...

  2. qt study 元对象,属性和反射编程

    所谓反射,就是指对象成员的自我检查,使用反射编程(reflective programming),就可以编写出通用的操作,可以对具有不同结构的类进行操作. QMetaObject 元对象模式,描述一个 ...

  3. Vue组件开发实践之scopedSlot的传递

    收录待用,修改转载已取得腾讯云授权 导语 现今的前端开发都讲究模块化组件化,即把公共的交互和功能封装到一个个的组件之中,在开发整体界面的时候就能像搭积木一样快速清晰高效.在使用Vue开发我们的vhtm ...

  4. [NPM] Avoid Duplicate Commands by Calling one NPM Script from Another

    We can get a lot of utility through CLI tools invoked via npm scripts. Many of these tools have APIs ...

  5. T-SQL 之 触发器

    触发器可以做很多事情,但也会带来很多问题.正确的使用在于在适当的时候使用,而不要在不适当的时候使用它们. 触发器的一些常见用途如下: [1] 弹性参照完整性:实现很多DRI不能实现的操作(例如,跨数据 ...

  6. selenium 问题:OSError: [WinError 6] 句柄无效

    问题: 执行多个用例的时候,会抛出异常: File "xxxxxx.py", line 16, in get_driver driver = webdriver.Chrome(ex ...

  7. iOS tabbar 图片,最佳大小方式

    iOS tabbar 图片,最佳大小方式 文档大小 30 *30 retaina 60 *60 最佳大小 48 *32 参考:http://stackoverflow.com/questions/15 ...

  8. JS生成EXCEL(Chrome浏览器)

    直接使用js+Html生成excel文件,当前版本:chrome浏览器 <!DOCTYPE html> <html> <head> <meta charset ...

  9. VMware 9.0.1安装Mac OS X Mountain Lion 10.8.2

    原地址:http://zengwu3915.blog.163.com/blog/static/278348972013117114742496/ 所需软件1.VMware Workstation Bu ...

  10. Java网络编程-URI和URL

    前提 前面的一篇文章<Java中的Internet查询>分析完了如何通过IP地址或者主机名确定主机在因特网中的地址.任意给定主机上可能会有任意多个资源,这些资源需要有标识符方便主机之间访问 ...