一、基本概念和用法

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

泛型是JDK1.5的一项新特性,它的本质是将类型参数化,简单的说就是将所操作的数据类型指定为一个参数,在用到的时候通过传参来指定具体的类型。在Java中,这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。

一个泛型类的例子如下:

//将要操作的数据类型指定为参数T
public class Box<T> {
    private T t;

    public void add(T t) {
        this.t = t;
    }

    public T get() {
        return this.t;
    }
}
//使用的时候指定具体的类型为Integer
//那么Box类里面的所有T都相当于Integer了
Box<Integer> integerBox = new Box<Integer>();

泛型接口和泛型方法的定义和使用示例如下:

//泛型接口
interface Show<T,U> {
    void show(T t,U u);
}

class ShowTest implements Show<String,Date> {
    @Override
    public void show(String str,Date date) {
        System.out.println(str);
        System.out.println(date);
    }
}

public static void main(String[] args) {
    ShowTest showTest = new ShowTest();
    showTest.show("Hello",new Date());
}
//泛型方法
public <T, U> T get(T t, U u) {
    if (u != null)
        return t;
    else
        return null;
}

String str = get("Hello", "World");

从上面的例子可以看出,用尖括号<>来声明泛型变量,可以有多个类型变量,例如Show<T, U>,但是类型变量名不能重复,例如Show<T, T>是错误的。另外,类型变量名一般使用大写形式,且比较短(不强制,只是一种命名规约),下面是一些常用的类型变量:

  • E:元素(Element),多用于java集合框架
  • K:关键字(Key)
  • N:数字(Number)
  • T:类型(Type)
  • V:值(Value)
  • S:第二类型
  • U:第三类型

二、泛型变量的类型限定

类型限定就是使用extends关键字对类型变量加以约束。比如限定泛型参数只接受Number类或者子类Integer、Float等,可以这样限定,这样限定之后,实际参数只能是Number类或者Number的子类。下面举例详细说明:

//定义一个水果类
//里面有一个示例方法getWeight()可以获取水果重量
public class Fruit {
    public int getWeight() {
        return 10; //这里假设所有水果重量都是10
    }
}
public class Apple extends Fruit {}

--------------------------------------------------------------------------------------------

//定义泛型类Box,并限定类型参数为Fruit
public class Box<T extends Fruit> {}

--------------------------------------------------------------------------------------------

//由于Box限定了类型参数,实际类型参数只能是Fruit或者Fruit的子类
Box<Fruit> integerBox = new Box<Fruit>();//编译通过
Box<Apple> integerBox = new Box<Apple>();//编译通过
Box<Integer> integerBox = new Box<Integer>();//编译器报错

上面代码用虚线分为三个部分,第一个部分是举例用的,定义一个水果类Fruit和它的子类Apple;第二部分定义一个泛型类Box,并且限定了泛型参数为Fruit,限定之后,实际类型只能是Fruit或者Fruit的子类,所以第三部分,实际泛型参数是Integer就会报错。

通过限定,箱子Box就只能装水果了,这是有好处的,举个例子,比如Box里面有一个getBigFruit()方法可以比较两个水果大小,然后返回大的水果,代码如下:

public class Box<T extends Fruit>{

    public T getBigFruit(T t1, T t2) {

        // if (!(t1 instanceof Fruit) || !(t2 instanceof Fruit)) {
        //    throw new RuntimeException("T不是水果");
        // }

        if (t1.getWeight() > t2.getWeight()) {
            return t1;
        }
        return t2;
    }
}

代码中需要注意两个地方:一个是注释的三行,参数限定之后,没必要判断t1和t2的类型了,如果类型不对,在Box实例化的时候就报错了;另一个是t1.getWeight(),在Box类里面,t1是T类型,T类型限定为Fruit,所以这里可以直接调用Fruit里面的方法getWeight()(确切的说是可以调用Fruit里面可以被子类继承的方法,因为限定之后,实参也可以是Fruit的子类),如果不加限定,那么T就默认是Object类型,t1.getWeight()就会报错因为Object里面没有这个方法(调用Object里面的方法是可以的)。这就是是类型限定的两个好处。

