安全的使用Java泛型

229 阅读12分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

引言

泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。泛型之所以被提出,主要是为了把类型参数化。我们在使用变量传参时,变量的值可以是不确定的,但是变量的类型是确定的,因为我们提前声明了变量,声明就确定了变量的类型。泛型就是希望把变量的类型也作为“变量”。把类型定义成参数形式(有人也称“类型形参”),然后在调用时再传入具体的类型(类型实参)。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

我们来看这样一个例子:

image.png

可以发现,有一行报错,编译无法通过,报错的提示是“要求是String,提供是Object”。也就是说,List根本记不住你放进来的是什么类型,所有的类型它都作为Object类型来处理。有人说这还不简单,我强转为String不就行了。

String item = (String)arrayList.get(i); \\强转后不报错,可以通过编译

但是运行时报错了,如下图:

image.png

这是因为我们加入了一个String和一个Integer类型,使用时都以String的方式使用,因此程序崩溃了。此时我们加入泛型,在声明时就指定类型,代码如下:

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

修改后的代码在编译阶段就会帮助检查,使得编程更加安全。

泛型特性及使用

前面我们添加泛型后仿佛List就能记住我们输入的变量类型了,输入不同类型就报错。但其实泛型只在编译阶段有效。如下图: image.png

可以看到输出结果为“类型相同”。通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。概括起来就是:泛型类型在逻辑上可以看成是不同的类型,实际上都是相同的基本类型。

前面我们说泛型有三种使用方式:泛型类、泛型接口、泛型方法。泛型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。泛型类的基本写法就是:类名后面加一个泛型标识,用尖括号包围,泛型标识可以写任意标识号。例如:

//此处的标识“T”,可以随便写,如T、E、K、V等。
//在实例化泛型类时,必须指定T的具体类型
    public class Generic<T>{
        //key这个成员变量的类型为T,T的类型由外部指定  
        private T key;

        public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
            this.key = key;
        }

        public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
            return key;
        }
    }

注意:泛型的类型参数只能是类类型(包括自定义类),不能是简单类型,如int、byte、float、char等。传入的实参类型需与泛型的类型相同,如:

//泛型为Integer,传入实参也必须是Integer
Generic<Integer> genericInteger = new Generic<Integer>(123456); 
//泛型为String,传入实参也必须是String
Generic<String> genericString = new Generic<String>("key_vlaue");

注意:定义的泛型类,不一定就要传入泛型类型实参,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。此外,不能对确切的泛型类型使用instanceof操作。

除了泛型类还有泛型接口。泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,基本如下:

//定义一个泛型接口
public interface Generator<T> {
    public T next();
}

有了接口,就要有实现,不然定义它就没有意义,我们来看实现类如何定义:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator<T> implements Generator<T>
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

还有一种情况,就是实现类直接写明泛型的类型。定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator <T>,但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。但如果在实现类实现泛型接口时已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型,如下:

/**
 * Generator<T>,public T next();中的的T都要替换成传入的String类型。
 */
public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

我们前面说了IngeterNumber的一个子类,也验证过Generic<Ingeter>Generic<Number>实际上是相同的一种基本类型。那么在使用Generic<Number>作为形参的方法中,能否使用Generic<Ingeter>的实例传入呢?在逻辑上类似于Generic<Number>Generic<Ingeter>是否可以看成具有父子关系的泛型类型呢?我们继续使用Generic<T>这个泛型类举例子:

public void showKeyValue1(Generic<Number> obj){\
    Log.d("泛型测试","key value is " + obj.getKey());\
}
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);

showKeyValue(gNumber);

结果就是showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer>。通过提示信息我们可以看到Generic<Integer>不能被看作为 Generic<Number> 的子类。由此可以看出,同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。

那么如何解决上面的问题,有人会说,定义一个新的方法来处理Generic<Integer>,但这显然与java中的多态理念相违背。因此我们需要一个在逻辑上可以表示同时是Generic<Integer>Generic<Number>父类的引用类型。由此类型通配符应运而生。我们可以将上面的方法改一下:

public void showKeyValue1(Generic<?> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}

类型通配符一般是使用" ?"代替具体的类型实参,注意了,此处" ?"是类型实参,而不是类型形参 。再直白点的意思就是,此处的" ?"和Number、String、Integer一样都是一种实际的类型,可以把" ?"看成所有类型的父类。是一种真实的类型。可以解决当具体类型不确定的时候,使用通配符" ?";当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用" ?"通配符来表未知类型。

最后就是泛型方法。在java中,泛型类的定义非常简单,但是泛型方法就相对复杂。尤其是我们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,这样可能错误理解泛型方法。泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。

