泛型是一种编程范式(Programming Paradigm,是为了效率和重用性产生的。由Alexander Stepanov(C++标准库主要设计师)和David Musser(伦斯勒理工学院CS名誉教授)首次提出,自实现始,就成为了ANSI/ISO C++重要标准之一。

Java自1.5版本开始提供泛型,其本质是一个参数化的类型,那么,何谓参数化?

参数是一个外部变量。设想一个方法,其参数的名称和实际的数值是外部传入的,那么,该参数的类型是否也作为一个参数,在运行时决定呢?这就是泛型的作用。参考如下代码:

List<String> list = new ArrayList<String>();

list.add(1);

在第2行,会抛出编译期错误。

The method add(int, String) in the type List<String> is not applicable for the arguments (int)

这就是因为,list在声明时定义了String为自己需要的类型,而1是一个整型数。在上面的例子中,以下几种添加方式都是合法的:

list.add("字符串");

list.add(new String());

String str="字符串";

list.add( str);

在J2SE1.5之前的版本中,Java没有办法显式的对容器进行编译期内容限制,在没有注释或者文档说明的情况下,很容易出现运行时错误。下面举一个错误的例子:

ArrayList list = new ArrayList();

list.add(0);

list.add(1);

list.add('2');

list.add(3);

//输出list内容

System.out.println(list);

//遍历输出list内容

for (int i = 0, len = list.size(); i < len; i++) {

Integer object = (Integer) list.get(i);

System.out.println(object);

}

输出结果如下所示:

[0, 1, 2, 3]

0

1

Exception in thread "main" java.lang.ClassCastException: java.lang.Character cannot be cast to java.lang.Integer

at capter3.generic.Generic3_1.test2(Generic3_1.java:28)

at capter3.generic.Generic3_1.main(Generic3_1.java:17)

可以看到,直接输出list的时候,int类型的1和char类型的2是看不出区别的,假设忽略了这点,直接转为Integer来使用的时候,就抛出了强制转化异常。

除了会造成异常外,还可以思考一个问题,如果没有泛型,那么list.get(int)方法返回的始终是个Object,那么要如何在运行时之外确定它的类型呢?

综上,已经证明了泛型存在的必要性,它提供了以下能力:

1、避免代码中的强制类型转换;

2、限定类型,在编译时提供一个额外的类型检查,避免错误的值被存入容器;

3、实现一些特别编程技巧。比如:提供一个方法用于拷贝对象,在不提供额外方法参数的情况下,使返回值类型和方法参数类型保持一致。

泛型的分类

根据泛型使用方式的不同,可分为泛型接口、泛型类和泛型方法。它们的定义如下:

1、泛型接口:在接口定义的接口名后加上<泛型参数名>,就定义了一个泛型接口,该泛型参数名的作用域存在于接口定义和整个接口主体之内;

2、泛型类:在类定义的类名后加上<泛型参数名>,就定义了一个泛型类,该泛型参数名的作用域存在于类定义和整个类主体之内;

3、方法类:在方法的返回值之前加上<泛型参数名>,就定义了一个泛型方法,该泛型参数名的作用域包括方法返回值,方法参数,方法异常,以及整个方法主体。

下面通过一个例子来分别介绍这几种泛型的定义方法,示例代码如下:

/**

* 在普通的接口后加上<泛型参数名>即可以定义泛型接口

*/

interface GenericInterface<T> {

}

/**

* 在类定义后加上<泛型参数名>即可定义一个泛型类,注意后面这个GenericInterface<T>,这里是使用类的泛型参数,而非定义。

*/

class GenericClass<T> implements GenericInterface<T>{

/**

* 在返回值前定义了泛型参数的方法,就是泛型方法。

*/

public <K, E extends Exception> K genericMethod(K param) throws E {

java.util.List<K> list = new ArrayList<K>();

K k = null;

return null;

}

}

在上例中,class GenericClass<T> implements GenericInterface<T>中有两个地方使用了<T>,它们是同一个概念吗?为了回答这个问题,下面给出几个基本概念,通过对这些基本概念的掌握,将可以解决大部分类似的泛型问题。

a、类(接口)的泛型定义位置紧接在类(接口)定义之后,可以替代该类(接口)定义内部的任意类型。在该类(接口)被声明时,确定泛型参数。

b、方法的泛型定义位置在修饰符之后返回值之前,可以替代该方法中使用的任意类型,包括返回值、参数以及局部变量。在该方法被调用时,确定泛型参数,一般来说,是通过方法参数来确定的泛型参数。

c、<>的出现有两种情况,一是定义泛型,二是使用某个类\接口来具象化泛型。

根据上面介绍的几个基本概念,再来分析class GenericClass<T> implemenets GenericInterface<T>这句代码。可知,class GenericClass是类的定义,那么第一个<T>就构成了泛型参数的定义,而接口GenericInterface是定义在别处的,该代码位置是对此接口的引用,所以,第二个<T>则是使用泛型T来规范GenericInterface。

引申:

如果泛型方法是没有形参的,那么是否还有其它方法来指定类型参数?

答案:有方法指定,但是这个语法并不常见,实现代码如下:

GenericClass<String> gc=new GenericClass<String>();

gc.<String>genericMethod(null);

可以看到这里出现一个很特别的代码形式,gc.genericMethod(null)中间多出了一个<String>,这就是为genericMethod方法进行泛型参数定义了。

有界泛型的定义

有界泛型有三个非常重要的关键字:?,extends和 super。

a) “?”,表示通配符类型,用于表达任意类型,需要注意的是,它指代的是“某一个任意类型”,但并不是Object;(注意,这里并不是准确的表达,具体的内容将在“泛型的不变性”相关小节来讨论)

