解构Java虚拟机——理解字节码

在复杂的JVM世界中,字节码充当着中间语言的角色,使得Java程序能够超越特定平台硬件和操作系统的界限。随着我们深入探究JVM内部的核心,本章重点解读字节码,这是执行Java应用程序的基本组成部分。字节码被表示为一组指令,它充当了高级Java代码与底层硬件的机器特定语言之间的桥梁。通过理解字节码,开发人员可以深入了解JVM的内部工作原理,从而使他们能够优化代码性能并解决复杂的问题。

字节码的核心是一系列指令,它们规定了JVM执行的低级操作。本章揭示了算术运算的细微差别,阐明了JVM如何处理数学计算。从基本的加法和减法到更复杂的过程,我们探索了管理这些过程的字节码指令。此外,我们深入研究了值转换,揭开了JVM如何在不同类型之间转换数据的神秘面纱。理解这些低级操作对于寻求优化应用程序性能和效率的开发人员至关重要。加入我们,踏上探索字节码领域的旅程,算术运算和值转换的复杂性为掌握JVM铺平了道路。

在本章中,我们将探讨以下主题:

  • 揭示字节码
  • 算术运算
  • 值转换
  • 对象操作
  • 条件指令

技术要求

对于本章,您将需要以下内容:

  • Java 21
  • Git
  • Maven
  • 任何首选的集成开发环境

本章的GitHub存储库位于- github.com/PacktPublis...

揭示字节码

字节码在Java编程中扮演着关键角色,它是一种中间语言,促进了Java应用程序在JVM上的跨平台兼容性和执行。本节旨在揭示字节码的奥秘,全面概述其意义、目的以及在JVM中启用的操作范围。

在其核心,字节码充当了高级Java代码与底层硬件的机器特定语言之间的桥梁。当Java程序编译时,源代码被转换为字节码,这是一组对JVM可理解的指令。这种与平台无关的字节码允许Java应用程序在各种环境中无缝执行,这是Java"一次编写,到处运行"的基本理念。

为什么需要字节码?答案在于它为Java应用程序带来的可移植性和多功能性。通过在高级源代码和机器代码之间引入一个中间步骤,Java程序可以在任何配备有JVM的设备上运行,而不受其体系结构或操作系统的限制。这种抽象屏蔽了开发人员对硬件特定细节的复杂性,促进了更通用和易于访问的编程环境。

现在,让我们深入研究字节码中编码的操作。字节码指令涵盖了各种功能,从基本的加载和保存操作到复杂的算术计算。JVM的基于栈的体系结构管理这些操作,在这里值被推送和弹出到栈上,构成了数据操作的基础。算术运算包括加法、减法、乘法等,通过特定的字节码指令执行,使开发人员能够理解和优化其代码的数学基础。

值转换是字节码操作的另一个方面,涉及在不同类型之间转换数据。无论是将整数转换为浮点数还是管理其他类型转换,字节码指令为这些操作提供了基础。这种灵活性对于开发人员来说至关重要,他们在Java生态系统中编写代码并无缝处理各种数据类型。

除此之外,字节码还负责创建和操作对象,管理条件语句的执行,并控制方法的调用和返回。每个字节码指令都对Java程序的总体执行流程有所贡献,了解这些操作使开发人员能够创建高效、性能良好和可靠的应用程序。

确实,理解字节码行为对于掌握JVM的复杂性至关重要。字节码指令旨在对特定类型的值进行操作,识别被操作的类型是编写高效和正确的Java代码的基础。每个字节码助记符的首字母通常作为识别所执行操作类型的有用提示。

让我们深入探讨根据字节码助记符的首字母识别操作类型的技巧:

  • i表示整数操作:以i开头的字节码,比如iload(加载整数)、iadd(整数相加)或isub(整数相减),表示涉及整数值的操作。这些字节码指令操作存储为32位有符号整数的数据。
  • l表示长整数操作:以l开头的字节码,如lload(加载长整数)或lmul(长整数乘法),表示对64位有符号长整数的操作。
  • s表示短整数操作:以s开头的字节码,例如sload(加载短整数),与16位有符号短整数的操作相关。
  • b表示字节操作:以b开头的字节码,例如bload(加载字节),表示对8位有符号字节整数的操作。
  • c表示字符操作:以c开头的字节码,如caload(加载字符数组),表示对16位Unicode字符的操作。
  • f表示浮点数操作:f前缀,如fload(加载浮点数)或fadd(浮点数相加),表示涉及32位单精度浮点数的操作。
  • d表示双精度操作:以d开头的字节码,如dload(加载双精度)或dmul(双精度乘法),表示对双精度浮点数(64位)的操作。
  • a表示引用操作:以a开头的字节码,如aload(加载引用)或areturn(返回引用),表示涉及对象引用的操作。

这种系统命名约定有助于开发人员快速识别字节码指令所操作的数据类型。通过识别首字母并将其与特定数据类型相关联,开发人员可以编写更加详细和准确的代码,确保字节码操作与JVM中预期的数据类型和行为一致。这种理解对于掌握字节码并优化Java应用程序的性能和可靠性至关重要。

在Java字节码中,布尔值通常使用整数表示(0表示false,1表示true)。然而,需要注意的是,布尔值没有专用的字节码指令;而是使用标准的整数算术和逻辑指令。例如:

  • iadd、isub、imul、idiv等指令可以无缝地与布尔值一起使用
  • 逻辑操作,如and(iand)、or(ior)和xor(ixor),可用于布尔逻辑

关键要点是,布尔值在字节码中被视为整数,使开发人员能够对数字和布尔计算使用相同的算术和逻辑指令。

字节码为算术操作奠定了基础,塑造了Java程序的数学核心。我们的旅程将在下一节继续,我们将深入探讨字节码中算术操作的复杂世界。我们将剖析定义Java应用程序数学本质的字节码序列,揭示加法、减法、乘法等运算的指令。

通过理解字节码中编码的算术操作,开发人员可以深入了解其代码的内部工作原理,从而优化性能并提高效率。加入我们,一起揭示算术操作背后的秘密,为掌握JVM的复杂性铺平道路。

算术操作

在本节中,我们着眼于字节码的一个基石方面:算术操作。这些操作是赋予Java程序生命的数学基础,塑造了JVM内的计算数值景观。

字节码的算术操作遵循一个基本原则:它们对操作数栈上的前两个值进行操作,执行指定的操作,并将结果返回到栈上。本节深入探讨了字节码算术的复杂性,阐明了其微妙之处、行为和对程序执行的影响。

