Java 高级特性泛型

1,038 阅读7分钟

概述

Generics(泛型),我们经常在Java集合或者框架里面经常看见对泛型的使用场景,那么泛型的作用有哪些呢。

  • 可以帮助我们处理多种数据类型执行相同的代码
  • 数据安全性,泛型中的类型在使用时指定类型后,不需要强制类型转换,最早起Java是没有泛型的,它是使用Object来代替的,这样程序员在写程序的时候很容易出现类型转换的错误。

泛型的本质是为了参数化类型,就是将类型由原来的具体的类型参数化,就像方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

泛型类和泛型接口

泛型类/接口的定义,使用一个类型变量T(其他大写字母都可以,不过常用的就是T,E,K,V等等),相当于一个占位符,并且用<>括起来,并放在类/接口名的后面。泛型类是允许有多个类型变量的

public class GenericsClass<T> {
    private void print(){
        System.out.println("泛型类型");
    }
    public static void main(String [] args){
        GenericsClass<String> genericsClass = new GenericsClass<>();
        genericsClass.print();
    }
}

public class GenericsClass<T,V> {
    private void print(){
        System.out.println("泛型类型");
    }
    public static void main(String [] args){
        GenericsClass<String,Integer> genericsClass = new GenericsClass<>();
        genericsClass.print();
    }
}
public interface ImpGenerics<V> {
}

而实现泛型接口的类,有两种实现方法

public class Generics<T> implements ImpGenerics<T> {
}

这中实现的时候,没有指定具体的类型,这种实现方式在创建对象的时候,需要传入具体的类型

public class Generics implements ImpGenerics<String> {
}

这种是在实现的时候传入具体的实参,创建对象的时候就想普通类一样使用就OK

泛型方法

泛型方法,是在调用方法的时候指明泛型的具体类型 ,泛型方法可以在任何地方和任何场景中使用,包括普通类和泛型类,

public class GenericsClass<K,V> {
    private K date;
    private V value;
    //普通方法
    private V getValue(K date){
        return value;
    }

    //泛型方法
    private <T> T genericsMethod(T date){
        return date;
    }

    public GenericsClass(K date, V value) {
        this.date = date;
        this.value = value;
    }

    private void print(){
        System.out.println("泛型类型");
    }
    public static void main(String [] args){
        GenericsClass<String,Integer> genericsClass = new GenericsClass<>("k",123);
        genericsClass.print();
        int a = genericsClass.getValue("k");
        System.out.println("v="+a);
    }
}

泛型方法必须通过“<类型占位符>”来声明返回的类型,譬如<V>等

泛型限定类型变量

通常在使用时候,我们需要让所有的类型具体同一个方法,我们需要对类型变量加以约束,比如计算两个变量的最小,最大值,为了确保传入的两个变量一定有compareTo方法?我们就需要将T限制为实现了接口Comparable的类

private <T extends Comparable> T min(T a,T b){
       return a.compareTo(b)>0 ? b:a;
    }

T extends Comparable中 T表示应该绑定类型的子类型,Comparable表示绑定类型,子类型和绑定类型可以是类也可以是接口,同时extends左右都允许有多个,如 T,V extends Comparable & Serializable 注意限定类型中,只允许有一个类,而且如果有类,这个类必须是限定列表的第一个。 这种类的限定既可以用在泛型方法上也可以用在泛型类上

 private <T extends Comparable & Serializable> T min(T a, T b){
       return a.compareTo(b)>0 ? b:a;
    }

泛型中的约束和局限性

  1. 不能用基本类型实例化类型参数
public class GenericsClass<K,V> {
    private K date;
    private V value;
    //普通方法
    private V getValue(K date){
        return value;
    }

    //泛型方法
    private <T> T genericsMethod(T date){
        return date;
    }

    private <T extends Comparable & Serializable> T min(T a, T b){
       return a.compareTo(b)>0 ? b:a;
    }