类型也可以使用接口限定,比如,这样的话,只有实现了MyInterface接口的类才能作为实际类型参数。下面是类型限定的几个注意点:

  1. 不管限定是类还是接口,统一都使用extends关键字
  2. 可以使用&符号给出多个限定,例如:<U extends Number & MyInterface1 & MyInterface2>
  3. 多个限制只能有一个类名,其他都是接口名,且类名在最前面。

三、通配符

先看三行代码

Fruit f = new Apple();
Fruit[] farray = new Apple[10];
ArrayList<Fruit> flist = new ArrayList<Apple>();

第一行的写法是很常见的,父类引用指向子类对象,这是java多态的表现。类似的,第二行父类数组的引用指向子类数组对象在java中也是可以的,这称为数组的协变。Java把数组设计为协变的,对此是有争议的,有人认为这是一种缺陷。

虽然Apple[]可以“向上转型”为Fruit[],但数组元素的实际类型还是Apple,所以只能向数组中放入Apple或者Apple的子类。在上面的代码中,向数组中放入了Fruit对象和Orange对象,对于编译器来说,这是可以通过编译的,但是在运行时期,JVM能够知道数组的实际类型是Apple[],所以当其它对象加入数组的时候在运行期会抛出异常。

由上可知,协变的缺陷在于可能的异常发生在运行期,而编译期间无法检查,泛型设计的目的之一就是避免这种问题,所以泛型是不支持协变的,也就是说,上面的第三行代码是编译不通过的。但是,有时候是需要建立这种“向上转型”的关系的,比如定义一个方法,打印出任意类型的List中的所有数据,示例如下:

public void printCollection(List<Object> collection) {
    for (Object obj : collection) {
        System.out.println(obj);
    }
}

------------------------------------
List<Integer> listInteger =new ArrayList<Integer>();
List<String> listString =new ArrayList<String>();

printCollection(listInteger); //编译错误
printCollection(listString); //编译错误

因为泛型不支持协变,即List<Object> collection = new ArrayList<Integer>();无法通过编译,所以printCollection(listInteger)就会报错。
这时就需要使用通配符来解决,通配符<?>,用来表示某种特定的类型,但是不知道这个类型到底是什么。例如下面的例子都是合法的:

List<?> collection1 = new ArrayList<Fruit>();
List<?> collection2 = new ArrayList<Number>();
List<?> collection3 = new ArrayList<String>();
List<?> collection4 = new ArrayList<任意类型>();
// 对比不合法的 List<Fruit> flist = new ArrayList<Apple>();

所以printCollection()方法改成下面这样即可:

public void printCollection(List<?> collection) {
    for (Object obj : collection) {
        System.out.println(obj);
    }
}

这就是通配符的简单用法。需要注意的是,因为不知道 "?" 类型到底是什么,所以List<?> collection中的collection不能调用带泛型参数的方法,但是可以调用与泛型参数类型无关的方法,如下:

collection.add("a"); //错误,因为add方法参数是泛型E
collection.size(); //正确,因为无参即与泛型参数类型E无关
collection.contains("a"); //正确,因为contains参数是Object类型,与泛型参数类型E无关

注:collection.add(null);是可以的,除了null其他任何类型都不可以,Object也不行。

通配符的边界

通配符可以使用extends和super关键字来限制:

  • List<? extends Number> 表示不确定参数类型,但必须是Number类型或者Number子类类型,这是上边界限定
  • List<? super Number> 表示不确定参数类型,但必须是Number类型或者Number的父类类型,这是下边界限定
  • List<?> 表示未受限的通配符,相当于 List<? extends Object>

注意区分 泛型变量的类型限定通配符的边界限定

  1. 泛型变量的类型限定,是在定义泛型类的时候对声明的泛型参数进行限定(限定的是形式参数)
    public class Box{}
  2. 通配符的边界限定,是在定义化泛型类的引用的时候对实际泛型参数进行限定(限定的是实际参数)
    List<? extends Number> listInteger =new ArrayList();

泛型变量的类型限定只能使用extends关键字,通配符的边界限定可以使用extends或super来限定上边界或下边界。


四、Java泛型的原理-类型擦除

Java中的泛型是通过类型擦除来实现的伪泛型。类型擦除指的是从泛型类型中清除类型参数的相关信息,并且在必要的时候添加类型检查和类型转换的方法。类型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通的java字节码,看下面的例子:

泛型的Java代码如下:

class Pair<T> {
    private T value;
    public T getValue() {
        return value;
    }
    public void setValue(T  value) {
        this.value = value;
    }
}

泛型Java代码,经过编译器编译后,会擦除泛型信息,将泛型代码转换为如下的普通Java代码:

class Pair {
    private Object value;
    public Object getValue() {
        return value;
    }
    public void setValue(Object  value) {
        this.value = value;
    }
}

由上面的例子可知,泛型擦除的结果就是用Object替换T,最终生成一个普通的类。上面的例子替换成Obejct是因为在Pair中,T是一个无限定的类型变量,所以用Object替换。如果T被限定了,比如,那么擦除后就用Number替换泛型类里面的T。多个限定的话,使用第一个边界的类型变量来作为原始类型。
至此可以知道,类型擦除的过程:

  1. 移除所有的类型参数。
  2. 将所有移除的泛型参数用其限定的最左边界类型替换。(多个限定的话,其他限定一定是接口,而且实际参数一定实现了这些接口,否则不合法,编译不通过,所以用最左边界类型替换)

泛型只存在于代码中,泛型信息在编译时都会被擦除,所以虚拟机中没有泛型,只有普通类和普通方法。


Java泛型的一些注意问题

使用泛型时会有一些问题和限制,大部分是由类型擦除引起的,所以只要记住:泛型擦除后留下的只有原始类型。那么大部分问题都是很容易理解的。比如下面的例子:

public void test(List<String> list){

}

public void test(List<Integer> list){

}

两个方法经过泛型擦除后,都只留下原始类型List,所以它们是同一个方法而不是方法的重载,如果这两个方法在同一个类中同时存在,编译器是会报错的。

其他更多问题如下:

  1. 先检查,在编译,以及检查编译的对象和引用传递的问题
  2. 自动类型转换
  3. 类型擦除与多态的冲突和解决方法
  4. 泛型类型变量不能是基本数据类型
  5. 运行时类型查询
  6. 异常中使用泛型的问题
  7. 数组(这个不属于类型擦除引起的问题)
  8. 类型擦除后的冲突
  9. 泛型在静态方法和静态类中的问题

这些问题的答案在:java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题,本文也是学习这篇博客和一些其他博客后的一个总结。

参考文章:
Java泛型-类型擦除
java泛型(一)、泛型的基本介绍和使用
java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题
Java 泛型总结(三):通配符的使用