示例代码如下:

class Parent {

}

class Sub1 extends Parent {

}

class Sub2 extends Parent {

}

class WildcardSample<T> {

T obj;

void test() {

WildcardSample<Parent> sample1 = new WildcardSample<Parent>();

//编译错误

WildcardSample<Parent> sample2 = new WildcardSample<Sub1>();

//正常编译

WildcardSample<?> sample3 = new WildcardSample<Parent>();

WildcardSample<?> sample4 = new WildcardSample<Sub1>();

WildcardSample<?> sample5 = new WildcardSample<Sub2>();

sample1.obj = new Sub1();

// 编译错误

sample3.obj = new Sub1();

}

}

这些代码体现了通配符的作用。

1、sample2声明里使用Parent作为泛型参数的时候,不能指向使用Sub1作为泛型参数的实例。因为编译器处理泛型时严格的按照定义来执行,Sub1虽然是Parent的子类,但它毕竟不是Parent。

2、sample3~5声明里使用?作为泛型参数的时候,可以指向任意WildcardSample实例。

3、sample1.obj可以指向Sub1实例,这是因为obj被认为是Parent,而Sub1是Parent的子类,满足向上转型。

4、sample3.obj不能指向Sub1实例,这是因为通配符是“某个类型”而并不是Object,所以Sub1并不是?的子类,抛出编译期错误。

5、虽然有如此多的限制,但是你还是可以以Object类型来读取sample3.obj,毕竟不论通配符是什么类型,Object一定是它的父类。

引申:设想如果sample3.obj = new Sub1()可以编译通过,事实上期望的sample3类型是WildcardSample<Object>,这样的话,通配符就失去意义了。而在实际应用中,这并不光是失去意义这样简单的事,还会引起执行异常。这里提供一个例子帮助理解:

WildcardSample<Parent> sample1 = new WildcardSample<Parent>();

sample1.obj = new Parent();

WildcardSample<?> extSample = sample1;

//原本应当被限定为Parent类型,这里使用了String类型,必须抛出异常。

extSample.obj = new String();

b) extends在泛型里不是继承,而是定义上界的意思,如T extends UpperBound,UpperBound为泛型T的上界,也就是说T必须为UpperBound或者它的子类;

泛型上界可以用于定义以及声明代码处,不同的位置使用的时候,它的作用于使用方法都有所不同,示例代码如下:

/**

* 有上界的泛型类

*/

class ExtendSample<T extends Parent> {

T obj;

/**

* 有上界的泛型方法

*/

<K extends Sub1> T extendMethod(K param) {

return this.obj;

}

}

public class Generic3_1_2_b {

public static void main(String[] args) {

ExtendSample<Parent> sample1 = new ExtendSample<Parent>();

ExtendSample<Sub1> sample2 = new ExtendSample<Sub1>();

ExtendSample<? extends Parent> sample3 = new ExtendSample<Sub1>();

ExtendSample<? extends Sub1> sample4;

// 编译错误

sample4 = new ExtendSample<Sub2>();

// 编译错误

ExtendSample<? extends Number> sample5;

sample1.obj = new Sub1();

// 编译错误

sample3.obj = new Parent();

}

}

这个例子中使用了一个具备上界的泛型方法和一个具备上界的泛型类,它们体现了extends在泛型中的应用:

1、在方法\接口\类的泛型定义时,需要使用泛型参数名(比如T或者K)。

2、在声明位置使用泛型参数时,需要使用通配符,意义是“用来指定类的上界(该类或其子类)”。

就算加上了上界,使用通配符来定义的对象,也是只能读,不能写。理由在通配符相关小节已经论证过,不再赘述。

c) super关键字用于定义泛型的下界。如T super LowerBound,则LowerBound为泛型T的下界,也就是说T必须为LowerBound或者它的父类;

泛型下界只能应用于声明代码处,表示泛型参数一定是指定类或其父类。

参考以下代码:

class SuperSample<T> {

T obj;

}

public class Generic3_1_2_c {

public static void main(String[] args) {

SuperSample<? super Parent> sample1 = new SuperSample<Parent>();

// 编译错误

SuperSample<? super Parent> sample2 = new SuperSample<Sub1>();

SuperSample<? super Sub1> sample3 = new SuperSample<Parent>();

sample1.obj = new Sub1();

sample1.obj = new Sub2();

sample1.obj = new Parent();

sample3.obj = new Sub1();

// 编译错误

sample3.obj = new Sub2();

// 编译错误

sample3.obj = new Parent();

}

}

在该示例中,可以注意到:

1、sample1.obj一定是Parent或者Parent的父类,那么,Sub1\Sub2\Parent都能满足向上转型。

2、sample3.obj一定是Sub1或者Sub1的父类,Parent和Sub2无法完全满足条件,所以抛出了异常。

引申:思考一个问题,在上面的例子里sample1.obj是什么类型?

答案: ? extends Parent,也就是说,没有类型。

通过对上述现象的分析可知:当使用extends上界时,所有以该泛型参数作为形参的方法,都不可用,当使用super下界时,所有以该泛型参数作为返回值的方法,只能以Object类型来引用。

思考:<? extends T>和<? super T>有哪些区别

定义复杂的泛型

复杂的泛型也是由简单的泛型组合起来的,需要掌握下面几个概念:

1、多个泛型参数定义由逗号隔开,就像<T,K>这样。

2、同一个泛型参数如果有多个上界,则各个上界之间用&符号连接。

3、多个上界类型里最多只能有一个类,其他必须为接口,如果上界里有类,则必须放置在第一位。

结合以上的知识,则可以灵活的组合出复杂的泛型声明来。参考以下代码:

class A {

}

class B extends A {

}

class C extends B {

}

/**

* 这是一个泛型类

*/

class ComplexGeneric<T extends A, K extends B & Serializable & Cloneable>  {...}

通过上面代码可以看出,ComplextGeneric 类具备两个泛型参数<T,K>,其中T具备上界A,换言之,T一定是A或者其子类;K具备三个上界,分别为类B,接口 Serializable和Cloneable,换言之,K一定是B或者其子类,并且实现了Serializable和Cloneable。

复杂的泛型为更规范更精确的设计提供了可能性。

引申:前面说过,在运行时,泛型会被处理为上界类型。也就是说,ComplextGeneric在其内部用到泛型T的时候,反射会把它当成A类来处理(需要注意的是,在字节码里,还是当作Object处理),那么,反射用到泛型K的时候呢?答案是,会把它当成上界定义的第一个上界处理,在当前例子是,也就是B这个类。

知道了这个有什么意义呢?

设想一个方法 <T extends A> void method(T t);

如果需要反射获取它,必须同时知道方法名和参数类型。这时候,使用Object是找不到它的,只能通过A类来获取。

