`

java泛型那些事

 
阅读更多

学习笔记,转自:http://www.techug.com/post/java-generic-type.html

泛型的类型安全性

有许多原因促成了泛型的出现,而最引人注意的一个原因,就是为了创建容器类。

如果没有泛型,如果我们需要实现一个通用的队列,那么只能使用Obejct数组去实现,并且add方法的参数和get方法的返回值都为Object:

public class MyList {
    private Object[] mData;

    public void add(Object obj) {
        ...
    }

    public Object get(int index) {
        ...
    }
    ...
}

但是这样的话其实是很不安全的,类型安全需要靠用户去自己维护。但用户往往都是愚蠢的:

MyList myList = new MyList();
myList.add("1");
myList.add("2");
myList.add(3);

String val1 = (String) myList.get(0);
String val2 = (String) myList.get(1);
String val3 = (String) myList.get(2);

上面的代码在编译的时候没有问题,但是真正运行的时候程序跑着跑着就挂了,这就叫做类型不安全的设计。

使用泛型的意义在于它是类型安全的,如果使用泛型规定了参数和返回值的类型的话,上面的代码在编译的时候就会失败:

public class MyList<E> {
  private Object[] mData;

  ...

  public void add(E obj) {
    ...
  }

  public E get(int index) {
    ...
    return (E) mData[index];
  }
}

MyList<String> myList = new MyList<>();
myList.add("1");
myList.add("2");
myList.add(3); //这里会编译失败

String val1 = myList.get(0);
String val2 = myList.get(1);
String val3 = myList.get(0);

类型标识符

在MyList<E>声明尖括号里面的就是类型标识符,它其实是一个占位符,代表了某个类型,我们在类里面就能用这个占位符代表某种类型。例如add方法的参数或者get的返回值,当然也能用来声明一个成员变量。

可能有人会说经常看到都是用T泛型作为泛型标识符,为什么这里我们用E呢?

其实用什么字母做标识符在java里面并没有硬性规定,甚至你也可以不用仅一个字符,用一个单词也是可以的。

不过我们通常会按照习惯在不同场景下用不同的字母标识符:

  • E – Element (在集合中使用)
  • T – Type(Java 类)
  • K – Key(键)
  • V – Value(值)

泛型通配符

在泛型中有个很重要的知识点就是泛型类型之间是不具有继承关系的,也就是说List<Object>并不是List<String>的父类:

public void printList(List<Object> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}


List<String> strList = Arrays.asList("a", "b", "c", "d", "e");
printList(strList); //错误,List<Object>不是List<String>的父类

为了实现上面的printList方法,类型通配符就出现了:

public void printList(List<?> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}

List<String> strList = Arrays.asList("a", "b", "c", "d", "e");
printList(strList);

List<?>可以匹配List<String>、List<Integer>等等的各种类型。

大家有可能会听过类型通配符上限和下限,这两个东西是怎样的概念呢?有时候我们会需要限定只能传入某些型的子类或者父类的容器:

  • 上限:<? extends T> 只能匹配T和T的子类
  • 下限:<? super T> 只能匹配T和T的父类
//只能传入ClassA的子类的容器
public void printList(List<? extends ClassA> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}

//只能传入ClassA的父类的容器
public void printList(List<? super ClassA> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}

除了上面的通配符”?”,我们还可以直接用泛型方法去实现printListde,可以指定所有类型的列表或者ClassA的子类的列表:

public <T> void printList(List<T> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}

public <T extends ClassA> void printList(List<T> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}

当然我们也能使用泛型的方式直接指定参数的上限,比如下面的foo方法就只能接收Number的子类:

public <T extends Number> void foo(T arg){
    ...
}

但是如果直接使用泛型的方式的话我们不能指定指定它的下限,例如下面两种写法都是不能通过编译的:

//错误.不能直接指定泛型的下限
public <T super Number> void printList(List<T> list) {
    ...
}

//错误.不能直接指定泛型的下限
public <T super Number> void foo(T arg){
    ...
}

类型擦除

可能很多同学都会听说过泛型类型擦除的概念,这个类型擦除具体指的是怎样一回事?

可以看看下面的foo方法,它本来想实现的功能是:如果传入的参数非空,就将它返回。否则,就创建一个同类型的实例返回。但是这段代码是不能通过编译的:

//错误,泛型的类型被擦除了,不能直接new出来
public <T> void foo(T arg) {
    return arg != null ? arg : new T();
}

原因在于java的泛型实现中有个叫做类型擦除的机制。简单来讲就是运行的时候是无法获取到泛型使用的实际类型的。

例如上面的T类型,因为我们在运行时不能知道它到底是什么类型,所以也就无法将它new出来。

java代码生成的Java字节代码中是不包含泛型中的类型信息的,所有泛型类的类型参数在编译时都会被擦除。虚拟机中没有泛型,只有普通类和普通方法。因此泛型的类型安全是在编译的时候去检测的。

所以我们创建泛型对象时需要指明类型,让编译器尽早的做参数检查。

像下面的代码可以顺利通过,甚至可以正常运行,直到将获取到的数值类型的数据强转成字符串的时候才报ClassCastException异常:

List list = new ArrayList<String>();
list.add("abc");
list.add(123);
String elemt1 = (String) list.get(0);
String elemt2 = (String) list.get(1); // java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

我们可以用反射的方法的验证一下类型擦除:

List<Integer> list = new ArrayList<Integer>();
System.out.println("type : " + Arrays.toString(list.getClass().getTypeParameters()));

它得到的类型仅仅是一个占位符而已:

type : [E]

类型擦除机制的历史原因

有人会问,为什么java会在编译的时候将类型擦除,而不像c++一样通过在编译的时候将泛型类实例化为多个具体的类去实现泛型呢?

其实“实例化为多个具体的类”这样的实现方式也是比较容易实现的,但是为了保持兼容性,所以java在泛型的实现上选取类型擦除的方式。实际上是做了一定的取舍的。

为什么说选用类型擦除是为了保持兼容性呢?因为泛型并不是java与生俱来的。实际上到了java5的时候才引入了泛型。

要让以前编译的程序在新版本的JRE还能正常运行,就意味着以前没有的限制不能突然冒出来。

例如在泛型出来之前java就已经有了容器的存在,而且它具有可以存储不同类型的的特性:

ArrayList things = new ArrayList();
things.add(Integer.valueOf(123));
things.add("abc");

那么这段代码在Java 5引入泛型之后还必须要继续可以运行。

这里有两种设计思路:

  1. 需要泛型化的类型(主要是容器(Collections)类型),以前有的就保持不变,然后平行地加一套泛型化版本的新类型;
  2. 直接把已有的类型泛型化,让所有需要泛型化的已有类型都原地泛型化,不添加任何平行于已有类型的泛型版。

.NET在1.1 -> 2.0的时候选择了上面选项的1,而Java则选择了2。

从Java设计者的角度看,这个取舍很明白:.NET在1.1 -> 2.0的时候,实际的应用代码量还很少(相对Java来说),而且整个体系都在微软的控制下,要做变更比较容易;

而在Java 1.4.2 -> 5.0的时候,Java已经有大量程序部署在生产环境中,已经有很多应用和库程序的代码。如果这些代码在新版本的Java中,为了使用Java的新功能(例如泛型)而必须做大量源码层修改,那么新功能的普及速度就会大受影响,而且新功能会被吐槽。

在原地泛型化后,java.util.ArrayList这个类型变成了java.util.ArrayList<E>。但是以前的代码直接用ArrayList,在新版本里必须还能继续用,所以就引出了“raw type”的概念——一个类型虽然被泛型化了,但还可以把它当作非泛型化的类型用。

ArrayList         - raw type
ArrayList<E>      - open generic type (assuming E is type variable)
ArrayList<String> - closed generic type
ArrayList<?>      - unbounded wildcard type

下面这样的代码必须可以编译运行:

ArrayList<Integer> ilist = new ArrayList<Integer>();
ArrayList<String> slist = new ArrayList<String>();
ArrayList list; // raw type
list = ilist;   // assigning closed generic type to raw type
list = slist;   // ditto

所以java的设计者在考虑了这一点之后选用类型擦除也就显而易见了。类型擦除实际上是将泛型类型转换了Obejct。由于所有的java类都是Object的子类,所以实现起来就很简单了。只需要在编译的时候将所有的泛型占位符都换成Object就可以了:

//源码的泛型代码
public <T> void foo(T arg){
    ...
}

//编译时转换成的代码
public void foo(Object arg){
    ...
}

而在擦除类型的同时,java编译器会对该方法的调用进行类型检查,防止非法类型的调用。

但如果在编写代码的时候就已经用raw type的话,编译器就不会做类型的安全性检查了。

这样的实现导致了一个问题,List<E>泛型参数E被擦除后就变成了Object,那么就不能在泛型中使用int、long等原生数据类型了,因为它们并不是Object的子类。

据说当时设计java语言的程序员和产品经理打了一架,并且在打赢之后成功劝服产品经理在提出兼容性这样奇葩的需求之后做出一点小小的让步。(虽然只是我胡说八道的脑补,但谁知道当时的实际情形是不是这样的呢?)

于是乎我们现在在泛型中只能使用Integer、Long等封箱类型而不能用int、long等原生类型了。

分享到:
评论

相关推荐

    JAVA泛型加减乘除

    这是一个使用JAVA实现的泛型编程,分为两部分,第一部分创建泛型类,并实例化泛型对象,得出相加结果。 第二部分用户自行输入0--4,选择要进行的加减乘除运算或退出,再输入要进行运算的两个数,并返回运算结果及...

    Java泛型的用法及T.class的获取过程解析

    Java泛型的用法及T.class的获取过程解析 Java泛型是Java编程语言中的一种重要特性,它允许开发者在编写代码时指定类型参数,从而提高代码的灵活性和可读性。本文将详细介绍Java泛型的用法 及T.class的获取过程解析...

    Java泛型应用实例

    Java泛型是Java编程语言中的一个强大特性,它允许我们在定义类、接口和方法时指定类型参数,从而实现代码的重用和类型安全。在Java泛型应用实例中,我们可以看到泛型如何帮助我们提高代码的灵活性和效率,减少运行时...

    很好的Java泛型的总结

    Java泛型机制详解 Java泛型是Java语言中的一种机制,用于在编译期检查类型安全。Java泛型的出现解决了Java早期版本中类型安全检查的缺陷。Java泛型的好处是可以在编译期检查类型安全,避免了运行时的...

    Java泛型三篇文章,让你彻底理解泛型(super ,extend等区别)

    Java 泛型详解 Java 泛型是 Java SE 5.0 中引入的一项特征,它允许程序员在编译时检查类型安全,从而减少了 runtime 错误的可能性。泛型的主要优点是可以Reusable Code,让程序员编写更加灵活和可维护的代码。 ...

    java泛型技术之发展

    Java泛型是Java编程语言中的一个关键特性,它在2004年随着Java SE 5.0的发布而引入,极大地增强了代码的类型安全性和重用性。本篇文章将深入探讨Java泛型的发展历程、核心概念以及其在实际开发中的应用。 1. **发展...

    1.java泛型定义.zip

    1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1....

    java 泛型接口示例

    下面我们将详细探讨Java泛型接口的相关知识点。 1. **泛型接口的定义** 泛型接口的定义方式与普通接口类似,只是在接口名之后添加了尖括号`&lt;T&gt;`,其中`T`是一个类型参数,代表某种未知的数据类型。例如: ```java...

    java 泛型方法使用示例

    下面我们将深入探讨Java泛型方法的概念、语法以及使用示例。 **一、泛型方法概念** 泛型方法是一种具有类型参数的方法,这些类型参数可以在方法声明时指定,并在方法体内部使用。与类的泛型类似,它们提供了编译时...

    4.java泛型的限制.zip

    4.java泛型的限制.zip4.java泛型的限制.zip4.java泛型的限制.zip4.java泛型的限制.zip4.java泛型的限制.zip4.java泛型的限制.zip4.java泛型的限制.zip4.java泛型的限制.zip4.java泛型的限制.zip4.java泛型的限制.zip...

    java 泛型类的类型识别示例

    综上所述,虽然Java泛型在编译后会进行类型擦除,但通过上述技巧,我们仍然能够在运行时获得关于泛型类实例化类型的一些信息。在实际开发中,这些方法可以帮助我们编写更加灵活和安全的代码。在示例文件`GenericRTTI...

    java泛型学习ppt

    "Java 泛型学习" Java 泛型是 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的...

    java泛型的内部原理及更深应用

    Java泛型是Java编程语言中的一个强大特性,它允许在定义类、接口和方法时使用类型参数,从而实现参数化类型。这使得代码更加安全、可读性更强,并且能够减少类型转换的必要。在“java泛型的内部原理及更深应用”这个...

    Java 泛型擦除后的三种补救方法

    Java 泛型是一种强大的工具,它允许我们在编程时指定变量的类型,提供了编译时的类型安全。然而,Java 的泛型在运行时是被擦除的,这意味着在运行时刻,所有的泛型类型信息都会丢失,无法直接用来创建对象或进行类型...

    SUN公司Java泛型编程文档

    Java泛型是Java编程语言中的一个关键特性,它在2004年随着JDK 5.0的发布被引入。这个特性极大地提高了代码的类型安全性和可读性,减少了在运行时出现ClassCastException的可能性。SUN公司的Java泛型编程文档,包括...

    Java泛型使用详细分析.pdf

    Java 泛型使用详细分析 Java 泛型是 Java 语言中的一种类型系统特性,允许开发者在编译期检查类型安全,以避免在运行时出现类型相关的错误。在本文中,我们将详细介绍 Java 泛型的使用方法和实现原理。 一、泛型的...

Global site tag (gtag.js) - Google Analytics