字节码中的算术操作分为两大类:涉及浮点数和处理整数。每个类别表现出不同的行为,了解这些差异对于寻求在数值计算中精度和可靠性的Java开发人员至关重要。

在我们探索字节码算术的过程中,我们探讨了指导浮点数和整数的加法、减法、乘法和除法的指令。我们剖析了封装这些操作的字节码序列,澄清了它们的实现和性能影响。

加法、减法、乘法和除法

基本算术操作是 Java 程序中数字计算的基石。从添加整数(iadd)到除法双精度数(ddiv),每个字节码指令都经过精心设计,以处理特定数据类型。解开整数、长整数、浮点数和双精度数的加法、减法、乘法和除法微妙之处:

加法:

  • iadd:添加两个整数
  • ladd:添加两个长整数
  • fadd:添加两个浮点数
  • dadd:添加两个双精度数

减法:

  • isub:从第一个整数中减去第二个整数
  • lsub:从第一个长整数中减去第二个长整数
  • fsub:从第一个浮点数中减去第二个浮点数
  • dsub:从第一个双精度数中减去第二个双精度数

乘法:

  • imul:两个整数相乘
  • lmul:两个长整数相乘
  • fmul:两个浮点数相乘
  • dmul:两个双精度数相乘

除法:

  • idiv:将第一个整数除以第二个整数
  • ldiv:将第一个长整数除以第二个长整数
  • fdiv:将第一个浮点数除以第二个浮点数
  • ddiv:将第一个双精度数除以第二个双精度数

余数和否定:

余数(remainder):

  • irem:计算第一个整数除以第二个整数的余数
  • lrem:计算第一个长整数除以第二个长整数的余数
  • frem:计算第一个浮点数除以第二个浮点数的余数
  • drem:计算第一个双精度数除以第二个双精度数的余数

否定(negation):

  • ineg:否定(改变符号)整数
  • lneg:否定长整数
  • fneg:否定浮点数
  • dneg:否定双精度数

移位和位操作:

深入了解位操作(ior、iand、ixor、lor、land、lxor)和移位操作(ishl、ishr、iushr、lshl、lshr、lushr)。了解这些操作如何操作单个位,为高级计算和优化提供强大工具:

移位操作(shift):

  • ishl、ishr、iushr:将整数的位向左、向右(带符号扩展)或向右(不带符号扩展)移位
  • lshl、lshr、lushr:将长整数的位向左、向右(带符号扩展)或向右(不带符号扩展)移位

位操作:

  • ior、lor:整数和长整数的按位或
  • iand、land:整数和长整数的按位与
  • ixor、lxor:整数和长整数的按位异或

本地变量增量:

解锁 iinc 指令的潜在潜力,这是一个微妙而强大的操作,它通过一个常量值增加本地变量。了解这个字节码指令如何在特定场景中提高代码的可读性和效率:

本地变量增量(iinc): iinc 通过一个常量值增加一个本地变量

比较操作:

深入了解使用 cmpg、dcmpl、fcmpg、fcmpl 和 lcmp 等指令比较值。揭示产生 1、-1 或 0 等结果的微妙之处,指示双精度数、浮点数和长整数的大、小或等于比较:

比较:

  • dcmpg、dcmpl:比较两个双精度数,产生 1、-1 或 0(大、小或等于)
  • fcmpg、fcmpl:比较两个浮点数,产生 1、-1 或 0(大、小或等于)
  • lcmp:比较两个长整数,产生 1、-1 或 0(大、小或等于)

在字节码比较的世界中,例如 dcmpg 和 dcmpl 这样的指令有效地比较双精度浮点数,产生 1、-1 或 0 来表示大、小或等于的比较。类似地,fcmpg 和 fcmpl 处理单精度浮点数。但是,当涉及到长整数时,lcmp 通过提供 1、-1 或 0 的单一结果,简化了事情,表示大、小或等于的比较。这种简化的方法优化了字节码中长整数的比较。

这些字节码指令构成了 Java 程序中算术和逻辑操作的基础。值得注意的是,这些操作的行为在整数和浮点数中可能有所不同,特别是在处理诸如除以零或溢出条件等边缘情况时。了解这些字节码指令为开发人员提供了在其 Java 应用程序中精确和健壮的数字计算的工具。

在解释了字节码的概念并展示了一些算术操作之后,我们将通过检查一个实际示例,深入探讨 JVM 中的字节码算术。我们的重点将放在通过分析一个简单的 Java 代码片段来理解该过程上,该代码片段执行了一个基本的算术操作,即添加两个整数。这个实践探索旨在揭示字节码算术的复杂工作。

考虑以下 Java 代码片段:

arduino 复制代码
public class ArithmeticExample {
    public static void main(String[] args) {
        int a = 5;
        int b = 7;
        int result = a + b;
        System.out.println("Result: " + result);
    }
}

将代码保存在名为 ArithmeticExample.java 的文件中,并使用以下命令编译它:

javac ArithmeticExample.java

现在,让我们使用 javap 命令来反汇编字节码:

arduino 复制代码
javap -c ArithmeticExample.class

执行命令后,将生成字节码的输出:

arduino 复制代码
public static void main(java.lang.String[]);
  Code:
     ...    
     5: iload_1       // 将 'a' 的值加载到堆栈上
     6: iload_2       // 将 'b' 的值加载到堆栈上
     7: iadd          // 将堆栈上的前两个值相加(即a和b)    
     8: istore_3      // 将结果存储到本地变量 'result'
     ...

在这些字节码指令中发生了以下操作:

  • iload_1:将局部变量 a 的值加载到堆栈上
  • iload_2:将局部变量 b 的值加载到堆栈上
  • iadd:将堆栈上的前两个值相加(即a和b)
  • istore_3:将加法的结果存储回一个本地变量以供进一步使用

这些字节码指令准确地反映了Java代码中的算术操作int result = a + b;。iadd指令执行加载的值的加法,istore_3指令将结果存储回一个本地变量以供进一步使用。了解这个字节码提供了对JVM如何在Java程序中执行简单的算术操作的详细了解。

在我们通过字节码算术的旅程中,我们剖析了在Java程序中添加两个整数这一看似平凡但深远影响的过程。字节码指令揭示了一个隐藏的复杂层,展示了高级操作如何在Java虚拟机(JVM)中转化为可执行的机器代码。