    public GenericsClass(K date, V value) {
        this.date = date;
        this.value = value;
    }

    private void print(){
        System.out.println("泛型类型");
    }
    public static void main(String [] args){
        //不能使用基本类型
        GenericsClass<String,int> genericsClass = new GenericsClass<>("k",123);
    }
}
  1. 运行时类型查询只适用于原始类型
public class GenericsClass<K> {
    private K date;

    private void print(){
        System.out.println("泛型类型");
    }
    public static void main(String [] args){
        GenericsClass<String> genericsClass = new GenericsClass<>();
        GenericsClass<String> genericsClass_ = new GenericsClass<>();
        System.out.println("泛型类型genericsClass="+genericsClass_.getClass().getName().toString());
        System.out.println("泛型类型genericsClass_="+genericsClass_.getClass().getName().toString());
    }
}

输出

泛型类型genericsClass=com.mtx.javalib.GenericsClass
泛型类型genericsClass_=com.mtx.javalib.GenericsClass
  1. 泛型类的静态上下文中类型变量失效

不能在静态域或方法中引用类型变量。因为泛型是要在对象创建的时候才知道是什么类型的,而对象创建的代码执行先后顺序是static的部分,然后才是构造函数等等。所以在对象初始化之前static的部分已经执行了,如果你在静态部分引用的泛型,那么毫无疑问虚拟机根本不知道是什么东西,因为这个时候类还没有初始化。 4. 不能创建参数化类型的数组

public class GenericsClass<K> {
    private K date;

    private void print(){
        System.out.println("泛型类型");
    }
    public static void main(String [] args){
        //编译会报错
        GenericsClass<String>[] genericsClass = new GenericsClass<String>()[3];
    }
}

为什么是这样,主要是Java的泛型实现方式有关,后续会说到

  1. 不能实例化类型变量
  2. 不能捕获泛型类的实例
 public <T extends Throwable> T testMethod(T t){
        try {
            System.out.println("泛型类型异常无法捕获");
        }catch (T e){
        }
        return t;
    }

但是这种方式是可以的

public <T extends Throwable> T testMethod(T t){
        try {
            System.out.println("泛型类型异常无法捕获");
        }catch (Throwable e){
        }
        return t;
    }

泛型类型的继承规则

泛型类可以继承或者扩展其他泛型类,比如List和ArrayList

通配符类型

一般来说泛型的通配符有两种,

  • ? extends X
    表示类型的上界,类型参数是X的子类或自身
  • ? super X
    表示类型的下界,类型参数是X的超类
public class Car {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

public class Bmw extends Car {
    @Override
    public String getName() {
        return super.getName();
    }
    @Override
    public void setName(String name) {
        super.setName(name);
    }
}
public class Bmw extends Car {
    @Override
    public String getName() {
        return super.getName();
    }
    @Override
    public void setName(String name) {
        super.setName(name);
    }

    public static void testMethod(GenericsClass<? extends Car> car){
        System.out.println("类型参数只能是Car的子类");
    }

    public static void  main(String[] args){
        GenericsClass<Bmw> genericsClass = new GenericsClass();
        testMethod(genericsClass);
    }
}

Java 泛型原理

Java语言中的泛型一般称为伪泛型,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说,ArrayList<int>与ArrayList<String>就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型
由于Java泛型的引入,各种场景(虚拟机解析、反射等)下的方法调用都可能对原有的基础产生影响和新的需求,如在泛型类中如何获取传入的参数化类型等。因此,JCP组织对虚拟机规范做出了相应的修改,引入了诸如Signature、LocalVariableTypeTable等新的属性用于解决伴随泛型而来的参数类型的识别问题,Signature是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名[3],这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。修改后的虚拟机规范要求所有能识别49.0以上版本的Class文件的虚拟机都要能正确地识别Signature参数。 另外,从Signature属性的出现我们还可以得出结论,擦除法所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据