《徐徐道来话Java》(1):泛型的基本概念的更多相关文章

  1. 《徐徐道来话Java》(2):泛型和数组,以及Java是如何实现泛型的

    数组和泛型容器有什么区别 要区分数组和泛型容器的功能,这里先要理解三个概念:协变性(covariance).逆变性(contravariance)和无关性(invariant). 若类A是类B的子类, ...

  2. 《徐徐道来话Java》:PriorityQueue和最小堆

    在讲解PriorityQueue之前,需要先熟悉一个有序数据结构:最小堆. 最小堆是一种经过排序的完全二叉树,其中任一非终端节点数值均不大于其左孩子和右孩子节点的值. 可以得出结论,如果一棵二叉树满足 ...

  3. [转] Java 的泛型擦除和运行时泛型信息获取

    原文链接 https://my.oschina.net/lifany/blog/875769 前言 现在很多程序员都会在简历中写上精通 Java.但究竟怎样才算是精通 Java 呢?我觉得不仅要熟练掌 ...

  4. Java“禁止”泛型数组

    Java“禁止”泛型数组 原文:https://blog.csdn.net/yi_Afly/article/details/52058708 1. 泛型定义泛型编程是一种通过参数化的方式将数据处理与数 ...

  5. C++ Java C#泛型

    泛型概述C#中的泛型C#泛型和java泛型的比较C#泛型和C++模板的比较C#泛型中的约束 泛型概述 Bruce Eckel :您能对泛型做一个快速的介绍么? Anders Hejlsberg : 泛 ...

  6. 100多道经典的JAVA面试题及答案解析

    面向对象编程(OOP) Java是一个支持并发.基于类和面向对象的计算机编程语言.下面列出了面向对象软件开发的优点: 代码开发模块化,更易维护和修改. 代码复用. 增强代码的可靠性和灵活性. 增加代码 ...

  7. Java中泛型的详细解析,深入分析泛型的使用方式

    泛型的基本概念 泛型: 参数化类型 参数: 定义方法时有形参 调用方法时传递实参 参数化类型: 将类型由原来的具体的类型参数化,类似方法中的变量参数 类型定义成参数形式, 可以称为类型形参 在使用或者 ...

  8. Java中的泛型 (上) - 基本概念和原理

    本节我们主要来介绍泛型的基本概念和原理 后续章节我们会介绍各种容器类,容器类可以说是日常程序开发中天天用到的,没有容器类,难以想象能开发什么真正有用的程序.而容器类是基于泛型的,不理解泛型,我们就难以 ...

  9. Java中泛型在集合框架中的应用

    泛型是Java中的一个重要概念,上一篇文章我们说过,当元素存入集合时,集合会将元素转换为Object类型存储,当取出时也是按照Object取出的,所以用get方法取出时,我们会进行强制类型转换,并且通 ...

随机推荐

  1. jquery.uploadify文件上传组件

    1.jquery.uploadify简介 在ASP.NET中上传的控件有很多,比如.NET自带的FileUpload,以及SWFUpload,Uploadify等等,尤其后面两个控件的用户体验比较好, ...

  2. 【探索】机器指令翻译成 JavaScript

    前言 前些时候研究脚本混淆时,打算先学一些「程序流程」相关的概念.为了不因太枯燥而放弃,决定想一个有趣的案例,可以边探索边学. 于是想了一个话题:尝试将机器指令 1:1 翻译 成 JavaScript ...

  3. Android 自定义 attr

    好纠结,弄了一个下午老是报错如是总结一下安卓自定视图和自定义属性. (一)自定义属性 在Values文件下建立一个attrs.xml文件,attr的format可以参考:http://www.cnbl ...

  4. 开始学nodejs——net模块

    net模块的组成部分 详见 http://nodejs.cn/api/net.html 下面整理出了整个net模块的知识结构,和各个事件.方法.属性的用法 net.Server类 net.Socket ...

  5. zookeeper源码分析之五服务端(集群leader)处理请求流程

    leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...

  6. 2013 Asia Changsha Regional Contest---Josephina and RPG(DP)

    题目链接 http://acm.hdu.edu.cn/showproblem.php?pid=4800 Problem Description A role-playing game (RPG and ...

  7. Linux杀死进程,查看进程

    http://blog.csdn.net/wojiaopanpan/article/details/7286430/

  8. DB2重启数据库实例

    DB2重启数据库实例时,有时停止实例会失败,此时需要先确认没有应用链接数据库,然后再关闭数据库实例,并重新启动. 1.查看是否有活动的链接 命令:db2 list applications for d ...

  9. 【swift】BlockOperation和GCD实用代码块

    //BlockOperation // // ViewController.swift import UIKit class ViewController: UIViewController { @I ...

  10. 初学DirectX11, 留个纪恋。

    以前学的是openGL, 最近才开始学DirectX11,写了个很垃圾的代码,怀念以前的glPushMatrix(), glPopMatrix(), glBegin(), glEnd(), 多简单啊, ...