当我们结束这一部分时,我们的下一个目的地即将到来:值转换的领域。了解不同数据类型在字节码中的交互方式对于构建健壮且高效的Java应用程序至关重要。在即将到来的部分中,让我们深入了解值转换的微妙之处,揭示在JVM内部转换数据的细微差别。旅程在继续,每个字节码指令都让我们更接近掌握Java字节码的深度。

值转换

在这一部分,我们将深入探讨 JVM 内部复杂的值转换机制。这些转换操作就像字节码领域的变色龙一样,使得变量能够优雅地从一个类型转换为另一个类型,使得整数可以变成长整型,浮点数可以转换为双精度浮点数,而不会损失原始值的精度。这些字节码指令有助于保持精度,防止数据丢失,并确保不同数据类型之间的无缝集成。让我们一起解析这些指令,揭示支撑 Java 编程的优雅和精确的交响乐:

整数转换为长整型 (i2l):

探索 i2l 指令如何将整数变量提升为长整型,保持原始值的精度

整数转换为浮点数 (i2f):

深入了解 i2f,其中整数优雅地转换为浮点数,而不会牺牲精度

整数转换为双精度浮点数 (i2d):

通过 i2d 指令见证整数到双精度浮点数的精度保持之旅

长整型转换为浮点数 (l2f) 和长整型转换为双精度浮点数 (l2d):

研究 l2f 和 l2d 的优雅之处,其中长整型值无缝地转换为浮点数和双精度浮点数

浮点数转换为双精度浮点数 (f2d):

探索 f2d 指令,展示浮点数向双精度浮点数的提升,同时保持精度

在我们穿越字节码的复杂性时,我们会遇到一个关键的部分,专门用于管理缩短------这是一个标记着潜在损失和溢出风险的细致过程。在这个探索中,我们将深入研究将变量转换为较短数据类型的字节码指令,承认与精度损失和溢出风险相关的微妙挑战。现在让我们来探索这组指令:

整数转换为字节 (i2b)、整数转换为短整数 (i2s)、整数转换为字符 (i2c):

通过 i2b、i2s 和 i2c 指令研究整数转换为字节、短整数和字符类型时可能出现的精度损失

长整型转换为整数 (l2i):

通过 l2i 指令研究将长整型转换为整数时涉及的考虑因素,注意可能的溢出风险

浮点数转换为整数 (f2i)、浮点数转换为长整数 (f2l):

通过 f2i 和 f2l 指令揭示将浮点数转换为整数和长整数时的挑战,注意精度和溢出问题

双精度浮点数转换为整数 (d2i)、双精度浮点数转换为长整数 (d2l)、双精度浮点数转换为浮点数 (d2f):

通过 d2i、d2l 和 d2f 指令导航,理解将双精度浮点数转换为整数、长整数和浮点数时精度和潜在溢出之间的微妙平衡

在字节码的复杂性领域,以下最佳实践作为指南,指引我们考虑实际情况。在这里,我们将理论与应用程序相结合,探讨字节码指令对实际 Java 编程场景的具体影响。从在复杂算术运算中保持精度到导航面向对象设计的灵活。

在算术运算中保持精度:

将值转换与算术运算联系起来,确保在复杂计算中保持精度

处理对象引用:

探索值转换如何为面向对象编程的灵活性做出贡献,允许在类和接口之间实现平滑过渡

随着我们解析管理值转换的字节码指令,前述要点为您提供了理解和掌握 JVM 内部变量类型转换细微差别所需的见解。在下面的示例 Java 代码中,我们聚焦于值转换,明确关注在 JVM 中转换变量类型的惯例。代码片段展示了提升和考虑精度损失或溢出的微妙过程。在遍历字节码结果时,我们将注意力集中在将这些惯例实现的指令上:

csharp 复制代码
public class ValueConversionsExample {   
    public static void main(String[] args) {
        // 提升:类型扩大
        int intValue = 42;
        long longValue = intValue; // 提升:整数到长整型
        float floatValue = 3.14f;
        double doubleValue = floatValue; // 提升:浮点数到双精度浮点数
        // 缩短:考虑精度损失和溢出
        short shortValue = 32767;
        byte byteValue = (byte) shortValue; // 缩短:短整数到字节
        double largeDouble = 1.7e308;
        int intFromDouble = (int) largeDouble; // 缩短:双精度浮点数到整数
        // 结果显示如下:
        System.out.println("提升结果:" + longValue + ", " +           doubleValue);
        System.out.println("缩短结果:" + byteValue + ", " + 
          intFromDouble);
    }
}

希望这些信息有助于您理解和利用 JVM 中的值转换机制。

请将提供的Java代码保存在名为ValueConversionsExample.java的文件中。打开您的终端或命令提示符,并导航到保存该文件的目录。然后,使用以下命令编译代码:

javac ValueConversionsExample.java

编译后,您可以使用javap命令反汇编字节码并显示相关部分。在终端或命令提示符中执行以下命令:

arduino 复制代码
javap -c ValueConversionsExample.class

在这个分析中,我们专注于字节码的特定部分,探索 Java 代码如何转化为 JVM 中的可执行指令。我们的注意力集中在选定的字节码部分上,揭示了在 Java 编程领域中提升、精度考虑和缩短的复杂性。跟随我们一起解读 JVM 的语言,提供 Java 字节码形成的惯例的视觉叙述。

yaml 复制代码
0: bipush        42
3: istore_1
4: iload_1
5: i2l
6: lstore_2
7: ldc           3.14
9: fstore_4
10: fload         4
12: dstore_5
13: ldc           32767
15: istore         7
17: iload          7
19: i2b
20: istore         8
22: ldc2_w        #2                  // double 1.7e308
28: dstore         9
30: dload          9
32: d2i
33: istore         11

在这段 Java 代码中,我们见证了提升和缩短惯例的应用。字节码片段专注于与这些惯例相关的指令,详细展示了 JVM 如何处理变量类型的扩大和缩短。

在我们探索 JVM 内的值转换时,我们剖析了编排提升和考虑精度损失或溢出的字节码指令。这些细微之处突显了 Java 编程中数据类型的微妙演变。随着我们结束这一部分,高级代码到字节码的无缝转换变得更加清晰,揭示了 JVM 的精心编排。在下一节中,我们将把焦点转移到字节码中对象操作的迷人领域,解开编织 Java 面向对象范式的线索。在即将到来的旅程中,我们将深入研究塑造和操纵对象的字节码指令,深入探讨动态、多功能的 Java 编程的核心。

