10 java泛型

泛型是JDK 5.0中引入的一个新特性。这个特性允许在类和接口的定义中使用类型参数。该类型参数在程序运行时被具体的类型替换。泛型就是类型参数的占位符。我们在学习API的类集框架时就使用过泛型来表示元素的类型。下面简单总结一下泛型的基础知识。

1. 使用泛型

最常见的使用带泛型的集合类:
import java.util.*;

public class Test
{
    public static void main(String[] args)
    {
        ArrayList<Integer> array = new ArrayList<Integer>();
        System.out.println(array.getClass().getName());
    }
}
//运行结果:
//java.util.ArrayList
因为泛型是在Java编译器中实现的,仅仅用作编译时类型检查,在编译得到的字节码中会被擦除。因此运行时的反射命令得到的array类型是ArrayList类而非ArrayList<Integer>类。利用这个特点,我们可以绕过编译时的泛型匹配检查,在运行时给集合类赋予与类型参数不符的元素。这同样要用到反射:
import java.util.*;
import java.lang.reflect.*;

public class Test
{
    public static void main(String[] args)  throws Exception
    {
        ArrayList<Integer> array = new ArrayList<Integer>();
        array.getClass().getMethod("add", Object.class).invoke(array, "test");
        System.out.println(array.get(0));
    }
}
//运行结果
//test
类型擦除不止适用于集合类,所有使用泛型的类,接口,方法和构造器都是一样的道理(泛型作为方法的形参除外)。

2. 泛型的兼容性和继承

类型参数非空的泛型类引用可以指向类型参数为空的泛型类实例,反之亦可。还是拿集合类来举例,比如:
ArrayList<Integer> array = new ArrayList();
ArrayList array2 = new ArrayList<Integer>();
这样的语句编译可以通过,只是会有警告。
但是类型参数同样非空的引用和实例,若要求该引用指向该实例,需先保证两者类型参数严格一致:
//以下语句都报错:不兼容的类型
ArrayList<Integer> array = new ArrayList<String>();
ArrayList<Object> array2 = new ArrayList<Integer>();
也即:类型参数的继承关系与泛型兼容性无关。另外,数组的定义中不能出现泛型:
//编译通过
ArrayList[] array = new ArrayList[3];
//报错:创建泛型数组
ArrayList<Integer>[] array2 = new ArrayList<Integer>[3];
上面就是对泛型兼容性的简单总结。特别要注意的是,这里不涉及泛型通配符。使用通配符时的兼容规则更为复杂。

3. 泛型通配符

3.1 形参的通配符

不考虑泛型的时候,一个方法的形参的类是传入对象的类或者其父类即可。如果不知道传入对象的具体类型,形参可以设置成Object类的对象。但若考虑泛型,则需要考虑前面说到的兼容性问题。如果方法在运行前只知道要传入一个泛型对象,但不知道具体的类型参数,这时由于类型参数没有继承关系,为了保证兼容性,可以使用泛型通配符"?"来表示此处有一个待定的类型参数(也可以不用,后面会提到):
import java.util.*;
import java.lang.reflect.*;

public class Test
{
    public static void main(String[] args)  throws Exception
    {
        show(new ArrayList<Integer>());
    }
    public static void show(ArrayList<?> obj)
    {
        //打印“0”
        System.out.println(obj.size());
        //报错:找不到add(int)方法
        obj.add(1);
        //报错:找不到add(java.lang.Integer)方法
        obj.add(new Integer(1));
        //编译通过
        obj = new ArrayList<String>();
    }
}
上面的例子可以看出,通配符只是表示类型参数的存在,但不能当成任何具体的类型参数来用。如果一个泛型对象是用通配符传入的,则该泛型对象中涉及类型参数的方法都不能使用。不过,不涉及类型参数的方法是正常使用的,比如ArrayList泛型类的不涉及泛型的size()方法。笔者实验得知,如果想正常使用涉及类型参数的方法,一是将该形参指向一个新的泛型对象,如上面例子所示,二是去掉形参的通配符,让其成为原始类型:
import java.util.*;
import java.lang.reflect.*;

