23

细说 Java 泛型及其应用

 5 years ago
source link: http://blueskykong.com/2019/03/31/java-Generic/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我们通过如下的示例,引出为什么泛型的概念。

public class Test {

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("abc");
        list.add(2);

        for (int i = 0; i < list.size(); i++) {
            String name = (String) list.get(i); // error
            System.out.println("name:" + name);
        }
    }
}

当获取列表中的第二个元素时,会报错, java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 。这是常见的类型转换错误。

当我们将元素放入到列表中,并没有使用指定的类型,在取出元素时使用的是默认的 Object 类型。因此很容易出现类型转换的异常。

我们想要实现的结果是,集合能够记住集合内元素各类型,且能够达到只要编译时不出现问题,运行时就不会出现 java.lang.ClassCastException 异常。泛型刚好能满足我们的需求。

什么是泛型?

泛型,即 参数化类型 。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型,即在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型。在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口和泛型方法。

泛型的特点

Java 语言中引入泛型是一个较大的功能增强。不仅语言、类型系统和编译器有了较大的变化,已支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经成为泛型化的了。这带来了很多好处:

  1. 类型安全。 泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。
  2. 消除强制类型转换。 泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。
  3. 潜在的性能收益。 泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。

命名类型参数

推荐的命名约定是使用大写的单个字母名称作为类型参数。对于常见的泛型模式,推荐的名称是:

  • K:键,比如映射的键
  • V:值,比如 List 和 Set 的内容,或者 Map 中的值
  • E:元素
  • T:泛型
public class Generic<T> { 
    //key的类型为T  
    private T key;

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

    public T getKey() { 
    	//泛型方法getKey的返回值类型为T
       return key;
    }
}

如上定义了一个普通的泛型类,成员变量的类型为 T,T的类型由外部指定。泛型方法和泛型构造函数同样如此。

Generic<Integer> genericInteger = new Generic<Integer>(123456); //1

Generic<String> genericString = new Generic<String>("key_vlaue"); // 2

System.out.println("key is " + genericInteger.getKey());
System.out.println("key is " + genericString.getKey());

泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。传入的实参类型需与泛型的类型参数类型相同,即为Integer/String。

如上所述,定义的泛型类,就一定要传入泛型类型实参么?

并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

Generic genericString = new Generic("111111");
Generic genericInteger = new Generic(4444);

System.out.println("key is " + genericString.getKey());
System.out.println("key is " + genericInteger.getKey());

如上的代码片段,将会输出如下的结果:

key is 111111
key is 4444

在不传入泛型类型实参的情况下,泛型类中使用的泛型防范或成员变量可以为 Integer 或 String 等等其他任意类型。不过需要注意的是,泛型的类型参数只能是类类型,不能是简单类型。且不能对确切的泛型类型使用 instanceof 操作。对于不同传入的类型实参,生成的相应对象实例的类型是不是一样的呢?具体看如下的示例:

public class GenericTest {

    public static void main(String[] args) {

        Generic<Integer> name = new Box<String>("111111");
        Generic<String> age = new Box<Integer>(712);

        System.out.println("name class:" + name.getClass());  
        System.out.println("age class:" + age.getClass()); 
        System.out.println(name.getClass() == age.getClass());    // true
    }

}

由输出结构可知,在使用泛型类时,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存上只有一个,即还是原来的最基本的类型(本例中为 Generic),当然在逻辑上我们可以理解成多个不同的泛型类型。

究其原因,在于 Java 中的泛型这一概念提出的目的,其只是作用于代码编译阶段。在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦除。也就是说,成功编译过后的 class 文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。

泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

通配符

Ingeter 是 Number 的一个子类,同时 Generic<Ingeter>Generic<Number> 实际上是相同的一种基本类型。那么问题来了,在使用 Generic<Number> 作为形参的方法中,能否使用 Generic<Ingeter> 的实例传入呢?在逻辑上类似于 Generic<Number>Generic<Ingeter> 是否可以看成具有父子关系的泛型类型呢?下面我们通过定义一个方法来验证。

public void show(Generic<Number> obj) {
    System.out.println("key value is " + obj.getKey());
}

进行如下的调用:

Generic<Integer> genericInteger = new Generic<Integer>(123);

show(genericInteger);  //error Generic<java.lang.Integer>  cannot be applied to Generic<java.lang.Number>

通过提示信息我们可以看到 Generic<Integer> 不能被看作为 Generic<Number> 的子类。由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。

我们不能因此定义一个 show(Generic<Integer> obj) 来处理,因此我们需要一个在逻辑上可以表示同时是Generic 和Generic 父类的引用类型。由此类型通配符应运而生。

T、K、V、E 等泛型字母为有类型,类型参数赋予具体的值。除了有类型,还可以用通配符来表述类型, 未知类型,类型参数赋予不确定值,任意类型只能用在声明类型、方法参数上,不能用在定义泛型类上。将方法改写成如下:

public void show(Generic<?> obj) {
    System.out.println("key value is " + obj.getKey());
}

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

泛型上下边界

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

public void show(Generic<? extends Number> obj) {
    System.out.println("key value is " + obj.getKey());
}

我们在泛型方法的入参限定参数类型为 Number 的子类。

Generic<String> genericString = new Generic<String>("11111");
Generic<Integer> genericInteger = new Generic<Integer>(2222);


showKeyValue1(genericString); // error
showKeyValue1(genericInteger);

当我们的入参为 String 类型时,编译报错,因为 String 类型并不是 Number 类型的子类。

类型通配符上限通过形如 Generic<? extends Number> 形式定义;相对应的,类型通配符下限为 Generic<? super Number> 形式,其含义与类型通配符上限正好相反,在此不作过多阐述。

泛型数组

在 java 中是不能创建一个确切的泛型类型的数组的,即:

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

如上会编译报错,而使用通配符创建泛型数组是可以的:

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

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

JDK1.7 对泛型的简化,所以另一种声明也是可以的。

由于JVM泛型的擦除机制,在运行时 JVM 是不知道泛型信息的。泛型数组实际的运行时对象数组只能是原始类型( T[]为Object[],Pair []为Pair[] ),而实际的运行时数组对象可能是T类型( 虽然运行时会擦除成原始类型 )。成功创建泛型数组的唯一方式就是创建一个被擦出类型的新数组,然后对其转型。

public class GenericArray<T> {
    private Object[] array;  //维护Object[]类型数组
    @SupperessWarning("unchecked")
    public GenericArray(int v) {
        array = new Object[v];
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    public T get(int index) { 
    	return (T)array[index]; 
    } //数组对象出口强转
    public T[] rep() { return (T[])array; } //运行时无论怎样都是Object[]类型 
    public static void main (String[] args){
        GenericArray<Integer> ga = new GenericArray<Integer>(10);
        // Integer[] ia = ga.rep(); //依旧ClassCastException
        Object[] oa = ga.rep(); //只能返回对象数组类型为Object[]
        ga.put(0, 11);
        System.out.println(ga.get(0)); // 11
    }
}

在运行时,数组对象的出口做转型输出,入口方法在编译期已实现类型安全,所以出口方法可以放心强制类型转换,保证成功。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK