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

本文尝试对java在编译器和运行期如何处理程序代码中的方法调用表达式进行描述,本文的大部分内容来自于java语言规范3.0.
由于java动态语言的特性,因此它在编译期和运行期都需要对程序代码中的方法调用表达式进行处理。其中对方法调用表达进行处理的大部分工作是在编译期完成的,而运行期的大部分工作则是对编译完成的方法调用表达式进行有效性检查。
[b][size=medium]编译期完成的处理[/size][/b]
在编译期,java需要对方法调用表达式进行检查和分析,如果表达式能够被正确地解释,那么java的编译器需要将有关方法调用的元数据记录到字节码文件中,为jvm能够在运行期正确地执行方法调用提供足够多的信息。java编译器要完成的主要工作可以用表1中的伪代码来表示。

根据表达式确定要在那些类和接口中搜索方法;
if 找到了一个或者多个类和接口
尝试从这些类和接口中找到所有潜在可用方法定义;
if 找到了一个或者多个可用的方法定义
从这些方法定义中找出与方法调用表达式最为匹配的一个方法定义;
if 找到了一个最为匹配的方法
对方法调用的语法进行检查;
if 方法调用具有正确的语法
将运行期需要的相关信息写入字节码中;
else
编译器产生错误;
else
编译器产生错误;
else
编译器产生错误;
else
编译器产生错误;