对象操作

在这个深入的会话中,我们将全面探索 Java 字节码复杂结构中的对象操作。我们的旅程揭示了创建和操作实例、构建数组以及访问类的静态和实例属性的字节码指令的重要性。我们审查了从数组中加载值、保存到堆栈、查询数组长度以及对实例或数组执行关键检查的指令。从基础的 new 指令到复杂的 multianewarray,每个字节码命令都将我们深入到面向对象操作的领域中。

在 Java 的字节码编织中,new 指令作为进入对象创建和操作领域的门户。它不仅为对象分配内存,还调用其构造函数,启动动态实体的诞生。加入我们深入研究字节码的复杂性,看似简单的 new 指令揭示了将 Java 对象带到生活的基本步骤。当我们剖析这个字节码命令时,内存分配和构造函数调用的基础交响乐变得更加清晰,为了更深入地理解 JVM 中的实例创建铺平了道路。

new:实例化一个新对象,分配内存并调用对象的构造函数。对新创建的对象的引用被放置在堆栈上。

在 Java 字节码的编排中,创建数组的命令呈现出多样性的编织。在这一部分中,我们深入研究了雕刻数组的字节码指令,为数据存储提供了一个动态的画布。从为原始类型创建的基础 newarray 到为对象引用创建的微妙 anewarray,以及为多维数组创建的复杂 multianewarray,每个字节码指令都为 JVM 中充满活力的数组生态系统做出了贡献。随着我们剖析这些命令,JVM 中的数组实例化的艺术性浮现,为了更深入地理解 Java 编程中的数据结构动态铺平了道路。

newarray:创建一个新的原始类型数组

anewarray:创建一个新的对象引用数组

multianewarray:创建一个多维数组

在 Java 字节码的复杂舞蹈中,访问类的静态或实例属性的命令 --- getfield、putfield、getstatic 和 putstatic --- 处于中心舞台。从优雅地检索实例字段值到动态地设置静态字段值,每个字节码指令都为面向对象编程的微妙编排做出了贡献。加入我们解开字节码访问的优雅之处,实例和类属性之间的微妙平衡展现出来,揭示了在 JVM 中管理数据操纵的基本机制。随着我们剖析这些指令,访问类属性的芭蕾舞蹈活跃起来,为深入理解 Java 编程中的面向对象复杂性铺平了道路。

getfield:从对象中检索实例字段的值

putfield:在对象中设置实例字段的值

getstatic:从类中检索静态字段的值

putstatic:在类中设置静态字段的值

在 Java 的字节码交响乐中,加载指令 --- baload、caload、saload、iaload、laload、faload、daload 和 aaload --- 处于中心舞台,定义了从数组中检索值的编排方式。在这一部分中,我们沉浸在优雅地将数组元素推向前台的节奏字节码命令中。从提取字节和字符到加载整数、长整数、浮点数、双精度数和对象引用,每个指令在数组和 JVM 之间的和谐互动中发挥着至关重要的作用。这些加载指令揭示了随着 Java 字节码无缝地穿越数组时展开的编排芭蕾舞蹈,展示了数组元素检索的多样性和精度。随着我们探索这些加载指令,从数组中加载值的复杂舞蹈活跃起来,提供了对 Java 编程流体动力学的更深入了解。

在 Java 的字节码杰作中,保存指令 --- bastore、castore、sastore、iastore、lastore、fastore、dastore 和 aastore --- 精细地指挥着数组操作的画布。这些指令对于将值存储到不同类型的数组中至关重要。让我们通过示例深入探讨它们的意义:

bastore:将字节或布尔值存储到字节数组中

castore:将字符值存储到字符数组中

sastore:将 short 值存储到 short 数组中

iastore:将整数值存储到整数数组中

lastore:将长整数值存储到长整数数组中

dastore:将双精度值存储到双精度数组中

这些指令在数组操作中发挥着基础作用,允许在数组中精确存储各种数据类型。

arraylength 指令在 Java 字节码中充当了指南针的作用,通过提供数组的长度来指导开发人员:

arraylength:检索数组的长度并将其推送到堆栈上。

在 Java 字节码的领域中,instanceof 和 checkcast 指令充当着警惕的守护者,确保对象类型的完整性及其与指定类的对齐。虽然我们先前的探索深入研究了数组操作,但现在让我们将焦点转移到这些指令在类型检查中的重要作用上。instanceof 评估对象是否属于特定类,为对象类型提供了关键洞察。另一方面,checkcast 细致地审查和转换对象,确保其与指定类的和谐对齐。这些字节码守护者共同在维护 JVM 中面向对象范式的健壮性和连贯性方面发挥着关键作用:

instanceof:检查对象是否是特定类的实例

checkcast:检查并将对象转换为给定类,确保类型兼容性

这些字节码指令为在 Java 中操作对象提供了基础,允许创建、访问和修改实例和数组。无论是实例化新对象、操作数组、访问类属性还是执行动态检查,每个指令都为 Java 字节码中面向对象编程的灵活性和强大性做出了贡献。理解这些指令对于掌握 Java 的对象操作功能至关重要。

在之前的讨论中,我们探索了关键的字节码指令,提供了对 Java 底层机制的深入理解。现在,让我们通过一个示例性的 Java 代码片段来将这些知识付诸实践。在这里,我们介绍了 Person 类的简化表示,重点关注一个属性:name。该类封装了对象操作的基本原则,包括用于访问和修改属性的方法。当我们浏览这个示例时,我们将深入探讨从这段代码生成的字节码,为了更好地理解 JVM 中的对象操作的低级细节。这些字节码指令支撑着 Java 编程的动态特性,这个实际示例将阐明它们在现实世界应用中的作用:

typescript 复制代码
public class Person {    
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String newName) {
        this.name = newName;
    }
    public static void main(String[] args) {
        // Creating an instance of Person
        Person person = new Person("John");
        // Accessing and displaying the name attribute
        System.out.println("Original Name: " + person.getName());
        // Changing the name attribute
        person.setName("Alice");
        // Displaying the updated name
        System.out.println("Updated Name: " + person.getName());
    }
}

编译并显示字节码:

arduino 复制代码
javac Person.java
javap -c Person.class

让我们专注于与对象操作对应的字节码的相关部分,包括对象创建(new)、属性访问(getfield、putfield)和方法调用:

yaml 复制代码
Compiled from "Person.java"
public class Person {
  private java.lang.String name;
  public Person(java.lang.String);
Code:

       0: aload_0         1: invokespecial #1                      // Method java/lang/
                                                                                                                                                                                                                                             Object."<init>":()V
       4: aload_0
       5: aload_1
       6: putfield        #2                      // Field name:Ljava/lang/
                                                                                                                                                                                                                                             String;
       9: return
  public java.lang.String getName();
Code:

       0: aload_0         1: getfield        #2                      // Field name:Ljava/lang/
                                                                                                                                                                                                                                             String;
       4: areturn
  public void setName(java.lang.String);
Code:

       0: aload_0         1: aload_1
       2: putfield        #2                      // Field name:Ljava/lang/
                                                                                                                                                                                                                                             String;
       5: return
  public static void main(java.lang.String[]);
Code:

       0: new             #3                      // class Person         3: dup
       4: ldc             #4                      // String John
       6: invokespecial   #5                      // Method "<init>":(Ljava/
                                                                                                                                                                                                                                             lang/String;)V
       9: astore_1
      10: getstatic       #6                      // Field java/lang/System.
                                                                                                                                                                                                                                             out:Ljava/io/PrintStream;
      13: ldc             #7                      // String Original Name:
      15: invokevirtual   #8                      // Method java/io/
                                                                                                                                                                                                                                             PrintStream.println:(Ljava/lang/String;)V
      18: getstatic       #6                      // Field java/lang/System.
                                                                                                                                                                                                                                             out:Ljava/io/PrintStream;
      21: aload_1
      22: invokevirtual   #9                      // Method getName:()Ljava/
                                                                                                                                                                                                                                             lang/String;
      25: invokevirtual   #8                      // Method java/io/
                                                                                                                                                                                                                                             PrintStream.println:(Ljava/lang/String;)V
      28: aload_1
      29: ldc             #10                     // String Alice
      31: invokevirtual   #11                     // Method setName:(Ljava/
                                                                                                                                                                                                                                             lang/String;)V
      34: getstatic       #6                      // Field java/lang/System.
                                                                                                                                                                                                                                             out:Ljava/io/PrintStream;
      37: ldc             #12                     // String Updated Name:
      39: invokevirtual   #8                      // Method java/io/
                                                                                                                                                                                                                                             PrintStream.println:(Ljava/lang/String;)V
      42: getstatic       #6                      // Field java/lang/System.
                                                                                                                                                                                                                                             out:Ljava/io/PrintStream;
      45: aload_1
      46: invokevirtual   #9                      // Method getName:()Ljava/
                                                                                                                                                                                                                                             lang/String;
      49: invokevirtual   #8                      // Method java/io/
                                                                                                                                                                                                                                             PrintStream.println:(Ljava/lang/String;)V
      52: return
}

让我们分解关键的字节码指令:

对象创建(new):

  • 0: new #3:创建一个 Person 类型的新对象
  • 3: dup:在堆栈上复制对象引用
  • 4: ldc #4:将常量字符串 "John" 推送到堆栈上
  • 6: invokespecial #5:调用构造函数 () 来初始化对象

属性访问(getfield、putfield):

  • 1: getfield #2:检索 name 字段的值
  • 2: putfield #2:设置 name 字段的值

方法调用:

  • 22: invokevirtual #9:调用 getName 方法
  • 31: invokevirtual #11:调用 setName 方法

这些字节码片段突出了与对象操作相关的基本指令,为我们提供了对 Java 编程在低级别字节码层面上动态性质的深入了解。

在简化的 Person 类的字节码编排中,我们揭示了指导对象创建、属性访问和方法调用的指令的编排。随着字节码交响乐的展开,我们顺畅地过渡到下一个部分,在那里我们将深入探讨方法调用和返回的动态领域。加入我们解密字节码指令,揭示方法调用本质的细微之处,为定义 JVM 中程序执行流的复杂性提供光明。随着我们的前进,方法调用和返回的探索将丰富我们对字节码交响乐的理解,揭示 Java 编程复杂性的下一层。

方法调用和返回

让我们踏上一场探索 Java 编程领域中方法调用和值返回微妙动态的旅程。我们将揭示动态调用方法和敏捷调用接口方法的细微差别,并探索调用私有或超类方法的独特和强大调音以及调用静态方法所产生的强烈音调。在这个探索过程中,我们将介绍动态构造的概念,展示 Java 编程的适应性。请记住,值返回的节奏由特定的指令定义。

在 Java 字节码中探索方法调用的交响乐中,以下指令编排了方法调用旋律中的各种音符,每个指令都为语言的动态和多态行为做出了独特贡献:

  • invokevirtual:开启方法调用旋律,此指令从实例中调用方法,在 Java 中提供了动态和多态行为的支柱
  • invokeinterface:添加和谐音符,此指令从接口中调用方法,为 Java 的面向对象范式的灵活性和适应性做出了贡献
  • invokespecial:引入独特和强烈音符,此指令调用私有或超类方法,封装了特权方法调用
  • invokestatic:发出强大音调,此指令调用静态方法,强调不依赖于实例创建的方法调用
  • invokedynamic:奏响多才多艺的旋律,此指令动态构造对象,展示了 Java 方法调用的动态能力

方法执行的节奏由返回指令(ireturn、lreturn、freturn、dreturn 和 areturn)补充,定义了方法返回的韵律。在出现异常的情况下,athrow 调用成为焦点,管理着错误处理的编排。

我们进一步探索了同步方法的复杂性,其中由 ACC_SYNCHRONIZED 标志标记的监视器 orchestrate 了一场受控舞蹈。通过 monitorenter 指令,方法进入监视器,确保了独占式执行,并在完成后以 monitorexit 优雅地退出,为字节码交响乐中的同步交响曲编织出了一个和谐的交响乐。现在让我们深入到 Java 代码中方法调用和返回的实时演示中。以下是一个通过方法调用执行计算的简单 Java 程序。然后,我们将仔细研究字节码,解读这些方法调用的编排:

java 复制代码
public class MethodCallsExample {    
    public static void main(String[] args) {
        int result = performCalculation(5, 3);
        System.out.println("Result of calculation: " + result);
    }
    private static int performCalculation(int a, int b) {
        int sum = add(a, b);
        int product = multiply(a, b);
        return subtract(sum, product);
    }
    private static int add(int a, int b) {
        return a + b;
    }
    private static int multiply(int a, int b) {
        return a * b;
    }
    private static int subtract(int a, int b) {
        return a - b;
    }
}

编译并显示字节码:

