前言

Java中的泛型支持是从JDK1.5开始的,目的是 让集合能够记住其元素的数据类型 。在没有泛型之前,将元素放在集合中,此元素会被当成Object类型来处理,但是用泛型可以让集合记住元素的数据类型(当然,此时集合只能存放同种类型的元素)。

初识泛型

一个使用泛型的例子如下:

@Test
public void test11(){
    //①不使用泛型时,集合中的元素都被当成Object类型的元素
    ArrayList listOne = new ArrayList();
    listOne.add(1);
    //必须将Object类型的元素强转为Integer,编译才能通过
    Integer a=(Integer)listOne.get(0);
    //②使用泛型时,集合可以记住元素的类型
    ArrayList<Integer> listTwo = new ArrayList<>();
    listTwo.add(1);
    //此时不需要进行强制类型转换,应为取出元素时,可以记住其元素类型:Integer
    Integer b=listTwo.get(0);
}

Java7支持的泛型菱形语法

上一节的例子当中使用泛型时,调用构造器时给出了完整的泛型信息,其实我们声明变量便已经给了泛型信息了,此做法显得多余,所以构造器后的泛型信息可以省略。如:List<Integer> a=new ArrayList<>(); ,这样的语法中,省略了构造器后的菱形中的泛型信息,此所谓泛型的菱形语法(仅支持Java7以上的版本)。

泛型接口、泛型类和泛型方法

泛型就是在定义此接口、类和方法的时候使用了类型形参,这个类型形参将在声明变量,创建对象,调用方法时动态的指定(类似给函数形参赋形参的过程),类型形参能够在整个接口、类体、方法体内当成类型使用。一个简单的例子如下:

@Test
public void test12(){
    class Test<E>{
        public void say(E e){
            System.out.println("类型实参的类型为:"+e.getClass());
        }
        public ArrayList<E> get(){
            ArrayList<E> list = new ArrayList<>();
            return list;
        }
    }
    //传入的泛型实参为Object,所以E为Object,在泛型类内都可以使用此泛型参数
    Test<Object> testOne = new Test<>();
    //E为Object,所以sya()方法的形参e能够是任何类型的值(都是Object的子类)
    testOne.say(1);
    //E为Object,所以啥都能添加,编译能通过
    testOne.get().add("Hiahia~");
}

