Java中的泛型以及JVM是如何实现泛型的?

泛型是什么

  • 泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理 解呢?
  • 顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形 参),然后在使用/调用时传入具体的类型(类型实参)。
  • 泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说 在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、 泛型方法。
  • 引入一个类型变量 T(其他大写字母都可以,不过常用的就是 T,E,K,V 等等),并且用<>括起来,并放在类名的后面。泛型类 是允许有多个类型变量的。

泛型类

/**
 * 泛型类
 * 引入一个类型变量T(其他大写字母都可以,不过常用的就是T,E,K,V等等)
 */
public class NormalGeneric<T> {
    private T data;

    public NormalGeneric() {
    }

    public NormalGeneric(T data) {
        this();
        this.data = data;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public static void main(String[] args) {
        NormalGeneric<String> normalGeneric = new NormalGeneric<>();
        normalGeneric.setData("Test");
        System.out.println(normalGeneric.getData());
    }
}

泛型接口

  • 泛型接口与泛型类的定义基本相同
/**
 *泛型接口
 * 引入一个类型变量T(其他大写字母都可以,不过常用的就是T,E,K,V等等)
 */
public interface Generator<T> {
    public T next();
}
  • 而实现泛型接口的类,有两种实现方法:
  1. 未传入泛型实参时:
/**
 * 实现泛型类,方式1
 * 引入一个类型变量T(其他大写字母都可以,不过常用的就是T,E,K,V等等)
 */
public class ImplGenerator<T> implements Generator<T> {

    private T data;

    public ImplGenerator(T data) {
        this.data = data;
    }

    @Override
    public T next() {
        return data;
    }
}

在 new 出类的实例时,需要指定具体类型:

 public static void main(String[] args) {
        ImplGenerator<String> implGenerator = new ImplGenerator<>("Test");
        System.out.println(implGenerator.next());

    }
  1. 传入泛型实参
/**
 *  实现泛型类,方式2
 */
public class ImplGenerator2 implements Generator<String> {
    @Override
    public String next() {
        return "King";
    }
}

在 new 出类的实例时,和普通的类没区别。

 public static void main(String[] args) {
        ImplGenerator2 implGenerator2 = new ImplGenerator2();
        System.out.println(implGenerator2.next());
    }

泛型方法

/**
 * 泛型方法
 * 引入一个类型变量T(其他大写字母都可以,不过常用的就是T,E,K,V等等)
 * 说明:
 * 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 * 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 * 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 * 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
 */
public class GenericMethod {
    //泛型方法
    public <T> T genericMethod(T t) {
        return t;
    }

    //普通方法
    public void test(int x, int y) {
        System.out.println(x + y);
    }

    public static void main(String[] args) {
        GenericMethod genericMethod = new GenericMethod();
        genericMethod.test(13, 7);
        System.out.println(genericMethod.<String>genericMethod("Test"));
        System.out.println(genericMethod.genericMethod(180));
    }
}

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

为什么我们需要泛型?

通过两段代码我们就可以知道为何我们需要泛型

/**
 * 为什么需要泛型
 */
public class NeedGeneric {

    public int addInt(int x,int y){
        return x+y;
    }

    public float addFloat(float x,float y){
        return x+y;
    }

    public static void main(String[] args) {
        //不使用泛型
        NeedGeneric needGeneric = new NeedGeneric();
        System.out.println(needGeneric.addInt(1,2));
        System.out.println(needGeneric.addFloat(1.2f,2.4f));

        //使用泛型
        System.out.println(needGeneric.add(3.2d,4.5d));
        System.out.println(needGeneric.add(1,2));
    }
    //泛型方法
    public <T extends Number> double add(T x,T y){
        return x.doubleValue()+y.doubleValue();
    }
}

实际开发中,经常有数值类型求和的需求,例如实现 int 类型的加法, 有时候还需要实现 long 类型的求和, 如果还需要 double 类型 的求和,需要重新在重载一个输入是 double 类型的 add 方法。

所以泛型的好处就是:

  • 适用于多种数据类型执行相同的代码
  • 泛型中的类型在使用时指定,不需要强制类型转换

虚拟机是如何实现泛型的?

  • Java 语言中的泛型,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(RawType,也称为裸类 型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的 Java 语言来说,ArrayList<int>与 ArrayList<String>就是同一 个类,所以泛型技术实际上是 Java 语言的一颗语法糖,Java 语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛 型。

  • 将一段 Java 代码编译成 Class 文件,然后再用字节码反编译工具进行反编译后,将会发现泛型都不见了,程序又变回了 Java 泛型 出现之前的写法,因为泛型类型都变回了原生类型

/**
 * 泛型擦除
 */
public class Theory {
    public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        map.put("King","18");
        System.out.println(map.get("King"));
    }
}

在这里插入图片描述

使用泛型注意事项(作为了解)

在这里插入图片描述
上面这段代码是不能被编译的,因为参数 List<Integer>和 List<String>编译之后都被擦除了,变成了一样的原生类型 List<E>, 擦除动作导致这两种方法的特征签名变得一模一样(注意在 IDEA 中是不行的,但是 jdk 的编译器是可以,因为 jdk 是根据方法返回值+ 方法名+参数)。

  • JVM 版本兼容性问题:JDK1.5 以前,为了确保泛型的兼容性,JVM 除了擦除,其实还是保留了泛型信息(Signature 是其中最重要的 一项属性,它的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类 型的信息)----弱记忆
  • 另外,从 Signature 属性的出现我们还可以得出结论,擦除法所谓的擦除,仅仅是对方法的 Code 属性中的字节码进行擦除,实际 上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章