Java方法
方法和函数的区别
为了区分开来,先不讨论static
方法。
方法是一个面向对象的概念,它依赖于对象,而函数不是。假设fun为一个方法/函数名,那么:fun()
是一个函数调用,而object.fun()
是一个方法调用。这里讲的是一个形式上的区别,不是一个严格的说法。
在方法内部可通过this
来访问所依赖的对象。访问this对象成员,调用this对象的方法也是同理。
那么这是如何实现的呢?
如:
package com.test;
public class Test{
int i;
void fun() {
}
public static void main(String[] args) {
Test test = new Test();
int i = test.i;
test.fun();
}
}
这里我们用javap -verbose
反编译:
关于
javap
的详细参数介绍可参阅这里javap - The Java Class File Disassembler。另外行号和本地变量表记录的参数名属于调试信息,如果编译的时候没有指定正确的参数,这里是显示不出来的。
$ javap -verbose com.test.Test
Classfile /Users/larry/Workspace/test/target/classes/com/test/Test.class
...
void fun();
descriptor: ()V
flags:
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/test/Test;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class com/test/Test
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: getfield #4 // Field i:I
12: istore_2
13: aload_1
14: invokevirtual #5 // Method fun:()V
17: return
LineNumberTable:
line 10: 0
line 11: 8
line 12: 13
line 13: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
8 10 1 test Lcom/test/Test;
13 5 2 i I
}
SourceFile: "Test.java"
从main
方法的LocalVariableTable
属性可以看出,test
变量对应本地变量表的第1个Slot
,所以aload_1
代表把test的值(也就是引用)压到栈顶。推到栈顶之后getfield
访问成员,invokeXXX
调用方法。
再看到fun
方法的Code
属性,args_size
代表参数个数,再看到LocalVariableTable
,第0个Slot
就是this
。fun
方法虽然没有参数,但是生成的字节码有一个参数,而这个参数就是this
。
结合以上两点,test.fun()
事实上是把test作为参数,调用了fun方法,相当于fun(test)
,而传入的引用,在方法块中就是this
。
Code属性详细内容参见4.7.3. The Code Attribute。
虚方法的简单总结
jvm 关于方法调用的指令有如下五个:
- invokestatic: 调用static方法。
- invokespecial: 调用带有priavte的方法,构造方法和通过super调用方法。
- invokeinterface: 通过接口引用变量调用方法。
- invokedynamic: Java8中lambda表达式生成对象时会出现这个指令。
- invokevirtual: 剩下的就全是invokevirtual了。
详见2.11.8. Method Invocation and Return Instructions
调用形式如invokevirtual #5
,指令+方法的符号引用(对应到常量池的索引)。刨去invokedynamic指令,依据以上规则简单总结一下满足那些条件一个方法才能算是虚方法:
- 不带
static
、final
和private
任意一个修饰符。 - 不是构造方法
- 不通过super调用
虚方法调用调用的方法为对象的实际类型中定义的版本。对象的实际类型即创建该对象的构造方法所属的类,在HotSpot中,对象头的第二部分存储的即为实际类型的指针。
与方法相关的几个东西
桥接方法
参数化类型子类中参数可以窄化,依然是符合多态性的。比如:
class A<T>{
void fun(T t) {}
}
class B extends A<String> {
void fun(String string) {}
}
如果B的实例赋值给类型为A的引用,调用fun方法依然能够对应到正确的版本。但是参数化类型参数擦除之后就是Object,和String不同,方法签名也会不同,这是怎么一回事呢?答案是B中生成了一个桥接方法。
$ javap -c com.test.B
Compiled from "Test.java"
class com.test.B extends com.test.A<java.lang.String> {
com.test.B();
Code:
0: aload_0
1: invokespecial #1 // Method com/test/A."<init>":()V
4: return
void fun(java.lang.String);
Code:
0: return
void fun(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #2 // class java/lang/String
5: invokevirtual #3 // Method fun:(Ljava/lang/String;)V
8: return
}
可以观查到B中多了一个参数为Object
的fun
方法,翻译一下就是。
void fun(Object object){
fun((String)object);
}
因此,在覆盖父类中带有参数类型参数的方法时,如果在子类中尝试添加一个同方法名、参数化类型参数替换为Object会出现编译期错误Error:(18, 10) java: name clash: fun(java.lang.Object) in com.test.B and fun(T) in com.test.A have the same erasure, yet neither overrides the other
,实际上就是和隐式生成的桥接方法冲突了。
可变参数
可变参数实际上是一个数组,如:
class A{
void fun(String... abc) {
fun("a", "b", "c");
}
}
编译后
void fun(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: ACC_VARARGS
Code:
stack=5, locals=2, args_size=2
0: aload_0
1: iconst_3
2: anewarray #2 // class java/lang/String
5: dup
6: iconst_0
7: ldc #3 // String a
9: aastore
10: dup
11: iconst_1
12: ldc #4 // String b
14: aastore
15: dup
16: iconst_2
17: ldc #5 // String c
19: aastore
20: invokevirtual #6 // Method fun:([Ljava/lang/String;)V
23: return
LineNumberTable:
line 14: 0
line 15: 23
LocalVariableTable:
Start Length Slot Name Signature
0 24 0 this Lcom/test/A;
0 24 1 abc [Ljava/lang/String;
可以看到方法的描述符为([Ljava/lang/String;)V,也就是参数为String[],返回值为void。在调用的地方,先通过anewarray
创建一个数组,再通过aastore
把引用赋值到对应的索引中,也就是上图中行号0到20所做的内容。
方法的参数和局部变量对应栈帧的局部变量表,如果编译的时候不加调试信息javac -g:none
的话,是不会存在变量名的。也就是说参数名和变量名不影响方法的调用,那么main方法其实可以写成以下形式:
public static void main(String... hahaha) {
}
依然是可以正常运行的。当然,这其实没什么用。。。
本文没有写方法解析的内容,如果对方法解析有兴趣建议读一读15.12.2.5. Choosing the Most Specific Method
网友评论