泛型类派生子类

  1. 可以派生泛型接口和泛型类,也可以实现泛型接口,但是不能出现这样的情况:class A extends B<E> ,其实也很好理解,此时E根本不能确定是什么类型,当然是错误的;但是可以出现这种情况:class A<E> extends B<E> ,此时当定义A类对象时,将根据传入的类型实参确定父类B的类型实参,所以两者的E都能够确定,所以编译能够通过。
  2. 如果派生子类时, 不指定父类(或接口)的类型实参,那么父类的类型实参将被当成Object来处理。
  3. List不是List的子类,两者是同一种类型。但是String[]类型的数组是Object[]类型数组的子类。
  4. instanceof运算符后不能使用泛型类。
  5. 类型通配符

    初始类型通配符

    类型通配符是一个问号: ? ,它可以匹配任何类型,此所谓类型统配。一个使用类型通配符的泛型例子如下:

    @Test
    public void test13(){
        //类型通配符何以接受任何类型,List<?>是任何泛型List的父类。
        List<?> a;
        ArrayList<String> list = new ArrayList<>();
        list.add("曾经沧海难为水,");
        list.add("除却巫山不是云。");
        //因为List<?>是任何泛型List的父类。所以此语句正确。
        a=list;
        //此时a的所有元素是未知类型(但可以肯定是Object类型),需要强转
        String one=(String)a.get(0);
        String two=(String)a.get(1);
        //由于List<?>使用了通配符,不能确定其类型形参,所以不能向此List添加元素
        //下面的语句是错误的,不能添加
        //a.add(new Object());
    }
    

    类型通配符的上限

    继续上节的例子,如果不想让List是所有泛型List的父类,而只想让它是某写泛型List的父类,那么就可以使用通配符的上限。一个例子如下:

    @Test
    public void test13(){
        //此时使用的受限的类型通配符,此时?只能匹配Object类型的子类型
        List<? extends Object> a;
        ArrayList<String> list = new ArrayList<>();
        list.add("曾经沧海难为水,");
        list.add("除却巫山不是云。");
        //String是Object的子类型,所以此语句正确
        a=list;
        //此时a的所有元素都是未知的(但是可以肯定是上限:Object类型),需要强转
        String one=(String)a.get(0);
        String two=(String)a.get(1);
        //同样此时只能知道类型形参是Object的子类,不能确定具体,所以不能往里面添加元素
        //下面的语句是错误的,不能添加
        //a.add(new String("沧海"));
    }
    

    此外,也可以在定义类和接口时使用类型形参上限,表示传给该类/接口的类型实参必须时该上限的子类(或者就是该上限类型),比通配符的上限多了一个地方: 可以为类型形参设定多个上限,最多一个父类上限,多个接口上限。表明该类型形参必须是其父类的子类(本身类也行),并且实现多个上限接口 。例如这种写法: public class A<T extends B & java.io.Serializable> ,表示T必须是B类型或者B的子类类型,并且实现了Serializable接口。 此外,类上限必须是第一位,接口上限在后面。

    类型通配符的下限

    存在类型通配符的上限,也存在类型通配符的下限。 表示匹配下限本身类型或者下限的父类类型。 只需将上限的关键字extends换成super即可,此外, 没有类型形参的上限。

    泛型方法

    泛型方法就是定义一个方法时,可以定义一个或者多个类型形参。泛型方法的语法格式如下:

    修饰符 <T,S> 方法返回值 方法名(形参列表){
        //方法体
    }
    

    可以看到,类型形参的位置是紧接着方法返回值的前面。 与类或者接口的类型形参不同的是,方法类型形参不用显示的传入类型实参,而是系统根据形参的类型推到出类型形参的具体类型。 一个泛型方法的例子如下:

    public static void main(String[] a){
        ArrayList<String> list = new ArrayList<>();
        list.add("我是0");
        list.add("我是1");
        ArrayList<Object> des = new ArrayList<>();
        //泛型推导R为String,所以无需强转
        String last=A.add(des,list);
        //打印:我是1
        System.out.println(last);
        //打印:[我是0, 我是1]
        System.out.println(des);
    }
    

    Notice: 如果定义泛型方法时,可以使用多个类型形参,但是它们有依赖关系,则可以同时使用使用类型通配符来代表他们的依赖关系,从而减少类型形参的个数。例如上面的例子中,类型形参其实泛型方法可以这样写: public static <R,T extends R> R add(Collection<R> des,Collection<T> src) ,但是这样无端多了一个类型形参,并且效果和使用类型通配符是一样的。

    擦除和转换

    1. 如果没有为泛型类指定类型实参,那么类型实参将会是声明类时指定的第一个上限类型(没有使用上限则为Object)。
    2. 当把一个具有泛型信息的对象赋值给一个没有泛型信息的变量时,将会丢失泛型信息。 此所谓泛型擦除。
    3. 当把一个被泛型擦除后的对象再次赋值给一个具有泛型信息的变量时(此变量的泛型信息必须正确),该对象又会获取被擦除的泛型信息。

    一个综合例子如下:

    @Test
    public void test15(){
        ArrayList<String> list = new ArrayList<>();
        list.add("测试");
        //泛型擦除
        List a=list;
        //泛型擦除后丢失了泛型信息,所以需要强转
        String v=(String)a.get(0);
        //将被擦除泛型信息的List再次转换为泛型List
        List<String> newList=list;
        //再次获取泛型信息,所以无需强转
        String v1=newList.get(0);
    }
    

    泛型和数组

    1. 创建泛型数组时,只能声明带泛型的数组,不能创建泛型数组。即只能: ArrayList<String> a=new ArrayList[5];
    2. 可以创建没有上限的类型通配符泛型数组,例如: List<?>[] a=new ArrayList<?>[3];

    一个综合的例子如下:

    @Test
    public void test16(){
        //语法:List<String>[] a=new ArrayList<String>[5]; 是错误的
        List<String>[] a=new ArrayList[5];
        a[0]=new ArrayList<>();
        a[0].add("giao!!");
    
        List<?>[] b=new ArrayList<?>[5];
        List<String>[] c=(List<String>[])b;
        ArrayList<String> list = new ArrayList<>();
        list.add("测试2");
        c[0]=list;
        //无法确定类型形参,但是知道至少是Object
        Object v=b[0].get(0);
    }