arduino 复制代码
javac MethodCallsExample.java
javap -c MethodCallsExample.class

在字节码中,我们将专注于与方法调用相关的指令(invokevirtual、invokespecial、invokestatic)以及返回指令(ireturn)。 以下是从 MethodCallsExample.java 编译的简化摘录:

yaml 复制代码
public class MethodCallsExample {  public MethodCallsExample();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/
                                            Object."<init>":()V
       4: return
  public static void main(java.lang.String[]);
Code:

       0: iconst_5       1: iconst_3
       2: invokestatic  #2                  // Method 
                                            performCalculation:(II)I
       5: istore_1
       6: getstatic     #3                  // Field java/lang/System.
                                            out:Ljava/io/PrintStream;
       9: new           #4                  // class java/lang/
                                            StringBuilder
      12: dup
      13: ldc           #5                  // String Result of 
                                            calculation:
      15: invokespecial #6                  // Method java/lang/
                          StringBuilder."<init>":(Ljava/lang/String;)V
      18: iload_1
      19: invokevirtual #7                  // Method java/lang/
                     StringBuilder.append:(I)Ljava/lang/StringBuilder;
      22: invokevirtual #8                  // Method java/lang/
                           StringBuilder.toString:()Ljava/lang/String;
      25: invokevirtual #9                  // Method java/io/
                             PrintStream.println:(Ljava/lang/String;)V
      28: return
  private static int performCalculation(int, int);
Code:

       0: iload_0       1: iload_1
       2: invokestatic  #2                  // Method 
                                            performCalculation:(II)I
       5: iload_0
       6: iload_1
       7: invokestatic  #11                 // Method multiply:(II)I
      10: invokestatic  #12                 // Method subtract:(II)I
      13: ireturn
  private static int add(int, int);
Code:

       0: iload_0       1: iload_1
       2: iadd
       3: ireturn
  private static int multiply(int, int);
Code:

       0: iload_0       1: iload_1
       2: imul
       3: ireturn
  private static int subtract(int, int);
Code:

       0: iload_0       1: iload_1
       2: isub
       3: ireturn
}

这个字节码摘录展示了与方法调用和返回相关的基本指令,为我们提供了一个窥视所提供的 Java 代码的字节码交响乐的机会。随着我们结束对 Java 字节码中方法调用和返回的探索,我们揭示了编排程序流的指令的复杂舞蹈。invokevirtual、invokeinterface、invokespecial 和 invokestatic 的交响乐在我们的字节码交响乐中回响,展示了方法调用的动态性质和值的节奏性返回。随着我们转向下一节,聚光灯将转向条件指令,其中字节码决策塑造了程序执行的路线。加入我们,解读条件语句的字节码细节,揭示 JVM 在由条件定义的路径中引导的逻辑,并继续我们对 Java 编程复杂性的深入探索之旅。

条件指令

在这一部分,我们深入探讨了 JVM 中条件指令的微妙领域,揭示了决策制定的复杂性。这些指令构成了条件语句的支柱,根据布尔结果指导 JVM 通过路径。让我们一起解读条件指令的字节码细节,揭示在 JVM 中动态塑造程序流的逻辑。这个探索提供了洞察 Java 编程中条件语句执行的基本原则。

在 Java 字节码的领域中,我们发现了一组指令,它们精密地控制着 JVM 中的条件逻辑。这些命令是条件语句的设计者,根据布尔结果编排精确的决策,并影响程序流程。这个探索揭示了字节码细节的层层剥离,提供了关于 JVM 中这些基本指令塑造的动态路径的见解。

让我们探索一组在 Java 中具有独特控制程序流和决策制定能力的字节码指令。这些指令涵盖了一系列条件、开关和跳转,每个指令在指导 Java 程序执行路径中扮演着独特的角色:

  • ifeq:如果堆栈顶部的值等于 0,则跳转到目标指令
  • ifne:如果堆栈顶部的值不等于 0,则跳转到目标指令
  • iflt:如果堆栈顶部的值小于 0,则跳转到目标指令
  • ifle:如果堆栈顶部的值小于或等于 0,则跳转到目标指令
  • ifgt:如果堆栈顶部的值大于 0,则跳转到目标指令
  • ifge:如果堆栈顶部的值大于或等于 0,则跳转到目标指令
  • ifnull:如果堆栈顶部的值为 null,则跳转到目标指令
  • ifnonnull:如果堆栈顶部的值不为 null,则跳转到目标指令
  • if_icmpeq:如果堆栈上的两个整数值相等,则跳转到目标指令
  • if_icmpne:如果堆栈上的两个整数值不相等,则跳转到目标指令
  • if_icmplt:如果堆栈上的第二个整数值小于第一个,则跳转到目标指令
  • if_icmple:如果堆栈上的第二个整数值小于或等于第一个,则跳转到目标指令
  • if_icmpgt:如果堆栈上的第二个整数值大于第一个,则跳转到目标指令
  • if_icmpge:如果堆栈上的第二个整数值大于或等于第一个,则跳转到目标指令
  • if_acmpeq:如果堆栈上的两个对象引用相等,则跳转到目标指令
  • if_acmpne:如果堆栈上的两个对象引用不相等,则跳转到目标指令
  • tableswitch:提供了更有效的方法来实现具有连续整数 case 的开关语句
  • lookupswitch:类似于 tableswitch,但支持稀疏 case 值
  • goto:无条件地跳转到目标指令
  • goto_w:无条件地跳转到目标指令(宽索引)
  • jsr:跳转到子例程,将返回地址保存在堆栈上
  • jsr_w:跳转到子例程(宽索引),将返回地址保存在堆栈上
  • ret:从子例程返回,使用前一个 jsr 指令保存的返回地址

这些指令在构建条件语句和根据各种条件和比较控制程序执行流程中发挥着关键作用。理解它们的行为是解读和优化 Java 字节码的关键。

现在让我们探索 Java 中的条件逻辑领域,在这里我们的代码成为决策的画布。在这个示例中,我们精心设计了一个简单的 Java 程序,其中包含条件语句。该代码检查两个整数和两个字符串之间的关系,并根据相等条件引导流程。当我们深入探讨这个 Java 片段的字节码表示时,我们将解读底层的指令,包括 if_icmpne 和 if_acmpeq,负责将程序引导到不同的路径。加入我们,解开 Java 代码和字节码之间动态相互作用的探索,塑造了 JVM 内逻辑决策的结果。

