`
Mysun
  • 浏览: 273448 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

java方法调用过程解析和执行--编译器的处理

阅读更多
本文尝试对java在编译器和运行期如何处理程序代码中的方法调用表达式进行描述,本文的大部分内容来自于java语言规范3.0.
由于java动态语言的特性,因此它在编译期和运行期都需要对程序代码中的方法调用表达式进行处理。其中对方法调用表达进行处理的大部分工作是在编译期完成的,而运行期的大部分工作则是对编译完成的方法调用表达式进行有效性检查。
编译期完成的处理
在编译期,java需要对方法调用表达式进行检查和分析,如果表达式能够被正确地解释,那么java的编译器需要将有关方法调用的元数据记录到字节码文件中,为jvm能够在运行期正确地执行方法调用提供足够多的信息。java编译器要完成的主要工作可以用表1中的伪代码来表示。
根据表达式确定要在那些类和接口中搜索方法;
if 找到了一个或者多个类和接口
    尝试从这些类和接口中找到所有潜在可用方法定义;
    if 找到了一个或者多个可用的方法定义
        从这些方法定义中找出与方法调用表达式最为匹配的一个方法定义;
        if 找到了一个最为匹配的方法
            对方法调用的语法进行检查;
            if 方法调用具有正确的语法
                将运行期需要的相关信息写入字节码中;
            else
                编译器产生错误;
        else
            编译器产生错误;
    else
        编译器产生错误;
else
    编译器产生错误;

在本文后面的叙述中,大家会发现,编译器的大部分精力需要花在为一个方法调用表达式确定一个最为匹配的方法定义,而这一工作之所以会被复杂化则是由于java对泛型的引入。另外,对于java来说,在确定需要调用哪个方法时,方法的返回值的确非常的不重要,通过后面的叙述大家也可以看到,java将方法的返回值排除在方法签名之外确实是有道理的。
确定要在哪些类与接口中搜索方法定义
我们知道,在java这样的面向对象语言中,方法是定义在类或者接口中的。所以,当java编译器面对一个方法调用表达式时,为了找到被调用的这个方法,首先要做的就是找到可能会定义这个方法的类或者接口。编译器会根据方法调用表达式的形式来确定要搜索的类和接口,表2列出了相关信息。表中列出的方法调用形式涵盖了我们可以在java中使用的所有方法调用形式。在这一步中找到的类和接口将作为后面进步进行解析的输入,最终被确定的方法将在这一步中找到的类或者接口中产生。另外值得一提的是,在搜索备选的类或者接口时,编译器是根据方法名来查找的,方法参数的个数及其类型在这一步是用不到的。
编号表达式形式要搜索的类和/或接口
1methodName(args) 对于这种形式的方法调用,如果是正确的,那么对于这条语句所在的作用域来说,必然会有一个可见的方法定义。这个方法定义可能是在类中,也可能是在内部类中,那么要搜索的类或者接口就是定义了这个方法的最内层的类或者接口。简单来说,我们要找的类或者接口就是包含这条语句的类及其外部类。
2TypeName[1].methodName(args) 对于这种形式的方法调用,要搜索的类就是TypeName所定义的那个类,如果TypeName定义的是一个接口,编译器会给出编译错误。因为这种类型的调用指定针对静态方法,而接口是不能有任何形式的方法定义的,它只能包含方法的声明。
3Primary.<NonWildTypeArguments>methodName(args) Primary是java规范中定义的初级表达式,用来构成其他更加复杂的表达式(参见java规范15.8节)。如果Primary表达式的结果是一个接口或者类,那么这个接口或者类就是我们要搜索的。如果Primary是一个类型参数T,那么要搜索的接口或者类就是T的上届。
4super.<NonWildTypeArguments>methodName(args) 在这种情况下,要搜索的类是包含了这条方法调用语句的类的超类。假设包含这条方法调用语句的类是T,如果T是Object(Object是没有超类得)或者T是一个接口(接口是不能定义方法的),那么编译器会给出编译错误。
5ClassName[1].super.<NonWildTypeArguments>methodName(args) 这种类型的调用语句,要搜索的类式ClassName代表的那个类的超类。如果包含这条语句的类不是ClassName代表的那个类的子类,编译器会给出错误。如果ClassName代表的那个类式Object,编译器也会给出错误。另外,如果包含这条语句的类是Object或者是一个接口,那么编译器同样会给出错误。
6FieldName.methodName(args) 这里FieldName表示类的属性,要搜索的类就是这个属性名所代表的那个类。如果FieldName代表的是一个类型参数,那么要搜索的类是这个类型参数的上届。
7TypeName.<NonWildTypeArguments>methodName(args)同2

确定方法签名
在这一步中,编译器会根据被调用的方法签名(方法名加上参数),在上一步得到的接口或者类中搜索可用的所有方法(方法定义符合调用语句中给出的参数个数及其类型)。如果可能用的方法有多个,那么编译器必须决定哪个方法定义是最符合调用语句期望的(chose the most specific method)。
考虑到兼容性的原因,搜索可用方法的过程被却分为三个阶段:
  • 在第一个阶段中,编译器在不允许自动装箱和解箱以及不考虑变长参数列表的情况下,执行方法搜索;
  • 在第二个阶段中,编译器在不考虑变长参数列表,但是允许自动装箱和解箱的情况下,执行方法搜索;
  • 在第三个阶段中,编译器在允许自动装箱和解箱以及变长参数列表的情况下,执行方法搜索;

编译器从第一个阶段开始执行,当第一个阶段完成之后,如果发现没有找到任何可用的方法定义,那么就会执行第二个阶段,以此类推。在任何一个阶段中,如果找到了一个以上的可用方法定义,就不会继续执行后面的阶段。
在每个阶段中,java都会试图找出符合以下要求的所有方法定义:
  • 定义的方法名与调用语句中的方法名一致(大小写敏感的);
  • 方法定义对于方法调用语句来说是可以见的;
  • 方法定义中的参数个数必须小于等于方法调用语句中提供的参数个数(变长参数被视为一个方法参数);
  • 如果方法定义的参数列表中包含了变长参数,假设方法定义有n个参数(变长参数被视为一个方法参数),那么方法调用语句提供的参数列表长度必须大于等于n-1;
  • 如果方法定义不包含变长参数,假设方法定义有n个参数,那么方法调用语句提供的参数列表的长度也必须是n;
  • 如果方法调用语句包含了显示的类型参数,并且方法声明也是泛型的,那么方法调用语句中的真实类型参数个数必须与方法声明中的形式化类型参数的格式保持一致;
  • 方法调用语句中参数列表中的参数类型必须依次能够与方法声明中参数列表的参数类型匹配;

从上面的定义中不难发现,java是允许泛型方法调用匹配到非泛型方法声明上的,这是兼容性和可替换性的要求。兼容性比较好理解,如果我们用使用了泛型的代码,调用一个用较早版本写出来的类库,这种调用也是应该成功的。可替换性是针对子类化和接口实现而言的,因为子类或者实现类可以将超类或者接口中的泛型方法覆写(override)成非泛型的方法,为了保证超类能用的地方子类也必然能用的这条规则,所以允许泛型方法调用匹配非泛型方法声明。
在上述几点中,最后一点,也就是关于方法参数类型匹配的过程相较其他几点要复杂一些。因为在Java中允许子类化、允许使用表达式的值作为方法的参数、允许变长参数,另外还有泛型参与其中,导致了处理上的复杂。在java规范中对此有比较详细的说明,因为多是数学上的一些推导,这里就不累述了。相关参见java规范15.12.2.2-15.12.2.4节内容。
重载方法决断
当编译器发现有多个方法都可以满足方法调用表达式时,这个时候就要选择一个方法声明作为运行时正真调用的方法。当然,Java规范对这个过程也有详细的数学定义,这里也就不说了。相关内容可以参见java规范15.12.2.5节
这里需要提一下对抽象方法(abstract method)的特殊处理,如果编译器发现了的多个最合适的方法,但是其中只有一个是非抽象方法,那么这个非抽象方法就是运行时要调用的方法。如果所有的方法都是抽象的,那么编译器会从具有最具体(most specific)返回类型的子集中随机选择一个来作为运行时调用的方法。所以,对于抽象方法的定义还是要注意一些。
关于类型推导
从1.5开始,java引入了泛型,这给编译器处理方法调用增加了不少的复杂度,其中很大一部分来自方泛型方法调用时候的类型推导。关于这个部分java规范上内容颇多,而且都是一些数学推导和定义,对平日代码编写也没有特别大的用处,这里就详细讲述了,有兴趣可以看一下java规范上的相关描述
扫尾处理
编译器在为一个方法调用语句选择了一个最合适的方法声明之后,会将相关信息写入class文件,这个方法声明便被称为编译时方法声明(compile-time declaration)。之后编译器还会进行进一步的校验。比如一个实例方法被放到一个静态上下文中调用,或者super.methodName这样的调用最后被发现找到的是一个抽象方法,又或者返回类型为void的方法被放到了负值语句或者被当作其他方法调用的参数,这个时候编译器都会给出编译期的异常。
分享到:
评论
发表评论

文章已被作者锁定,不允许评论。

相关推荐

    groovy和Java相互调用1

    Java调用Groovy类则稍微复杂一些,通常有两种主要方法: 1. **方法1:直接调用** - 如果你的开发环境(如IntelliJ IDEA或Eclipse)已经安装了Groovy解释器插件,那么可以直接在Java代码中像调用Java类一样调用...

    protobuf--java-3.2.0.jar & protoc-3.2.0-windows-x86_32.exe

    标题中的"protobuf--java-3.2.0.jar & protoc-3.2.0-windows-x86_32.exe" 提及了两个关键组件,它们分别是Protocol Buffers(protobuf)的Java运行库和编译器。Protocol Buffers是Google开发的一种数据序列化协议,...

    Java小型编译器

    10. 主函数:Java小型编译器中的main()方法是程序的入口点,负责调用词法分析的过程。 11. 词法分析的结果:Java小型编译器的词法分析结果是一个Token流,表示源代码中的词汇单元。 12. 词法分析的应用:Java小型...

    论文研究-IDL3.0编译器符合性测试研究和设计.pdf

    8. 开发和测试环境:文档提到的IDL到C++的编译器服务器、开发程序头文件、存根文件、骨架文件、实现文件以及RPC(远程过程调用)服务器的执行程序等,都涉及到IDL3.0编译器测试环境的搭建。这些环境的建立对于执行...

    java小型basic编译器

    Java小型Basic编译器是一种基于Java编程语言实现的解释器,用于处理和执行Basic语言的源代码。Basic语言,全称Beginner's All-purpose Symbolic Instruction Code,是为初学者设计的一种简单易学的编程语言。它具有...

    C#调用JAVA方法

    通常,你会看到如何加载Java类、创建Java对象,以及如何执行方法调用。 例如,你可能会看到类似以下的代码片段: ```csharp using IKVM.Runtime; using java.lang; // 导入Java类 [JavaType(...

    基于java实现的C语言编译器【100012136】

    在本项目中,"基于Java实现的C语言编译器【100012136】"是一个课程设计任务,目标是构建一个能够解析并处理C语言源代码的编译器。这个编译器的实现采用了Java编程语言,这使得它具有跨平台的能力,能够在多种操作...

    java编译器

    Java编译器是Java编程语言的核心工具之一,它将程序员编写的人类可读的源代码(.java文件)转换为计算机可执行的字节码(.class文件)。这个过程被称为编译,它是Java程序生命周期中的关键步骤。Java的编译器被称为...

    Java编译器源代码

    3. **语义分析**:检查程序的语义,如类型匹配、变量声明、方法调用等,确保程序符合Java语言规范。 4. **代码生成**:将抽象语法树转换为字节码,字节码是一种二进制格式,能够被Java虚拟机(JVM)理解和执行。 5...

    C#写的java编译器

    此外,它还具有编译和执行Java文件的功能,这表明它包含了一个Java源码到字节码的转换过程,类似于Oracle的Javac编译器。 在学习这个项目的过程中,开发者可以深入理解以下知识点: 1. **C#基础**:熟悉C#语法、...

    C语言实现的Java编译器

    在本项目中,"C语言实现的Java编译器"是一个独特的尝试,它将传统的编译原理理论应用于实际编程中,以C语言作为基础来构建一个能够解析和处理Java源代码的工具。这个项目主要涉及两个核心部分:词法分析器(Scanner...

    Java调用SPSS的实例

    总的来说,Java调用SPSS是一项实用的技术,它允许开发者利用Java的强大功能和SPSS的统计分析能力,实现数据处理和分析的自动化。不过,由于SPSS的语法有一定的学习曲线,开发者需要对SPSS有一定程度的了解,并熟悉其...

    Java表达式语法解析库 parboiled

    这使得解析结果易于遍历和处理,适合进行编译器或解释器的实现。 - 通过`createParseTree`标志,可以开启Parse Tree模式,然后使用`parseTree`方法获取解析树。 Parboiled还支持错误处理和回溯机制。当解析失败时...

    Java调用Groovy,实时动态加载数据库groovy脚本

    Java作为一种广泛使用的静态类型编程语言,有时候可能无法满足所有需求,特别是在处理动态代码执行和热部署时。此时,Groovy这种基于JVM的动态语言就发挥了重要作用。本文将详细讲解如何在Java应用程序中调用Groovy...

    PL/0编译器(Java完整版)

    再者,提到main方法位于Parser类中,这意味着在这个编译器的设计中,解析源代码和执行程序的入口点被集成在一起。在传统的编译器设计中,解析器通常负责分析源代码并生成抽象语法树(AST),而主程序通常独立于解析...

    java_cup_v10k.rar_java c++_java cup_java-c_java_cup.z_java_cup_v

    总结来说,Java_CUP_v10k是一个用于生成Java代码的语法分析器生成器,它使用LALR(1)算法解析用户定义的文法,生成的解析器和词法分析器能够帮助开发者处理符合特定文法的输入,简化了编译器和解释器开发过程中的底层...

    JAVA 对SWMM模型DLL调用

    例如,`runSWMMModel()`方法将被Java程序调用,实际上会执行SWMM模型。 7. **处理结果**:Java程序可以继续读取`rpt`和`out`文件,分析模型的结果。这可能涉及到文件I/O操作,以及对SWMM输出格式的理解。 在...

    A Trace-based Java JIT Compiler Retrofitted from a Method-based Compiler

    ### 基于追踪的Java即时编译器:从基于方法的编译器改造而来 #### 概述 本文档介绍了一种基于追踪的Java即时编译器(简称trace-JIT),该编译器是从一个高质量的基于方法的即时编译器(简称method-JIT)改造而来的...

    疯狂JAVA之学习笔记(1--15)

    - **编译阶段**:Java 源代码文件(.java 文件)首先通过 Java 编译器(javac)编译为字节码文件(.class 文件),这个过程类似于编译型语言。 - **解释执行阶段**:生成的字节码文件由 Java 虚拟机 (JVM) 解释执行...

Global site tag (gtag.js) - Google Analytics