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,一经查实,立即删除!

相关文章

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

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

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…

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

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

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

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

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…

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 编程助…

redis之RDB持久化过程

redis的rdb持久化过程 流程图就想表达两点&#xff1a; 1.主进程会fork一个子进程&#xff0c;子进程共享主进程内存数据(fork其实是复制页表)&#xff0c;子进程读取数据并写到新的rdb文件&#xff0c;最后替换旧的rdb文件。 2.在持久化过程中主进程接收到用户写操作&#x…

15.PPT:文静-云计算行业发展【29】

目录 NO123​ NO345​ NO6​ NO78 NO9/10/11/12​ NO123 设计→幻灯片大小→自定义幻灯片大小→ 全屏显示&#xff08;16&#xff1a;9&#xff09;→最大化 NO345 SmartArt 主题颜色2/6/9&#xff1a;形状样式&#xff1a;样式 加大行距加宽间距 NO6 NO78 设计→设置背景…

deepseek本地部署,使用python交互运行

deepseek Github 地址&#xff1a;https://github.com/deepseek-ai/DeepSeek-R1 在Github中我们看到这样的图片&#xff0c;模型参数等都可以通过HuggingFace下载&#xff0c;DeepSeek-R1-Distill-Qwen-参数量&#xff0c;参数量越大&#xff0c;对显存的要求更高 我们以参数量…

SpringUI Web高端动态交互元件库

Axure Web高端动态交互元件库是一个专为Web设计与开发领域设计的高质量资源集合&#xff0c;旨在加速原型设计和开发流程。以下是关于这个元件库的详细介绍&#xff1a; 一、概述 Axure Web高端动态交互元件库是一个集成了多种预制、高质量交互组件的工具集合。这些组件经过精…

Spring Boot整合MQTT

MQTT是基于代理的轻量级的消息发布订阅传输协议。 1、下载安装代理 进入mosquitto下载地址&#xff1a;Download | Eclipse Mosquitto&#xff0c;进行下载&#xff0c;以win版本为例 下载完成后&#xff0c;在本地文件夹找到下载的代理安装文件 使用管理员身份打开安装 安装…

网络数据请求

1.GET和POST请求 1.1发送GET请求 1.2发送POST请求 1.3 在页面刚加载的时候请求数据 2.request请求的注意事项

【OpenCV实战】基于 OpenCV 的多尺度与模板匹配目标跟踪设计与实现

文章目录 基于 OpenCV 的模板匹配目标跟踪设计与实现1. 摘要2. 系统概述3. 系统原理3.1 模板匹配的基本原理3.2 多尺度匹配 4. 逻辑流程4.1 系统初始化4.2 主循环4.3 逻辑流程图 5. 关键代码解析5.1 鼠标回调函数5.2 多尺度模板匹配 6. 系统优势与不足6.1 优势6.2 不足 7. 总结…

数据结构与算法学习笔记----博弈论

# 数据结构与算法学习笔记----博弈论 author: 明月清了个风 first publish time: 2025.2.6 ps⭐️包含了博弈论中的两种问题Nim游戏和SG函数&#xff0c;一共四道例题&#xff0c;给出了具体公式的证明过程。 Acwing 891. Nim游戏 [原题链接](891. Nim游戏 - AcWing题库) 给…

deepseek本地部署

DeepSeek本地部署详细指南 DeepSeek作为一款开源且性能强大的大语言模型&#xff0c;提供了灵活的本地部署方案&#xff0c;让用户能够在本地环境中高效运行模型&#xff0c;同时保护数据隐私&#xff0c;这里记录自己DeepSeek本地部署流程。 主机环境 cpu:amd 7500Fgpu:406…