csharp 复制代码
  public class ConditionalExample {    
    public static void main(String[] args) {
        int a = 5;
        int b = 3;
        if (a == b) {
            System.out.println("a is equal to b");
        } else {
            System.out.println("a is not equal to b");
        }
        String str1 = "Hello";
        String str2 = "Hello";
        if (str1.equals(str2)) {
            System.out.println("Strings are equal");
        } else {
            System.out.println("Strings are not equal");
        }
    }
}

编译并显示字节码:

arduino 复制代码
javac ConditionalExample.java
javap -c ConditionalExample.class

在字节码中,我们将关注条件指令,如ifeq、ifne和if_acmpeq,它们根据相等条件处理分支。以下是一个简化的摘录: 从 ConditionalExample.java 编译

yaml 复制代码
public class ConditionalExample {  public ConditionalExample();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/
                                            Object."<init>":()V
       4: return
  public static void main(java.lang.String[]);
Code:

       0: iconst_5       1: istore_1
       2: iconst_3
       3: istore_2
       4: iload_1
       5: iload_2
       6: if_icmpne     19
       9: getstatic     #2                  // Field java/lang/System.
                                            out:Ljava/io/PrintStream;
      12: ldc           #3                  // String a is equal to b
      14: invokevirtual #4                  // Method java/io/
                             PrintStream.println:(Ljava/lang/String;)V
      17: goto          32
      20: getstatic     #2                  // Field java/lang/System.
                                            out:Ljava/io/PrintStream;
      23: ldc           #5                  // String a is not equal 
                                            to b
      25: invokevirtual #4                  // Method java/io/
                             PrintStream.println:(Ljava/lang/String;)V
      28: goto          32
      31: astore_3
      32: ldc           #6                  // String Hello
      34: astore_3
      35: ldc           #6                  // String Hello
      37: astore        4
      39: aload_3
      40: aload         4
      42: if_acmpeq     55
      45: getstatic     #2                  // Field java/lang/System.
                                            out:Ljava/io/PrintStream;
      48: ldc           #7                  // String Strings are not 
                                            equal
      50: invokevirtual #4                  // Method java/io/
                             PrintStream.println:(Ljava/lang/String;)V
      53: goto          68
      56: getstatic     #2                  // Field java/lang/System.
                                            out:Ljava/io/PrintStream;
      59: ldc           #8                  // String Strings are equal
      61: invokevirtual #4                  // Method java/io/
                             PrintStream.println:(Ljava/lang/String;)V
      64: goto          68
      67: astore        5
      69: return
    [...]
}

这段字节码摘录展示了条件指令(if_icmpne 和 if_acmpeq)的运行情况,根据 Java 代码中指定的相等条件引导程序流程。

在这次对 Java 字节码的探索中,我们解读了塑造 JVM 内部逻辑的条件指令的复杂运作。从基于相等性的分支到无条件跳转,这些字节码指令引导了决策过程。随着我们结束了条件指令的这一部分,地平线扩展,引领我们进入下一段旅程。即将到来的部分将深入探讨整个类在字节码中的表示,解开了封装 Java 程序本质的指令层次。加入我们,完成这次转换,在字节码中,我们的关注从孤立的条件扩展到类的整体视图,揭示 Java 运行时环境的内部工作原理。

展示给我字节码

随着我们对Java字节码的探索继续,我们现在将目光投向整个类,深入了解Java程序的二进制表示。值得注意的是,我们检查的字节码可能会根据JVM版本和具体的JVM供应商而有所不同。在这个过程中,我们揭示编译和检查字节码的复杂性,这些字节码包含了完整Java类的本质。从类的初始化到方法的实现,类的每个方面都体现在字节码中。让我们一起揭开Java程序整体视图的面纱,探索我们的代码如何转化为JVM所理解的语言的微妙之处。 在我们的Java字节码之旅中,让我们从创建一个简单而多才多艺的Animal类开始。以下是定义该类的Java代码片段:

typescript 复制代码
public class Animal {    
    private String name;
    public String name() {
        return name;
    }
    public int age() {
        return 10;
    }
    public String bark() {
        return "woof";
    }
}

现在,让我们通过编译和查看字节码的过程:

javac Animal.java
javap -verbose Animal

有了这个,我们开始了一次引人入胜的探索,编译我们的Java类并揭示隐藏在表面下的字节码的复杂性。让我们解码JVM的语言,照亮我们Animal类的字节码表示。 字节码输出的这一部分提供了关于编译类文件的元数据。让我们分解一下关键信息:

最后修改时间:指示类文件上次修改的日期;在本例中,为2023年11月16日。

大小:指定类文件的大小(以字节为单位),在本例中为433字节。

SHA-256校验和:表示类文件的SHA-256校验和。此校验和用作文件的唯一标识符,并确保其完整性。

编译自"Animal.java":通知我们此字节码是从源文件Animal.java编译而来的。

类声明:声明名为Animal的类。

版本信息: 次要版本:设置为0。 主要版本:设置为65,表示与Java 11兼容。

标志:显示应用于类的访问控制修饰符的十六进制标志。在本例中,这是一个公共类(ACC_PUBLIC),具有额外属性(ACC_SUPER)。

