Java语法糖详解

前言

        在现代编程语言的发展历程中,语法糖(Syntactic Sugar)作为一种提升代码可读性和开发效率的重要特性,已经成为语言设计的重要组成部分。Java作为一门成熟且广泛应用的编程语言,在其长期演进过程中,语法糖的引入和优化始终是一个重要方向。从Java 5的自动装箱泛型,到Java 8的Lambda表达式,再到后续版本中的模式匹配等特性,语法糖不仅简化了代码编写,还推动了编程范式的革新。

                                        

        然而,语法糖并非仅仅是表面上的语法简化。它的背后隐藏着复杂的编译器处理机制和字节码转换逻辑。同时,语法糖也是大厂 Java 面试常问的一个知识点。本文将从Java编译器的工作机制入手,结合字节码分析与class文件结构解析,深入剖析常见语法糖的实现原理。通过javap反编译工具与ASM字节码框架的实际应用,帮助读者了解语法糖背后的技术本质。



1 什么是语法糖?

语法糖(Syntactic Sugar),也称语法糖衣。是指在编程语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用,能够让程序更加简洁,有更高的可读性,同时也更容易编写程序。

我们所熟知的编程语言中几乎都有语法糖。比如python中的列表推导式、JavaScript 中的箭头函数、Go 语言中的多返回值、Java 中的 Lambda 表达式等等。

比较有意思的是,在编程领域,除了语法糖,还有语法盐和语法糖精的说法。这里不展开叙述,读者可以自行查阅资料了解。

2 语法糖处理流程解析

2.1 Java编译处理流程

Java源代码(.java)通过javac编译器转换为平台无关的字节码(.class),这个转换过程包含三个关键阶段:

  1. 解析与符号表构建

  2. 语法糖解糖(Desugar)处理

  3. 字节码生成与优化

其中语法糖处理发生在编译器的com.sun.tools.javac.comp.TransTypescom.sun.tools.javac.comp.Lower阶段,负责将高级语法转换为JVM规范定义的标准结构。

2.2 解糖过程示例

以增强for循环为例:

List<String> list = Arrays.asList("a", "b");
for (String s : list) { /*...*/ }

编译器将其转换为:

for (Iterator<String> i = list.iterator(); i.hasNext();) {String s = i.next();/*...*/
}

3 Java中常见的典型语法糖及原理

前面讲过,语法糖的存在主要是方便开发人员使用。但实际上, Java 虚拟机并不支持这些语法糖。这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖

在 Java 语言的编译过程中,我们熟知可使用 `javac` 命令将后缀为 `.java` 的源文件编译成后缀为 `.class` 的字节码文件,这些字节码能够在 Java 虚拟机上运行。深入探究 `com.sun.tools.javac.main.JavaCompiler` 类的源码,可发现其 `compile()` 方法包含多个关键步骤,其中有一个重要环节是调用 `desugar()` 方法。这个方法就是专门负责实现 Java 源文件中语法糖的解糖操作

Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。下面我们一一列举阐述,帮助大家理解这些语法糖背后的原理本质。

1 泛型

        多数语言编程都支持泛型,但是,不同的编译器对于泛型的处理方式是不同的

虽然不同语言编译器对泛型的实现策略存在差异,但是通常可以分为以下两种:

Code Specialization(代码特化)和Code Sharing(代码共享)

        在C++和C#中,泛型的处理采用的是Code Specialization策略,而Java则采纳了Code Sharing的途径。 在Code Sharing模式下,Java编译器为每个泛型类型生成唯一的字节码表示,并将所有泛型类型的实例统一映射到这一字节码上。

        这种映射是通过类型擦除(Type Erasure)技术来实现的,它允许Java虚拟机(JVM)在运行时忽略泛型的具体类型信息。 具体来说,JVM并不能直接识别类似于Map<String, String> map这样的泛型语法。也就是说,对于 Java 虚拟机来说,他根本不认识Map<String, String> map这样的语法。需要在编译阶段通过类型擦除的方式进行解语法糖。

        类型擦除的核心步骤包括:

        ①将所有泛型参数替换为其最左边界类型,即泛型参数的最顶级父类型。

        ②删除代码中的所有类型参数,从而使得泛型类型实例化为原始类型。

举例:

Map<String, String> map = new HashMap<String, String>();
map.put("name", "xiaoliang");
map.put("school", "PKUT");
map.put("address", "anhuihefei");