在本文后面的叙述中,大家会发现,编译器的大部分精力需要花在为一个方法调用表达式确定一个最为匹配的方法定义,而这一工作之所以会被复杂化则是由于java对泛型的引入。另外,对于java来说,在确定需要调用哪个方法时,方法的返回值的确非常的不重要,通过后面的叙述大家也可以看到,java将方法的返回值排除在方法签名之外确实是有道理的。
[b][size=small]确定要在哪些类与接口中搜索方法定义[/size][/b]
我们知道,在java这样的面向对象语言中,方法是定义在类或者接口中的。所以,当java编译器面对一个方法调用表达式时,为了找到被调用的这个方法,首先要做的就是找到可能会定义这个方法的类或者接口。编译器会根据方法调用表达式的形式来确定要搜索的类和接口,表2列出了相关信息。表中列出的方法调用形式涵盖了我们可以在java中使用的所有方法调用形式。在这一步中找到的类和接口将作为后面进步进行解析的输入,最终被确定的方法将在这一步中找到的类或者接口中产生。另外值得一提的是,在搜索备选的类或者接口时,编译器是根据方法名来查找的,方法参数的个数及其类型在这一步是用不到的。
[table]
|[b]编号[/b]|[b]表达式形式[/b]|[b]要搜索的类和/或接口[/b]|
|1|methodName(args)| 对于这种形式的方法调用,如果是正确的,那么对于这条语句所在的作用域来说,必然会有一个可见的方法定义。这个方法定义可能是在类中,也可能是在内部类中,那么要搜索的类或者接口就是定义了这个方法的最内层的类或者接口。简单来说,我们要找的类或者接口就是包含这条语句的类及其外部类。|
|2|TypeName[1].methodName(args)| 对于这种形式的方法调用,要搜索的类就是TypeName所定义的那个类,如果TypeName定义的是一个接口,编译器会给出编译错误。因为这种类型的调用指定针对静态方法,而接口是不能有任何形式的方法定义的,它只能包含方法的声明。|
|3|Primary.<NonWildTypeArguments>methodName(args)| Primary是java规范中定义的初级表达式,用来构成其他更加复杂的表达式([url=http://docs.oracle.com/javase/specs/jls/se5.0/html/expressions.html#15.8]参见java规范15.8节[/url])。如果Primary表达式的结果是一个接口或者类,那么这个接口或者类就是我们要搜索的。如果Primary是一个类型参数T,那么要搜索的接口或者类就是T的上届。|
|4|super.<NonWildTypeArguments>methodName(args)| 在这种情况下,要搜索的类是包含了这条方法调用语句的类的超类。假设包含这条方法调用语句的类是T,如果T是Object(Object是没有超类得)或者T是一个接口(接口是不能定义方法的),那么编译器会给出编译错误。|
|5|ClassName[1].super.<NonWildTypeArguments>methodName(args)| 这种类型的调用语句,要搜索的类式ClassName代表的那个类的超类。如果包含这条语句的类不是ClassName代表的那个类的子类,编译器会给出错误。如果ClassName代表的那个类式Object,编译器也会给出错误。另外,如果包含这条语句的类是Object或者是一个接口,那么编译器同样会给出错误。|
|6|FieldName.methodName(args)| 这里FieldName表示类的属性,要搜索的类就是这个属性名所代表的那个类。如果FieldName代表的是一个类型参数,那么要搜索的类是这个类型参数的上届。|
|7|TypeName.<NonWildTypeArguments>methodName(args)|同2|
[/table]
[b][size=small]确定方法签名[/size][/b]
在这一步中,编译器会根据被调用的方法签名(方法名加上参数),在上一步得到的接口或者类中搜索可用的所有方法(方法定义符合调用语句中给出的参数个数及其类型)。如果可能用的方法有多个,那么编译器必须决定哪个方法定义是最符合调用语句期望的(chose the most specific method)。
考虑到兼容性的原因,搜索可用方法的过程被却分为三个阶段:
[list]
[*]在第一个阶段中,编译器在不允许自动装箱和解箱以及不考虑变长参数列表的情况下,执行方法搜索;
[*]在第二个阶段中,编译器在不考虑变长参数列表,但是允许自动装箱和解箱的情况下,执行方法搜索;
[*]在第三个阶段中,编译器在允许自动装箱和解箱以及变长参数列表的情况下,执行方法搜索;
[/list]
编译器从第一个阶段开始执行,当第一个阶段完成之后,如果发现没有找到任何可用的方法定义,那么就会执行第二个阶段,以此类推。在任何一个阶段中,如果找到了一个以上的可用方法定义,就不会继续执行后面的阶段。
在每个阶段中,java都会试图找出符合以下要求的所有方法定义:
[list]
[*]定义的方法名与调用语句中的方法名一致(大小写敏感的);
[*]方法定义对于方法调用语句来说是可以见的;
[*]方法定义中的参数个数必须小于等于方法调用语句中提供的参数个数(变长参数被视为一个方法参数);
[*]如果方法定义的参数列表中包含了变长参数,假设方法定义有n个参数(变长参数被视为一个方法参数),那么方法调用语句提供的参数列表长度必须大于等于n-1;
[*]如果方法定义不包含变长参数,假设方法定义有n个参数,那么方法调用语句提供的参数列表的长度也必须是n;
[*]如果方法调用语句包含了显示的类型参数,并且方法声明也是泛型的,那么方法调用语句中的真实类型参数个数必须与方法声明中的形式化类型参数的格式保持一致;
[*]方法调用语句中参数列表中的参数类型必须依次能够与方法声明中参数列表的参数类型匹配;
[/list]
从上面的定义中不难发现,java是允许泛型方法调用匹配到非泛型方法声明上的,这是兼容性和可替换性的要求。兼容性比较好理解,如果我们用使用了泛型的代码,调用一个用较早版本写出来的类库,这种调用也是应该成功的。可替换性是针对子类化和接口实现而言的,因为子类或者实现类可以将超类或者接口中的泛型方法覆写(override)成非泛型的方法,为了保证超类能用的地方子类也必然能用的这条规则,所以允许泛型方法调用匹配非泛型方法声明。
在上述几点中,最后一点,也就是关于方法参数类型匹配的过程相较其他几点要复杂一些。因为在Java中允许子类化、允许使用表达式的值作为方法的参数、允许变长参数,另外还有泛型参与其中,导致了处理上的复杂。在java规范中对此有比较详细的说明,因为多是数学上的一些推导,这里就不累述了。相关[url=http://docs.oracle.com/javase/specs/jls/se5.0/html/expressions.html#15.12.2.2]参见java规范15.12.2.2-15.12.2.4节[/url]内容。
[b][size=small]重载方法决断[/size][/b]
当编译器发现有多个方法都可以满足方法调用表达式时,这个时候就要选择一个方法声明作为运行时正真调用的方法。当然,Java规范对这个过程也有详细的数学定义,这里也就不说了。相关内容可以参见[url=http://docs.oracle.com/javase/specs/jls/se5.0/html/expressions.html#15.12.2.5]java规范15.12.2.5节[/url]。
这里需要提一下对抽象方法(abstract method)的特殊处理,如果编译器发现了的多个最合适的方法,但是其中只有一个是非抽象方法,那么这个非抽象方法就是运行时要调用的方法。如果所有的方法都是抽象的,那么编译器会从具有最具体(most specific)返回类型的子集中随机选择一个来作为运行时调用的方法。所以,对于抽象方法的定义还是要注意一些。
[b][size=small]关于类型推导[/size][/b]
从1.5开始,java引入了泛型,这给编译器处理方法调用增加了不少的复杂度,其中很大一部分来自方泛型方法调用时候的类型推导。关于这个部分java规范上内容颇多,而且都是一些数学推导和定义,对平日代码编写也没有特别大的用处,这里就详细讲述了,有兴趣可以看一下[url=http://docs.oracle.com/javase/specs/jls/se5.0/html/expressions.html#15.12.2.7]java规范上的相关描述[/url]。
[b][size=small]扫尾处理[/size][/b]
编译器在为一个方法调用语句选择了一个最合适的方法声明之后,会将相关信息写入class文件,这个方法声明便被称为编译时方法声明(compile-time declaration)。之后编译器还会进行进一步的校验。比如一个实例方法被放到一个静态上下文中调用,或者super.methodName这样的调用最后被发现找到的是一个抽象方法,又或者返回类型为void的方法被放到了负值语句或者被当作其他方法调用的参数,这个时候编译器都会给出编译期的异常。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值