类层次结构: this_class:指向表示当前类Animal的常量池索引(#8)。

super_class:指向表示超类java/lang/Object的常量池索引(#2)。

接口、字段、方法和属性:提供了类中这些元素的计数。

此元数据提供了类文件属性的快照,包括其版本、访问修饰符和结构细节。

字节码输出中的常量池部分提供了对常量池的一瞥,常量池是一个表格结构,用于存储各种常量,如字符串、方法和字段引用、类名等等。让我们解读这个常量池中的条目:

常量池: - #1 = Methodref #2.#3 // java/lang/Object."":()V - #2 = Class #4 // java/lang/Object - #3 = NameAndType #5:#6 // "":()V - #4 = UTF-8 java/lang/Object - #5 = UTF-8 - #6 = UTF-8 ()V - #7 = Fieldref #8.#9 // Animal.name:Ljava/lang/String; - #8 = Class #10 // Animal - #9 = NameAndType #11:#12 // name:Ljava/lang/String; - #10 = UTF-8 Animal - #11 = UTF-8 name - #12 = UTF-8 Ljava/lang/String; - #13 = Fieldref #8.#14 // Animal.age:I - #14 = NameAndType #15:#16 // age:I - #15 = UTF-8 age - #16 = UTF-8 I - #17 = String #18 // woof - #18 = UTF-8 woof - #19 = UTF-8 Code - #20 = UTF-8 LineNumberTable - #21 = UTF-8 ()Ljava/lang/String; - #22 = UTF-8 ()I - #23 = UTF-8 bark - #24 = UTF-8 SourceFile - #25 = UTF-8 Animal.java

这里会显示字段的引用:

  • 对Object构造函数的方法引用: - #1 = Methodref #2.#3 // java/lang/Object."":()V 这个条目引用了java/lang/Object类的构造函数,表示为。它指示了每个类从Object类隐式继承的初始化方法。
  • 对Object类的类引用: - #2 = Class #4 // java/lang/Object 指向java/lang/Object的类引用,表示Animal类扩展自Object。
  • Object构造函数的名称和类型: - #3 = NameAndType #5:#6 // "":()V 指定了构造函数()的名称和类型,没有参数并返回void。
  • Object类名的UTF-8条目: - #4 = UTF-8 java/lang/Object 用UTF-8编码表示java/lang/Object类的名称。
  • 构造函数和参数类型的UTF-8条目: - #5 = UTF-8 - #6 = UTF-8 ()V 用UTF-8编码表示构造函数的名称()和其类型(无参数,返回void)。
  • 对Animal的name字段的字段引用: - #7 = Fieldref #8.#9 // Animal.name:Ljava/lang/String; 引用Animal类中的name字段,其类型为java/lang/String。
  • 对Animal类的类引用: - #8 = Class #10 // Animal 指向Animal类的类引用。
  • 对name字段的名称和类型: - #9 = NameAndType #11:#12 // name:Ljava/lang/String; 指定了name字段的名称和类型:其名称(name)和类型(String)。
  • Animal类名的UTF-8条目: - #10 = UTF-8 Animal 用UTF-8编码表示Animal类的名称。
  • name字段的UTF-8条目: - #11 = UTF-8 name - #12 = UTF-8 Ljava/lang/String; 用UTF-8编码表示name字段的名称和类型(String)。

类似的条目还存在于age字段和bark方法,引用字段和方法名称、它们的类型以及常量池中的类名。总的来说,常量池是解析字节码执行过程中符号引用的关键组成部分。

提供的字节码片段代表了Animal类中的方法。让我们逐个分析每个方法:

构造方法(public Animal();):

  • 描述符:()V(无参数,返回void)
  • 标志:ACC_PUBLIC(公共方法)
  • 代码:
makefile 复制代码
javaCopy code
stack=1, locals=1, args_size=1   
0: aload_0   
1: invokespecial #1 // Method java/lang/Object."<init>":()V   
4: return

这个构造函数通过调用其父类(Object)的构造函数来初始化Animal对象。aload_0指令将对象引用(this)加载到堆栈上,invokespecial调用了超类构造函数。LineNumberTable表明此代码对应于源文件中的第1行。

公共方法java.lang.String name();:

  • 描述符:()Ljava/lang/String;(无参数,返回String)
  • 标志:ACC_PUBLIC(公共方法)
  • 代码:
makefile 复制代码
javaCopy code
stack=1, locals=1, args_size=1   
0: aload_0   
1: getfield #7 // Field name:Ljava/lang/String;   
4: areturn

name()方法检索name字段的值并返回。aload_0加载对象引用(this),getfield获取name字段的值。LineNumberTable表明此代码对应于源文件中的第9行。

公共方法int age();:

  • 描述符:()I(无参数,返回int)
  • 标志:ACC_PUBLIC(公共方法)
  • 代码:
makefile 复制代码
javaCopy code
stack=1, locals=1, args_size=1   
0: aload_0   
1: getfield #13 // Field age:I   
4: ireturn

name方法类似,这个方法检索age字段的值并返回。getfield获取值,ireturn返回值。LineNumberTable表明此代码对应于源文件中的第13行。

公共方法java.lang.String bark();:

  • 描述符:()Ljava/lang/String;(无参数,返回String)
  • 标志:ACC_PUBLIC(公共方法)
  • 代码:
ini 复制代码
javaCopy code
stack=1, locals=1, args_size=1   
0: ldc #17 // String woof   
2: areturn

bark()方法直接返回字符串woof而不访问任何字段。ldc加载常量字符串,areturn返回它。LineNumberTable表明此代码对应于源文件中的第17行。

这些字节码片段封装了Animal类中每个方法的逻辑,展示了方法执行期间执行的低级操作。

在Java字节码中,每个变量和方法参数都被分配了一个类型描述符,以指示其数据类型。这些描述符是紧凑的表示,用于传达有关变量类型或参数的信息。以下是详细解释:

  • B(byte):表示带符号的8位整数
  • C(char):表示Unicode字符
  • D(double):表示双精度浮点值
  • F(float):表示单精度浮点值
  • I(int):表示32位整数
  • J(long):表示64位长整数
  • L Classname(引用):指向指定类的实例;完全限定的类名跟随L并以分号结尾
  • S(short):表示16位短整数
  • Z(Boolean):表示布尔值(true或false)
  • [(数组引用):表示数组。数组元素的类型由跟随[的额外字符确定

对于数组引用:

  • [L Classname:表示指定类的对象数组
  • [[B:表示二维字节数组

在检查与Java类中的方法声明、字段定义和变量使用相关的字节码指令时,这些类型描述符至关重要。它们使得能够在Java程序的低级字节码表示中简洁地表示数据类型。

相关推荐
秋意钟6 分钟前
Spring新版本
java·后端·spring
椰椰椰耶8 分钟前
【文档搜索引擎】缓冲区优化和索引模块小结
java·spring·搜索引擎
mubeibeinv9 分钟前
项目搭建+图片(添加+图片)
java·服务器·前端
青莳吖11 分钟前
Java通过Map实现与SQL中的group by相同的逻辑
java·开发语言·sql
Buleall18 分钟前
期末考学C
java·开发语言
重生之绝世牛码20 分钟前
Java设计模式 —— 【结构型模式】外观模式详解
java·大数据·开发语言·设计模式·设计原则·外观模式
小蜗牛慢慢爬行26 分钟前
有关异步场景的 10 大 Spring Boot 面试问题
java·开发语言·网络·spring boot·后端·spring·面试
新手小袁_J1 小时前
JDK11下载安装和配置超详细过程
java·spring cloud·jdk·maven·mybatis·jdk11
呆呆小雅1 小时前
C#关键字volatile
java·redis·c#
Monly211 小时前
Java(若依):修改Tomcat的版本
java·开发语言·tomcat