public class Test
{
    public static void main(String[] args)  throws Exception
    {
        show(new ArrayList<Integer>());
    }
    //不使用通配符
    public static void show(ArrayList obj)
    {
        //编译通过,但有警告
        obj.add(1);
        //编译通过,但有警告
        obj.add("abc");
    }
}
从上面两个例子看出,在不使用通配符的情况下,泛型形参和泛型实参的关系与同一方法中泛型引用和泛型对象的关系完全一致,类型参数没有继承关系且要兼容。在形参使用通配符的情况下,除了涉及类型参数的方法不能使用外,其它规则与无通配符时一致。

3.2 非形参的通配符

如果一个引用不是形参,那可不可以用通配符呢?答案是可以:
//编译通过
ArrayList<?> array = new ArrayList();
//编译通过
ArrayList<?> array2 = new ArrayList<Integer>();
//报错:意外的类型,等号右边需要不带限制的类或接口
ArrayList<?> array3 = new ArrayList<?>();
上面的例子说明了,生成泛型对象时new语句不能使用通配符,但是泛型引用可以使用,而且可以指向拥有具体类型参数的泛型对象(这一点与形参通配符的情形一致)。

3.3 通配符的上下界

通配符不仅可以表示一个类型参数,还可以表示一定范围内的多个类型参数:
ArrayList<? extends Number> array1 = new ArrayList<Integer>();
ArrayList<? extends Number> array2 = new ArrayList<Number>();
ArrayList<? super Integer> array3 = new ArrayList<Number>();
ArrayList<? super Integer> array4 = new ArrayList<Integer>();
上面四种情况都是可以的。? extends A表示该处类型参数是A类或者A的子类,? super
B表示该处类型参数是B类或者B的父类。前面提到类型参数没有继承关系指的是不用通配符时,类型参数为子类的泛型对象不能赋值给类型参数为父类的泛型引用。这里使用通配符表示类型参数的上下界,界内的泛型对象就可以赋值给泛型引用了,这也是使用通配符时的兼容性与不使用通配符时的兼容性不同的地方。

4. 自定义泛型

我们可以给类,接口和方法加入类型参数,也就是自定义泛型。

4.1 自定义泛型方法

泛型方法的类型参数的占位符要在返回值前的<>里注明,以区别于类的名称:
public class Test
{
    public static void main(String[] args)  throws Exception
    {
        show(1);
        show("abc");
    }
    //<T>表示这是一个泛型方法,T是类型参数的占位符
    public static <T> void show(T obj)
    {
        System.out.println(obj);
    }
}
//运行结果:
//1
//abc
在使用自定义的泛型方法的时候要注意,传入的泛型对象的类型是不确定的,许多操作是不能进行的:
public class Test
{
    public static void main(String[] args)  throws Exception
    {
        //编译方法时报错,所以该句不执行
        //int a = add(2, 3);
        //运行时报错:ClassCastException
        String C = convert("abc", 1);
    }
    //编译时报错:" + "不能用于T, T
    //public static <T> T add(T obj1, T obj2)
    //{
    //    return obj1 + obj2;
    //}
    public static <A, B> A convert(A obj1, B obj2)
    {
        return (A) obj2;
    }
}
上面对泛型对象进行加法,编译时即报错。而试图将一个泛型对象转换成另一个泛型表示的类时,运行时会报错。
这里的类型参数占位符就如同前面的通配符。3.1和3.2中通配符仅出现在一个确定的类的类型参数位置上,比如ArrayList<?> a,表示集合元素的类不明确,但引用a的类仍然明确是ArrayList。上面例子中obj1的类本身都是不明确的。定义泛型时需要占位符,使用泛型时可以给这个占位符赋予具体的值, 这个值可以是通配符。
使用泛型进行类型转换能否成功,要考虑很多因素。
一是A,B两个类的父子关系:
public class Test
{
    public static void main(String[] args)  throws Exception
    {
        //警告
        Object C = convert(new Object(), "abc");
    }
  
