• 一、泛型简介
    • 1.引入泛型的目的
    • 2.泛型初探
    • 3.泛型的好处
  • 二、泛型的使用
    • 1.泛型类和泛型接口
    • 2.泛型的方法
    • 3.泛型构造器
  • 三、类型通配符
    • 带限通配符
      • 1.上限通配符
      • 2.下限通配符
  • 四、类型擦除

    一、泛型简介

    1.引入泛型的目的

    了解引入泛型的动机,就先从语法糖开始了解。

    语法糖

    语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家Peter.J.Landin发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。Java中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。虚拟机并不支持这些语法,它们在编译阶段就被还原回了简单的基础语法结构,这个过程成为解语法糖。

    泛型的目的: Java 泛型就是把一种语法糖,通过泛型使得在编译阶段完成一些类型转换的工作,避免在运行时强制类型转换而出现ClassCastException,即类型转换异常。

    2.泛型初探

    JDK 1.5 时才增加了泛型,并在很大程度上都是方便集合的使用,使其能够记住其元素的数据类型。

    在泛型(Generic type或Generics)出现之前,是这么写代码的:

    1. public static void main(String[] args)
    2. {
    3. List list = new ArrayList();
    4. list.add("123");
    5. list.add("456");
    6. System.out.println((String)list.get(0));
    7. }

    当然这是完全允许的,因为List里面的内容是Object类型的,自然任何对象类型都可以放入、都可以取出,但是这么写会有两个问题:

    1、当一个对象放入集合时,集合不会记住此对象的类型,当再次从集合中取出此对象时,该对象的编译类型变成了Object。

    2、运行时需要人为地强制转换类型到具体目标,实际的程序绝不会这么简单,一个不小心就会出现java.lang.ClassCastException。

    所以,泛型出现之后,上面的代码就改成了大家都熟知的写法:

    1. public static void main(String[] args)
    2. {
    3. List<String>
    4. list = new ArrayList<String>();
    5. list.add("123");
    6. list.add("456");
    7. System.out.println(list.get(0));
    8. }

    这就是泛型。泛型是对Java语言类型系统的一种扩展,有点类似于C++的模板,可以把类型参数看作是使用参数化类型时指定的类型的一个占位符。引入泛型,是对Java语言一个较大的功能增强,带来了很多的好处。

    3.泛型的好处

    ①类型安全。类型错误现在在编译期间就被捕获到了,而不是在运行时当作java.lang.ClassCastException展示出来,将类型检查从运行时挪到编译时有助于开发者更容易找到错误,并提高程序的可靠性。

    ②消除了代码中许多的强制类型转换,增强了代码的可读性。

    ③为较大的优化带来了可能。

    二、泛型的使用

    1.泛型类和泛型接口

    下面是JDK 1.5 以后,List接口,以及ArrayList类的代码片段。

    1. //定义接口时指定了一个类型形参,该形参名为E
    2. public interface List<E> extends Collection<E> {
    3. //在该接口里,E可以作为类型使用
    4. public E get(int index) {}
    5. public void add(E e) {}
    6. }
    7. //定义类时指定了一个类型形参,该形参名为E
    8. public class ArrayList<E> extends AbstractList<E> implements List<E> {
    9. //在该类里,E可以作为类型使用
    10. public void set(E e) {
    11. .......................
    12. }
    13. }

    这就是泛型的实质:允许在定义接口、类时声明类型形参,类型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通类型的地方都可以使用这种类型形参。

    下面具体讲解泛型类的使用。泛型接口的使用与泛型类几乎相同,可以比对自行学习。

    泛型类

    定义一个容器类,存放键值对key-value,键值对的类型不确定,可以使用泛型来定义,分别指定为K和V。

    1. public class Container<K, V> {
    2. private K key;
    3. private V value;
    4. public Container(K k, V v) {
    5. key = k;
    6. value = v;
    7. }
    8. public K getkey() {
    9. return key;
    10. }
    11. public V getValue() {
    12. return value;
    13. }
    14. public void setKey() {
    15. this.key = key;
    16. }
    17. public void setValue() {
    18. this.value = value;
    19. }
    20. }

    在使用Container类时,只需要指定K,V的具体类型即可,从而创建出逻辑上不同的Container实例,用来存放不同的数据类型。

    1. public static void main(String[] args) {
    2. Container<String,String> c1=new Container<String ,String>("name","hello");
    3. Container<String,Integer> c2=new Container<String,Integer>("age",22);
    4. Container<Double,Double> c3=new Container<Double,Double>(1.1,1.3);
    5. System.out.println(c1.getKey() + " : " + c1.getValue());
    6. System.out.println(c2.getKey() + " : " + c2.getValue());
    7. System.out.println(c3.getKey() + " : " + c3.getValue());
    8. }

    在JDK 1.7 增加了泛型的“菱形”语法:Java允许在构造器后不需要带完成的泛型信息,只要给出一对尖括号(<>)即可,Java可以推断尖括号里应该是什么泛型信息。
    如下所示:

    1. Container<String,String> c1=new Container<>("name","hello");
    2. Container<String,Integer> c2=new Container<>("age",22);

    泛型类派生子类

    当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或者从该父类派生子类,需要注意:使用这些接口、父类派生子类时不能再包含类型形参,需要传入具体的类型。
    错误的方式:

    1. public class A extends Container<K, V>{}

    正确的方式:

    1. public class A extends Container<Integer, String>{}

    也可以不指定具体的类型,如下:

    1. public class A extends Container{}

    此时系统会把K,V形参当成Object类型处理。

    2.泛型的方法

    前面在介绍泛型类和泛型接口中提到,可以在泛型类、泛型接口的方法中,把泛型中声明的类型形参当成普通类型使用。 如下面的方式:

    1. public class Container<K, V>
    2. {
    3. ........................
    4. public K getkey() {
    5. return key;
    6. }
    7. public void setKey() {
    8. this.key = key;
    9. }
    10. ....................
    11. }

    但在另外一些情况下,在类、接口中没有使用泛型时,定义方法时想定义类型形参,就会使用泛型方法。如下方式:

    1. public class Main{
    2. public static <T> void out(T t){
    3. System.out.println(t);
    4. }
    5. public static void main(String[] args){
    6. out("hansheng");
    7. out(123);
    8. }
    9. }

    所谓泛型方法,就是在声明方法时定义一个或多个类型形参。 泛型方法的用法格式如下:

    1. 修饰符<T, S> 返回值类型 方法名(形参列表){
    2. 方法体

    注意: 方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。

    1. class Demo{
    2. public <T> T fun(T t){ // 可以接收任意类型的数据
    3. return t ; // 直接把参数返回
    4. }
    5. };
    6. public class GenericsDemo26{
    7. public static void main(String args[]){
    8. Demo d = new Demo() ; // 实例化Demo对象
    9. String str = d.fun("汤姆") ; // 传递字符串
    10. int i = d.fun(30) ; // 传递数字,自动装箱
    11. System.out.println(str) ; // 输出内容
    12. System.out.println(i) ; // 输出内容
    13. }
    14. };

    当调用fun()方法时,根据传入的实际对象,编译器就会判断出类型形参T所代表的实际类型。

    3.泛型构造器

    正如泛型方法允许在方法签名中声明类型形参一样,Java也允许在构造器签名中声明类型形参,这样就产生了所谓的泛型构造器。
    和使用普通泛型方法一样没区别,一种是显式指定泛型参数,另一种是隐式推断,如果是显式指定则以显式指定的类型参数为准,如果传入的参数的类型和指定的类型实参不符,将会编译报错。

    1. public class Person {
    2. public <T> Person(T t) {
    3. System.out.println(t);
    4. }
    5. }
    1. public static void main(String[] args){
    2. //隐式
    3. new Person(22);
    4. //显示
    5. new<String> Person("hello");
    6. }

    这里唯一需要特殊注明的就是,如果构造器是泛型构造器,同时该类也是一个泛型类的情况下应该如何使用泛型构造器:
    因为泛型构造器可以显式指定自己的类型参数(需要用到菱形,放在构造器之前),而泛型类自己的类型实参也需要指定(菱形放在构造器之后),这就同时出现了两个菱形了,这就会有一些小问题,具体用法再这里总结一下。
    以下面这个例子为代表

    1. public class Person<E> {
    2. public <T> Person(T t) {
    3. System.out.println(t);
    4. }
    5. }

    这种用法:Person<String> a = new <Integer>Person<>(15);这种语法不允许,会直接编译报错!

    三、类型通配符

    顾名思义就是匹配任意类型的类型实参。

    类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作:List<?>(意思是元素类型未知的List)。这个问号(?)被成为通配符,它的元素类型可以匹配任何类型。

    1. public void test(List<?> c){
    2. for(int i =0;i<c.size();i++){
    3. System.out.println(c.get(i));
    4. }
    5. }

    现在可以传入任何类型的List来调用test()方法,程序依然可以访问集合c中的元素,其类型是Object。

    1. List<?> c = new ArrayList<String>();
    2. //编译器报错
    3. c.add(new Object());

    但是并不能把元素加入到其中。因为程序无法确定c集合中元素的类型,所以不能向其添加对象。
    下面就该引入带限通配符,来确定集合元素中的类型。

    带限通配符

    简单来讲,使用通配符的目的是来限制泛型的类型参数的类型,使其满足某种条件,固定为某些类。

    主要分为两类即:上限通配符下限通配符

    1.上限通配符

    如果想限制使用泛型类别时,只能用某个特定类型或者是其子类型才能实例化该类型时,可以在定义类型时,使用extends关键字指定这个类型必须是继承某个类,或者实现某个接口,也可以是这个类或接口本身。

    1. 它表示集合中的所有元素都是Shape类型或者其子类
    2. List<? extends Shape>

    这就是所谓的上限通配符,使用关键字extends来实现,实例化时,指定类型实参只能是extends后类型的子类或其本身。
    例如:

    1. //Circle是其子类
    2. List<? extends Shape> list = new ArrayList<Circle>();

    这样就确定集合中元素的类型,虽然不确定具体的类型,但最起码知道其父类。然后进行其他操作。

    2.下限通配符

    如果想限制使用泛型类别时,只能用某个特定类型或者是其父类型才能实例化该类型时,可以在定义类型时,使用super关键字指定这个类型必须是是某个类的父类,或者是某个接口的父接口,也可以是这个类或接口本身。

    1. 它表示集合中的所有元素都是Circle类型或者其父类
    2. List <? super Circle>

    这就是所谓的下限通配符,使用关键字super来实现,实例化时,指定类型实参只能是extends后类型的子类或其本身。
    例如:

    1. //Shape是其父类
    2. List<? super Circle> list = new ArrayList<Shape>();

    四、类型擦除

    1. Class c1=new ArrayList<Integer>().getClass();
    2. Class c2=new ArrayList<String>().getClass();
    3. System.out.println(c1==c2);

    程序输出:

    true。

    这是因为不管为泛型的类型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一类处理,在内存中也只占用一块内存空间。从Java泛型这一概念提出的目的来看,其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。

    在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类。