解语法糖后:

Map map = new HashMap(); // 类型擦除,泛型参数被移除
map.put("name", "xiaoliang"); // 自动装箱,因为 put 方法接受 Object 类型的参数
map.put("school", "PKUT");
map.put("address", "anhuihefei");

又如以下代码:

public static <A extends Comparable<A>> A max(Collection<A> xs) {Iterator<A> xi = xs.iterator();A w = xi.next();while (xi.hasNext()) {A x = xi.next();if (w.compareTo(x) < 0)w = x;}return w;
}

类型擦除后会变成:

 public static Comparable max(Collection xs){Iterator xi = xs.iterator();Comparable w = (Comparable)xi.next();while(xi.hasNext()){Comparable x = (Comparable)xi.next();if(w.compareTo(x) < 0)w = x;}return w;
}

虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的Class类对象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class

2 变长参数

可变参数(variable arguments)是在 Java 1.5 中引入的一个特性。它允许一个方法把任意数量的值作为参数。

看下以下可变参数代码,其中 print 方法接收可变参数:

public static void main(String[] args) {print("xiaoliang", "博客:https://blog.csdn.net/m0_73804764?spm=1000.2115.3001.5343", "QQ:2337504725");
}public static void print(String... strs) {for (int i = 0; i < strs.length; i++) {System.out.println(strs[i]);}
}

反编译后:

public static void main(String[] args) {String[] varargs = new String[] {"xiaoliang","博客:https://blog.csdn.net/m0_73804764?spm=1000.2115.3001.5343","QQ:2337504725"};print(varargs);
}public static void print(String[] strs) {for (int i = 0; i < strs.length; i++) {String str = strs[i];System.out.println(str);}
}

可变参数在被使用的时候,首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。

解语法糖原理
①可变参数转换为数组: 当方法 print 被调用时,传入的参数列表 "xiaoliang", "博客:https://blog.csdn.net/m0_73804764?spm=1000.2115.3001.5343", "QQ:2337504725" 会被编译器转换成一个 String 类型的数组。
②方法调用替换: 编译器会将 print 方法的调用替换为对一个新的方法调用的形式,这个新方法接受一个 String 数组作为参数。

3 条件编译

—般情况下,程序中的每一行代码都要参加编译。但有时候出于对程序代码优化的考虑,希望只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译。

如在 C 或 CPP 中,可以通过预处理语句来实现条件编译。其实在 Java 中也可实现条件编译。我们先来看一段代码:

public class ConditionalCompilation {public static void main(String[] args) {final boolean DEBUG = true;if(DEBUG) {System.out.println("Hello, DEBUG!");}final boolean ONLINE = false;if(ONLINE){System.out.println("Hello, ONLINE!");}}
}

反编译后:

public class ConditionalCompilation
{public ConditionalCompilation(){}public static void main(String args[]){boolean DEBUG = true;System.out.println("Hello, DEBUG!");boolean ONLINE = false;}
}

观察反编译后的代码我们发现,在反编译后的代码中没有System.out.println("Hello, ONLINE!");,这其实就是条件编译。当if(ONLINE)为 false 的时候,编译器就没有对其内的代码进行编译。

所以,Java 语法的条件编译,是通过判断条件为常量的 if 语句实现的。其原理也是 Java 语言的语法糖。根据 if 判断条件的真假,编译器直接把分支为 false 的代码块消除。

解语法糖原理
①常量折叠: 在编译时,编译器会识别出 final 关键字修饰的变量,这些变量被赋值为常量。编译器会执行一个称为“常量折叠”的过程,即它会在编译时计算并替换这些常量的值。
②条件优化: 当编译器遇到条件语句时,如果条件是一个编译时常量(即被 final 修饰且在编译时已知的值),编译器会根据该常量的值来决定是否包含对应的代码块。如果条件为 true,则包含该代码块;如果条件为 false,则不包含该代码块。

4 自动拆装箱

(1)自动装箱:Java 自动将原始类型值转换成对应的对象,比如将 int 的变量转换成 Integer 对象

原始类型 byte, short, char, int, long, float, double 和 boolean

对应的封装类为 Byte, Short, Character, Integer, Long, Float, Double, Boolean。

先来看个自动装箱的代码:

 public static void main(String[] args) {int i = 1688;Integer n = i;
}

反编译后代码如下:

public static void main(String args[])
{int i = 1688;Integer n = Integer.valueOf(i);
}

(2)自动拆箱:当需要将封装类的实例赋值给原始数据类型的变量时,编译器会自动插入对封装类相应 xxxValue 方法的调用,从而提取出原始数据类型的值。比如将 Integer 对象转换成 int 类型值

来看个自动拆箱的代码:

public static void main(String[] args) {Integer i = 1688; // 自动装箱int n = i; // 自动拆箱
}

反编译后:

public static void main(String args[]) {Integer i = Integer.valueOf(1688); // 调用valueOf方法实现装箱int n = i.intValue(); // 调用intValue方法实现拆箱
}

解语法糖原理

总结来说,自动装箱是通过封装类的 valueOf 方法实现的,而自动拆箱则是通过封装类的 xxxValue 方法实现的,其中 xxx 代表对应原始数据类型的名称

5 内部类

内部类又称为嵌套类,可以把内部类理解为外部类的一个普通成员。

内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念,outer.java里面定义了一个内部类inner,一旦编译成功,就会生成两个完全不同的.class文件了,分别是outer.class和outer$inner.class。所以内部类的名字完全可以和它的外部类名字相同。

public class OutterClass {private String userName;public String getUserName() {return userName;}public void setUserName(String userName) {this.userName = userName;}public static void main(String[] args) {}class InnerClass{private String name;public String getName() {return name;}public void setName(String name) {this.name = name;}}
}

以上代码编译后会生成两个 class 文件:OutterClass$InnerClass.class、OutterClass.class 。当我们尝试对OutterClass.class文件进行反编译的时候,命令行会打印以下内容:Parsing OutterClass.class...Parsing inner class OutterClass$InnerClass.class... Generating OutterClass.jad 。他会把两个文件全部进行反编译,然后一起生成一个OutterClass.jad文件。文件内容如下:

public class OutterClass
{class InnerClass{public String getName(){return name;}public void setName(String name){this.name = name;}private String name;final OutterClass this$0;InnerClass(){this.this$0 = OutterClass.this;super();}}public OutterClass(){}public String getUserName(){return userName;}public void setUserName(String userName){this.userName = userName;}public static void main(String args1[]){}private String userName;
}

解语法糖原理

当编译器遇到内部类的定义时,它会执行以下步骤来“解语法糖”:

①生成独立的类文件:编译器会为内部类生成一个独立的 .class 文件。这个文件的命名规则通常是外部类名++内部类名,例如‘𝑂𝑢𝑡𝑡𝑒𝑟𝐶𝑙𝑎𝑠𝑠+内部类名,例如‘OutterClassInnerClass.class`。
②修改成员访问:内部类中对外部类成员的访问会被编译器修改,以便在运行时正确地访问这些成员。这通常涉及到添加额外的方法来访问外部类的私有成员。
③添加外部类引用:编译器会在内部类的构造方法中添加一个额外的参数,这个参数是对外部类实例的引用。这样,内部类就可以访问外部类的成员。

6 Lambda表达式

Lambda 表达式是 Java 8 中引入的一个特性,它提供了一种更简洁的方式来表示只有一个抽象方法的接口(称为函数式接口)的实例。

Lambda 表达式通常由以下三部分组成:

  1. 参数列表:对应于函数式接口中的方法的参数。
  2. 箭头(->):将参数列表与方法体分隔开。
  3. 方法体:可以是表达式或代码块,其结果或返回值将作为 Lambda 表达式的返回值。

例如:

Runnable r = () -> System.out.println("Hello, World!");

解语法糖原理

当编译器遇到 Lambda 表达式时,它会执行以下步骤来“解语法糖”:

  1. 生成匿名内部类:编译器会为 Lambda 表达式生成一个匿名内部类,该类实现了函数式接口。

  2. 实现抽象方法:编译器会在匿名内部类中实现函数式接口的抽象方法。Lambda 表达式的参数列表和方法体将被转换成这个方法的参数和代码。

  3. 处理变量捕获:如果 Lambda 表达式访问了外部作用域的变量,编译器会确保这些变量是有效的。对于局部变量,它们必须是事实上的最终变量(effectively final),即它们在 Lambda 表达式被创建之后不能被修改。

解语法糖后:

Runnable r = new Runnable() {@Overridepublic void run() {System.out.println("Hello, World!");}
};

关于 lambda 表达式,有人可能会有质疑,因为网上有人说他并不是语法糖。其实我想纠正下这个说法。Lambda 表达式不是匿名内部类的语法糖,但是他也是一个语法糖。

总结:Lambda 表达式是一种语法糖,它依赖于 JVM 底层的 invokedynamic 指令和方法句柄等特性来实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/70304.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深度学习 - 神经网络的原理

## 深度学习 - 神经网络的原理 深度学习是机器学习的一个分支&#xff0c;其核心是模拟人脑神经网络的结构和功能&#xff0c;构建多层的神经网络模型&#xff0c;从数据中学习特征并进行预测或分类。 **神经网络的基本原理&#xff1a;** 1. **神经元模型:** * 神经网…

机器学习中的关键概念:通过SKlearn的MNIST实验深入理解

欢迎来到我的主页&#xff1a;【Echo-Nie】 本篇文章收录于专栏【机器学习】 1 sklearn相关介绍 Scikit-learn 是一个广泛使用的开源机器学习库&#xff0c;提供了简单而高效的数据挖掘和数据分析工具。它建立在 NumPy、SciPy 和 matplotlib 等科学计算库之上&#xff0c;支持…

【深度学习框架】MXNet(Apache MXNet)

MXNet&#xff08;Apache MXNet&#xff09;是一个 高性能、可扩展 的 开源深度学习框架&#xff0c;支持 多种编程语言&#xff08;如 Python、R、Scala、C 和 Julia&#xff09;&#xff0c;并能在 CPU、GPU 以及分布式集群 上高效运行。MXNet 是亚马逊 AWS 官方支持的深度学…

Java数据结构与算法之“树”

目录 一、什么是树 ​编辑 二、树的相关组成 1. 常用名词 2.需要了解的名词 三、树的分类 &#xff08;一&#xff09;初级树 1.普通树 2.二叉树 &#xff08;二&#xff09;中级树 1.哈夫曼树HuffmanTree 2.二叉搜索树BST 3.平衡二叉树AVL &#xff08;三&#x…

【Linux】27.Linux 多线程(1)

文章目录 1. Linux线程概念1.1 线程和进程1.2 虚拟地址是如何转换到物理地址的1.3 线程的优点1.4 线程的缺点1.5 线程异常1.6 线程用途 2. Linux进程VS线程2.1 进程和线程2.2 关于进程线程的问题 3. Linux线程控制3.1 POSIX线程库3.2 创建线程3.3 线程终止3.4 线程等待3.5 分离…

SpringAI系列 - 使用LangGPT编写高质量的Prompt

目录 一、LangGPT —— 人人都可编写高质量 Prompt二、快速上手2.1 诗人 三、Role 模板3.1 Role 模板3.2 Role 模板使用步骤3.3 更多例子 四、高级用法4.1 变量4.2 命令4.3 Reminder4.4 条件语句4.5 Json or Yaml 方便程序开发 一、LangGPT —— 人人都可编写高质量 Prompt La…

2025.2.6

一、C思维导图&#xff1a; 二、C&#xff1a; 三、注释代码 1> 配置文件&#xff1a;.pro文件 QT core gui # 引入的类库&#xff0c;core表示核心库 gui图形化界面库greaterThan(QT_MAJOR_VERSION, 4): QT widgets # 超过版本4的qt&#xff0c;会自动加widgets…

vue2-插槽slot

文章目录 vue2-插槽slot1. 什么是slot2. slot分类2.1 默认插槽2.2 具名插槽2.3 作用域插槽 vue2-插槽slot 1. 什么是slot 在vue中&#xff0c;slot翻译为插槽&#xff0c;简单点说&#xff0c;就是在子组件内放置一个插槽&#xff0c;等待父组件在使用子组件的时候决定放什么…

【算法应用】Alpha进化算法求解二维栅格路径规划问题

目录 1.算法原理2.二维路径规划数学模型3.结果展示4.参考文献5.代码获取 1.算法原理 Alpha进化&#xff1a;一种具有进化路径自适应和矩阵生成的高效进化算法 2.二维路径规划数学模型 栅格法模型最早由 W.E. Howden 于 1968 年提出&#xff0c;障碍物的栅格用黑色表示&#…

《深度洞察ICA:人工智能信号处理降维的独特利器》

在人工智能技术飞速发展的今天&#xff0c;信号处理作为关键环节&#xff0c;面临着数据维度不断攀升的挑战。高维信号数据虽蕴含丰富信息&#xff0c;但也给处理和分析带来诸多难题&#xff0c;如计算资源消耗大、分析复杂度高、模型易过拟合等。独立成分分析&#xff08;ICA&…

ubuntu20.04+RTX4060Ti大模型环境安装

装显卡驱动 这里是重点&#xff0c;因为我是跑深度学习的&#xff0c;要用CUDA&#xff0c;所以必须得装官方的驱动&#xff0c;Ubuntu的附件驱动可能不太行. 进入官网https://www.nvidia.cn/geforce/drivers/&#xff0c;选择类型&#xff0c;最新版本下载。 挨个运行&#…

vmware虚拟机可以使用Windows的GPU吗

是的&#xff0c;VMware虚拟机可以使用Windows的GPU&#xff0c;但这需要满足一定的条件&#xff0c;并且需要进行一些配置。以下是关键点&#xff1a; 1. 硬件要求 GPU支持直通&#xff08;Passthrough&#xff09;&#xff1a;你的物理GPU必须支持硬件直通&#xff08;VT-d…

Spring Boot 2 快速教程:WebFlux优缺点及性能分析(四)

WebFlux优缺点 【来源DeepSeek】 Spring WebFlux 是 Spring 框架提供的响应式编程模型&#xff0c;旨在支持非阻塞、异步和高并发的应用场景。其优缺点如下&#xff1a; 优点 高并发与低资源消耗 非阻塞 I/O&#xff1a;基于事件循环模型&#xff08;如 Netty&#xff09;&am…

DeepSeek 硅基流动

DeepSeek 硅基流动 &#x1f381; 四大神仙优势&#x1f31f; 三步拥有官网同款671B大模型1️⃣ 戳这里&#x1f449; 国内直连通道2️⃣ 复制API密钥3️⃣ 安装Chatbox贴进软件秒变AI大佬 &#x1f4c1; 网盘地址&#xff1a;&#xff08;所用到的软件可以直接下载&#xff09…

利用UNIAPP实现短视频上下滑动播放功能

在 UniApp 中实现一个短视频上下滑动播放的功能,可以使用 swiper 组件来实现滑动效果,并结合 video 组件来播放短视频。以下是一个完整的示例,展示如何在 UniApp 中实现这一功能。 1. 创建 UniApp 项目 如果你还没有创建 UniApp 项目,可以使用 HBuilderX 创建一个新的项目…

ES6 变量解构赋值总结

1. 数组的解构赋值 1.1 基本用法 // 基本数组解构 const [a, b, c] [1, 2, 3]; console.log(a); // 1 console.log(b); // 2 console.log(c); // 3// 跳过某些值 const [x, , y] [1, 2, 3]; console.log(x); // 1 console.log(y); // 3// 解构剩余元素 const [first, ...re…

数据库迁移后在密码不知道的情况下重建DBLINK

9i和10gR1版本之前&#xff0c;所有 dblink 的密码都是以明文方式在 sys.link$ 中的password字段中存储。可以直接通过查询sys.link$基表进行SQL拼接来完成迁移dblink。 select create database link ||NAME || connect to || USERID || identified by || password || using…

mysql 学习10 多表查询 -多表关系,多表查询

多表关系 一对多 多对多 创建学生表 #多对多表 学生选课系统create table student(id int primary key auto_increment comment 主键ID,name varchar(64) comment 姓名,studentnumber varchar(10) comment 学号 )comment 学生表;insert into student(id,name,studentnumber)va…

云端IDE如何重定义开发体验

豆包 MarsCode 是一个集成了AI功能的编程助手和云端IDE&#xff0c;旨在提高开发效率和质量。它支持多种编程语言和IDE&#xff0c;提供智能代码补全、代码解释、单元测试生成和问题修复等功能&#xff0c;同时具备AI对话视图和开发工具。 豆包 MarsCode 豆包 MarsCode 编程助…

6. k8s二进制集群之各节点部署

获取kubernetes源码安装主节点&#xff08;分别执行以下各节点命令&#xff09;安装工作节点&#xff08;同步kebelet和kube-proxy到各工作节点&#xff09;总结 继续上一篇文章《k8s二进制集群之ETCD集群部署》下面介绍一下各节点的部署与配置。 获取kubernetes源码 https:/…