    public static <A, B> A convert(A obj1, B obj2)
    {
        return (A) obj2;
    }
}
A是B的父类,所以能成功。
二是多态:
public class Test
{
    public static void main(String[] args)  throws Exception
    {
        //编译通过,运行警告
        Object obj1 = "abc";
        String D = convert("abc", obj1);
        
        //编译通过,运行报错:ClassCastException
        Object obj2 = new Object();
        String E = convert("abc", obj2);
    }
  
    public static <A, B> A convert(A obj1, B obj2)
    {
        return (A) obj2;
    }
}
对象具有多态性。上面的obj1的运行时类型是String, obj2的运行时类型是Object。故obj1转换成功,obj2不成功。张老师的视频里检验了obj1的情况,但没检验obj2的情况。
总之,泛型的使用不能使得原本不可行的操作变成可行的,毕竟占位符也好,通配符也好,都是要替换成具体类型的。不明确某个对象所属的类,就不能对它进行不确定是否可行的操作。

4.2 自定义泛型类

当一个类中的多个泛型方法共用同样的类型参数时,可以将占位符从方法移到类定义处,成为自定义泛型类:
import java.util.*;
import java.lang.reflect.*;

public class Test
{
    public static void main(String[] args)  throws Exception
    {
        Operate<Integer> op = new Operate<Integer>();
        op.print(1);
        op.reflect(1);
    }
}

class Operate<T>
{
    ArrayList<T> array = new ArrayList<T>();

    public void print(T obj)
    {
        System.out.println(obj);
    }
    public void reflect(T obj)
    {
        System.out.println(obj.getClass().getName());
    }
    public void add(T obj) 
    {
        array.add(obj);
    }
}
//运行结果:
//1
//java.lang.Integer
上面是一个简单的泛型类,类型参数占位符写在类名之后,方法名中无需再写。

5. 泛型的反射

前面提到过由于类型擦除的缘故,泛型引用的反射是无法得到类型参数的。
但是我们却可以用一种较为曲折的方法得到:
(1)建立一个包含该泛型的形参的方法;
(2)反射该方法所在的类得到该方法的类对象;
(3)通过Method类的getGenericParameterTypes()方法得到形参实际类型组成的数组,以及getGenericReturnType()方法得到返回值实际类型;
(4)从类型数组中取出泛型形参对应的类型;
(5)用ParameterizedType类的getActualTypeArguments()方法得到该泛型形参的类型参数:
import java.util.*;
import java.lang.reflect.*;

public class Test
{
    public static void main(String[] args)  throws Exception
    {
        Vector<Integer> op = new Vector<Integer>();
        //反射得到getGenerics()方法的类对象
        Method GenericMethod = Test.class.getMethod("getGenerics", Vector.class);
        //得到getGenerics()方法形参的类型参数
        Type[] parameterType = GenericMethod.getGenericParameterTypes();
        ParameterizedType pType = (ParameterizedType) parameterType[0];
        System.out.println(pType.getActualTypeArguments()[0]);
        //得到getGenerics()方法返回值的类型参数
        Type returnType = GenericMethod.getGenericReturnType();
        ParameterizedType rType = (ParameterizedType) returnType;
        System.out.println(rType.getActualTypeArguments()[0]);
    }

    public Vector<Integer> getGenerics(Vector<Integer> tmp)
    {
        return null;
    }
}
//运行结果:
//class java.lang.Integer
//class java.lang.Integer
上面的例子说明,类型擦除仅针对非形参的泛型的类型参数,如果泛型出现在方法的形参列表中,是一个形参,则内存的方法区中仍然保留了形参的类型参数信息(用作运行时检验兼容性)。

6. 总结

泛型的内容抽象且繁杂。笔者感觉要掌握好泛型,一是要能推断类型参数的实际值,二是能避免对泛型进行不可行的操作。第一个能力的掌握是第二个的前提。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章