public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
        IllegalAccessException{
    T instance = tClass.newInstance();
    return instance;
}
Object obj = genericMethod(Class.forName("com.test.test"));

上述代码中,public 与 返回值 T 中间的<T>非常重要,可以理解为声明此方法为泛型方法。只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。与泛型类的定义一样,此处T可以随便,常见的如T、E、K、V等。

光看上面的例子有的同学可能依然会非常迷糊,我们再通过一个例子,把我泛型方法再总结一下。

public class GenericTest {
    //泛型类
    public class Generic<T>{
        private T key;
        public Generic(T key) {
            this.key = key;
        }
        public T getKey(){
            return key;
        }
    }

    /**
     * 这才是一个真正的泛型方法。
     * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
     * 这个T可以出现在这个泛型方法的任意位置.
     * 泛型的数量也可以为任意多个 
     *    如:public <T,K> K showKeyName(Generic<T> container){
     *        ...
     *        }
     */
    public <T> T showKeyName(Generic<T> container){
        System.out.println("container key :" + container.getKey());
        //当然这个例子举的不太合适,只是为了说明泛型方法的特性。
        T test = container.getKey();
        return test;
    }

    //这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
    public void showKeyValue1(Generic<Number> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }

    //这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
    //同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
    public void showKeyValue2(Generic<?> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }

    /**
     * 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
     * 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
     * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
     public <T> T showKeyName(Generic<E> container){
     ...
     }
     */

    /**
     * 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
     * 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
     * 所以这也不是一个正确的泛型方法声明。
     public void showkey(T genericObj){

     }
     */

    public static void main(String[] args) {
    }
}

注意其中的getKey()方法,虽然在方法中使用了泛型,但是这不是一个泛型方法。这只是类中一个普通的成员方法,只不过他的返回值是在定义泛型类时的泛型。如果此方法把T改为E,那么就会报错。因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。

当然这并不是泛型方法的全部,泛型方法可以出现杂任何地方和任何场景中使用。但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,我们再通过一个例子看一下:

public class GenericFruit {
    class Fruit{
        @Override
        public String toString() {
            return "fruit";
        }
    }

    class Apple extends Fruit{
        @Override
        public String toString() {
            return "apple";
        }
    }

    class Person{
        @Override
        public String toString() {
            return "Person";
        }
    }

    class GenerateTest<T>{
        public void show_1(T t){
            System.out.println(t.toString());
        }
        public <E> void show_3(E t){
            System.out.println(t.toString());
        }
        public <T> void show_2(T t){
            System.out.println(t.toString());
        }
    }

    public static void main(String[] args) {
        Apple apple = new Apple();
        Person person = new Person();

        GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
        //apple是Fruit的子类,所以这里可以
        generateTest.show_1(apple);
        //编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
        //generateTest.show_1(person);

        //使用这两个方法都可以成功
        generateTest.show_2(apple);
        generateTest.show_2(person);

        //使用这两个方法也都可以成功
        generateTest.show_3(apple);
        generateTest.show_3(person);
    }
}

上述代码的show_2()方法,在泛型类GenerateTest中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。再看方法show_3(),在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。

泛型方法也能做可变参数,例如:

public <T> void printMsg( T... args){
    for(T t : args){
        Log.d("泛型测试","t is " + t);
    }
}
printMsg("111",222,"aaaa",55.55);

静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。如下:

public class StaticGenerator<T> {
    /**
     * 如果静态方法要使用泛型,需要添加额外的泛型声明(将这个方法定义成泛型方法)
     * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
     * 如:public static void show(T t){..},此时编译器会提示错误信息:
     "StaticGenerator cannot be refrenced from static context"
     */
    public static <T> void show(T t){
    }
}

最后就是泛型上下边界。在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。为泛型添加上边界,即传入的类型实参必须是指定类型的子类型:

public void showKeyValue1(Generic<? extends Number> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}
Generic<String> generic1 = new Generic<String>("11111");
Generic<Integer> generic2 = new Generic<Integer>(2222);
Generic<Float> generic3 = new Generic<Float>(2.4f);
Generic<Double> generic4 = new Generic<Double>(2.56);
//这一行代码编译器会提示错误,因为String类型并不是Number类型的子类
//showKeyValue1(generic1);

showKeyValue1(generic2);
showKeyValue1(generic3);
showKeyValue1(generic4);

看到了很多文章中都会提起泛型数组,经过查看sun的说明文档,在java中是”不能创建一个确切的泛型类型的数组”的。也就是说下面的这个例子是不可以的:

List<String>[] ls = new ArrayList<String>[10];

而使用通配符创建泛型数组是可以的,如下面这个例子:

List<?>[] ls = new ArrayList<?>[10];
\\这样也是可以的:
List<String>[] ls = new ArrayList[10];