Java进阶之泛型与语法糖

Java泛型是一个重要的Java语法糖概念,虽然Java的继承和接口丰富了多态的灵活性,但我们仍然希望通过编写更通用的代码,能够适用“非特定的类型”,而不是一个具体的接口或类。

本来是写了很多东西,然而后来发现泛型涉及到的跨语言比较,概念理解和设计模式应用等内容其实是很多的,秉承只讲基础的原则,本期讲Java是如何做到泛型的,泛型在JVM又是怎么活动的?

一,Java的编译期与运行期

我们首先需要知道Java程序是如何转化成字节码文件,再转化成机器码在操作系统上调用实现功能的。

这里就涉及到两个Java的重要执行流程--编译器和运行期

Java进阶之泛型与语法糖_第1张图片

上图可以清楚地看出Java程序编译期和运行期的关系,我们编写完Java程序代码时是.java文件,通过本地编译期的编译转化成.class文件,也就是Java字节码文件。我们可以使用这些编译后的字节码文件拷贝到任意有Java环境的地方去执行,也就是进入Java的运行期。通过类加载器加载到JVM,翻译成机器码在操作系统调用执行。(这里需要好好体会概念,比如JVM和OS的关系,编译和翻译等等)

 

Java编译期

这里的Java编译期指的是我们常说的从.java文件转化成.class文件的过程,了解了这一过程你就知道你编写的.java文件和.class文件有什么不同,并为下文介绍泛型埋个伏笔。

编译期大致过程:

.java文件->解析与填充符号表->注解处理器->语义分析->解语法糖->字节码生成->输出->.class文件

这里简单介绍下过程,我们知道一种语言转化成另一种语言都需要走单词的词法分析,句子的语法分析和联系上下文的语义分析。Java编译期的第一步就是解析与填充符号表(过程为:词法分析-语法分析-填充符号表),在这一步中初步完成了目标代码生成阶段地址分配的依据;JDK5之后Java提供了对注解的支持,这些注解和普通Java代码一样都是在运行期发挥作用,注解处理器完成在Java文件解析之后注解的代码解析。语法分析后,编译器获得程序代码的抽象语法树,开始标注检查等一系列联系上下文的语义分析。

 

语法糖与解语法糖过程

语法糖指在计算机中添加某种语法,能够增加程序的可读性和扩展性。Java的语法糖常见的有泛型,变长参数,条件编译,自动拆装箱,内部类等等。语法糖的出现是为了方便开发人员使用, JVM并不识别, 会在编译阶段解语法糖,还原为基础语法,这个过程就是编译器的解语法糖。

随后执行字节码的生成并输出,即可得到一处编译处处执行的字节码文件了。

 

Java运行期

类加载器的任务就是把字节码资源载入到虚拟机运行时环境里。经过字节码检验后,Java解释器可以把高级语言一行一行解释称机器码供机器运行,通过利用热点探测和计数器处罚即时编译,即时编译(Just-in-time compilation)是一种提高程序运行效率的方法。通常,程序在执行前全部被翻译为机器码。完成提高编译机器码的执行效率。

 

二,泛型与类型擦除

Java泛型是JDK5提供的一项特性,本质是参数化类型的应用,也就是所操作的数据类型被指定为一个参数,这种参数类型可以用在类,接口和方法的创建中,分别称为泛型类,泛型接口和泛型方法。

要了解Java泛型之前,还需要知道什么是类型擦除。

Java 泛型是使用擦除实现的。这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此,List 和 List 在运行时实际上是相同的类型。它们都被擦除成原生类型 List。
.java---(编译期: Object类型)--->.class---(运行期: 原生类型)---->机器码

举个例子:

// 类型擦除前
Map map = new HashMap<>();
map.put("key1", "value1");
System.out.pringln(map.get("key1"));

// 类型擦除后
Map map = new HashMap();
map.put("key1", "value1");
System.out.pringln((String)map.get("key1"));

Java的泛型只在程序源码中存在,编译后的字节码文件就已经替换成原来的原生类型,并且在相应的地方插入了强制转型代码,所以对运行期Java来说,ArrayList和ArrayList是一个类。

类型擦除方式

// 代码不能被编译
// 类型擦除后两个方法特征签名一模一样
public static void method(List list) {
  //content
}

public static void method(List list) {
  //content
}

//代码可以被编译
//通过方法重载实现类型擦除后的可用

public static String method(List list) {
  //content
}

public static int method(List list) {
  //content
}

参数List和List编译之后类型擦除都变成了List, 实际编码中使用类型擦除会需要添加两个并不需要的返回值完成方法重载方式来实现类型扩展。不过擦除法所谓的擦除仅仅对方法代码属性中字节码进行擦除,实际元数据中还是保留了泛型信息,可以通过反射手段取得参数化类型。
 

三,简单Java泛型

我们可以利用类型擦除这种特点来作为一颗语法糖实现Java泛型。下面介绍一个简单泛型类的实现。

// Holder1.java
// 这个类只能持有单个对象的类
class Automobile {}

public class Holder1 {
    private Automobile a;
    public Holder1(Automobile a) { this.a = a; }
    Automobile get() { return a; }
}

jdk5之前可以利用直接持有object类达到类型的扩展

// ObjectHolder.java
// 通过装入object类,拿出对象时强制转换实现类型扩展

public class ObjectHolder {
    private Object a;
    public ObjectHolder(Object a) { this.a = a; }
    public void set(Object a) { this.a = a; }
    public Object get() { return a; }
    
    public static void main(String[] args) {
        ObjectHolder h2 = new ObjectHolder(new Automobile());
        Automobile a = (Automobile)h2.get();
        h2.set("Not an Automobile");
        String s = (String)h2.get();
        h2.set(1); // 自动装箱为 Integer
        Integer x = (Integer)h2.get();
    }
}

在JDK5之前,类型的扩展依靠java.lang.Object类,因为所有类型都继承于这个类,所以只有程序员和运行期的JVM知道这个object是什么类型,在编译期间无法检查这个object有没有转型成功,这样会把很多风险转嫁给程序运行期,不利于工程的维护。

// GenericHolder.java
// 通过泛型,使用holder类的时候再初始化,实现类型扩展

public class GenericHolder {
    private T a;
    public GenericHolder() {}
    public void set(T a) { this.a = a; }
    public T get() { return a; }
    
    public static void main(String[] args) {
        GenericHolder h3 = new GenericHolder<>();
        h3.set(new Automobile()); // 此处有类型校验
        Automobile a = h3.get();  // 无需类型转换
        //- h3.set("Not an Automobile"); // 报错
        //- h3.set(1);  // 报错
    }
}

与其使用 Object ,我们更希望先指定一个类型占位符,稍后再决定具体使用什么类型。要达到这个目的,需要使用类型参数,用尖括号括住,放在类名后面。然后在使用这个类时,再用实际的类型替换此类型参数来获得泛型的语法糖特性,实现类型扩展。

 

泛型是一颗Java提供给程序员的语法糖,利用类型擦除实现编程的类型扩展。

 

参考:

1. 《深入理解JVM》第四部分

2.  Java编译期与运行期:https://www.cnblogs.com/wyc1994666/p/11366802.html

3. Java泛型实例:https://blog.csdn.net/s10461/article/details/53941091?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-5.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-5.nonecase

你可能感兴趣的:(后台相关)