Java泛型总结---基本用法,类型限定,通配符,类型擦除的更多相关文章

  1. Effective Java 第三版——31.使用限定通配符来增加API的灵活性

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  2. Java 泛型 四 基本用法与类型擦除

    简介 Java 在 1.5 引入了泛型机制,泛型本质是参数化类型,也就是说变量的类型是一个参数,在使用时再指定为具体类型.泛型可以用于类.接口.方法,通过使用泛型可以使代码更简单.安全.然而 Java ...

  3. java 泛型中 T 和 问号(通配符)的区别

    类型本来有:简单类型和复杂类型,引入泛型后把复杂类型分的更细了: 现在List<Object>, List<String>是两种不同的类型;且无继承关系: 泛型的好处如: 开始 ...

  4. Java泛型的一点用法(转)

    1.一个优秀的泛型,建议不要这样写public static <K, V> Map<K, V> getMap(String source, String firstSplit, ...

  5. java中switch的用法以及判断的类型有哪些(String\byte\short\int\char\枚举类型)

    switch关键字对于多数java学习者来说并不陌生,由于笔试和面试经常会问到它的用法,这里做了一个简单的总结: 能用于switch判断的类型有:byte.short.int.char(JDK1.6) ...

  6. Java 泛型 通配符类型

    Java 泛型 通配符类型 @author ixenos 摘要:限定通配符类型.无限定通配符类型.与普通泛型区别.通配符捕获 通配符类型 通配符的子类型限定(?都是儿孙) <? extends ...

  7. Java深度历险(五)——Java泛型

      作者 成富 发布于 2011年3月3日 | 注意:QCon全球软件开发大会(北京)2016年4月21-23日,了解更多详情!17 讨论 分享到:微博微信FacebookTwitter有道云笔记邮件 ...

  8. java 深度探险 java 泛型

    Java泛型(generics)是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter).声明的类型参数在使用时用具体的类型来替换.泛型最主要的应用是在JD ...

  9. Java泛型总结——吃透泛型开发

    什么是泛型 泛型是jdk5引入的类型机制,就是将类型参数化,它是早在1999年就制定的jsr14的实现. 泛型机制将类型转换时的类型检查从运行时提前到了编译时,使用泛型编写的代码比杂乱的使用objec ...

随机推荐

  1. 前端之bootstrap

    一.响应式介绍 众所周知,电脑.平板.手机的屏幕是差距很大的,假如在电脑上写好了一个页面,在电脑上看起来不错,但是如果放到手机上的话,那可能就会乱的一塌糊涂,这时候怎么解决呢?以前,可以再专门为手机定 ...

  2. (转)rvm安装与常用命令

    rvm是一个命令行工具,可以提供一个便捷的多版本ruby环境的管理和切换. https://rvm.io/ 如果你打算学习ruby/rails, rvm是必不可少的工具之一. 这里所有的命令都是再用户 ...

  3. 关于stm32优先级大小的理解

    转载自:https://www.cnblogs.com/ZKeJun/p/6112591.html 一. 组别:0>1>2>3>4   组别优先顺序(第0组优先级最强,第4组优 ...

  4. TCP缓冲区大小及限制

    这个问题在前面有的部分已经涉及,这里在重新总结下.主要参考UNIX网络编程. (1)数据报大小IPv4的数据报最大大小是65535字节,包括IPv4首部.因为首部中说明大小的字段为16位.IPv6的数 ...

  5. 栈的push、pop序列 【微软面试100题 第二十九题】

    题目要求: 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序.假设压入栈的所有数字均不相等.例如序列1.2.3.4.5是某栈的压栈序列,序列4.5.3.2.1是该压栈 ...

  6. python - 函数的相互调用 及 变量的作用域

    # -*- coding:utf-8 -*- '''@project: jiaxy@author: Jimmy@file: study_函数的相互调用及变量的作用域.py@ide: PyCharm C ...

  7. python中用exit退出程序

    在python中运行一段代码,如果在某处已经完成整次任务,可以用exit退出整个运行.并且还可以在exit()的括号里加入自己退出程序打印说明.不过注意在py3中要加单引号或双引号哦!

  8. POJ 2353 Ministry

    Ministry Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 4220   Accepted: 1348   Specia ...

  9. (转)关于Jackson2.x中com.fasterxml.jackson包的用法

    Jackson应该是目前最好的json解析工具了,之前一直用的是org.codehaus.jackson包中的工具,使用的 包是jackson-all-1.9.11.jar. 最近发现Jackson升 ...

  10. IBM QMF下载

    官网下载页面: http://www-01.ibm.com/support/docview.wss?uid=swg27009383 官方BBS: https://w3-connections.ibm. ...