1. 第19章 方法的调用与返回
1.1 关于构造方法那点事
关于构造方法调用的总结性说明:
l 在一个类中,如果不定义构造方法,则编译器会默认生成一个无参构造方法;
l 在构造方法中,如果想要调用其他的构造方法(用this调用本类的其它构造方法,用super调用父类的构造方法),必须放在构造方法的第一行;
l 在构造方法中,如果不显式地调用其它的构造方法,则默认在执行构造方法中任何代码之前,会先调用父类的默认无参构造方法;
l 注意,在子类的构造方法中,必须调用父类的构造方法(不管是隐式的调用,还是显式的调用,也不管调用的是无参构造方法,还是有参构造方法),如果不调用,则编译器将会报错!
首先来看构造方法的调用。
在一个类中,如果不定义构造方法,则编译器会默认生成一个无参构造方法。
//此类没有定义任何构造方法,在编译之后,编译器将自动生成一个构造方法 public class ConstructorTest01 {
}
|
用jClassLib观察,可以看到生成的类文件中有一个<init>方法,它就是无参的构造方法。
此无参构造方法对应的字节码如下:
0 aload_0 把局部变量区中第0个元素(一个对象引用)压入操作数栈 1 invokespecial #8 <java/lang/Object.<init>> 调用父类的无参构造方法 4 return 返回 |
可以看到,当我们调用这个构造方法,它将自动调用父类的无参构造方法。
现在,我们自己亲自定义这个类的构造方法,如下:
public class ConstructorTest01 {
public ConstructorTest01() {
}
} |
再来观察这个类的构造方法所对应的字节码:
0 aload_0 1 invokespecial #8 <java/lang/Object.<init>> 4 return |
从上可以看出,不管我们定不定义这个构造方法,字节码是一样的。
再看,下面我们在构造方法中增加一个super()调用。
public class ConstructorTest01 { public ConstructorTest01() { super(); } } |
再次观察生成的字节码,可以看到,是完全和前面一样的!
现在,我们在这个构造方法内部干点事,比如:
public class ConstructorTest01 { public ConstructorTest01() { int i = 1 + 2; } } |
那么,现在的字节码将会如何?
0 aload_0 1 invokespecial #8 <java/lang/Object.<init>> 4 iconst_3 5 istore_1 6 return |
可以看到,首先是调用父类的构造方法,然后是执行其它的指令。
假如我们企图改变这个缺省的过程(即在构造方法内部是先调用父类的构造方法,然后再执行其它的指令这个过程),那么编译器将会编译报错,无法编译通过,比如下面的代码是无法编译通过的:
public class ConstructorTest01 { public ConstructorTest01() { int i = 1 + 2; super(); } } |
super()不能放到其它代码的后面!
假设我们有多个构造方法(构造方法重载),那么每个构造方法里面,不管你有没有显式地调用super(),它们都会默认执行父类的无参构造方法!
比如:
public class ConstructorTest01 { public ConstructorTest01() { int i = 1 + 2; }
public ConstructorTest01(int i){ int b = i + 10; } }
|
我们看这二个构造方法的字节码:
第一个构造方法的字节码是:
0 aload_0 1 invokespecial #8 <java/lang/Object.<init>> 4 iconst_3 5 istore_1 6 return |
第二个构造方法的字节码是:
0 aload_0 1 invokespecial #8 <java/lang/Object.<init>> 4 iload_1 5 bipush 10 7 iadd 8 istore_2 9 return |
可以看到依然调用了父类的默认无参构造方法。
假设我们在其中一个构造方法里面调用另外一个构造方法,则在第一个构造方法中就不会调用父类的构造方法了(但第二个构造方法依然会调用父类的构造方法)。如下:
public class ConstructorTest01 { public ConstructorTest01() { this(50); int i = 1 + 2; }
public ConstructorTest01(int i){ int b = i + 10; } } |
第一个构造方法的字节码如下:
0 aload_0 1 bipush 50 3 invokespecial #8 <cn/com/leadfar/ConstructorTest01.<init>> 6 iconst_3 7 istore_1 8 return |
我们看,上面这个字节码,它不再调用父类(Object类)中的构造方法!
第二个构造方法的字节码如下:
0 aload_0 1 invokespecial #17 <java/lang/Object.<init>> 4 iload_1 5 bipush 10 7 iadd 8 istore_2 9 return |
我们可以看到,这第二个构造方法依然调用了父类的默认无参构造方法!
假如父类没有无参构造方法,而只提供了有参构造方法的话,对于子类来说,必须显式地调用这个构造方法!
假如父类代码如下:
public class ConstructorTest01Parent { public ConstructorTest01Parent(String s){ } } |
那么如下代码在编译时将不会通过:
public class ConstructorTest01Child extends ConstructorTest01Parent { } |
刚刚说过,子类必须调用父类的构造方法,现在父类只有一个带参数的构造方法(无参构造方法就没有了!)而子类没有定义构造方法,所以缺省情况下,子类有一个无参构造方法,而且子类的这个无参构造方法,默认将调用父类的无参构造方法,现在父类没有无参构造方法,所以上述子类在编译时将会通不过!
把子类代码改成下面这样,也依然是不可以的:
public class ConstructorTest01Child extends ConstructorTest01Parent { public ConstructorTest01Child(String s) { } } |
原因很清楚,我们在构造方法代码中没有显式调用任何其它构造方法,所以默认情况下,编译器会在这个构造方法的第一行插入调用父类默认无参构造方法的代码,而父类是没有这个构造方法的,所以,这段代码编译不会通过!
下面的代码才是正确的:
public class ConstructorTest01Child extends ConstructorTest01Parent { public ConstructorTest01Child() { //因为父类没有无参构造方法,所以 //必须显式的调用父类的构造方法 super(""); } } |
下面的代码也是正确的:
public class ConstructorTest01Child extends ConstructorTest01Parent { public ConstructorTest01Child(String s) { //因为父类没有无参构造方法,所以 //必须显式的调用父类的构造方法 super(s); } } |
或者下面的代码:
public class ConstructorTest01 extends ConstructorTest01Parent{ public ConstructorTest01() { this(50); int i = 1 + 2; }
public ConstructorTest01(int i){ super("kkk"); //此行是必须的,因为父类已经没有默认构造方法了 int b = i + 10; } } |
当我们调用new关键字创建一个对象的时候,需要指定相应的构造方法:
public class ConstructorTest01Client { public static void main(String[] args) { ConstructorTest01 ct01 = new ConstructorTest01(); } } |
上述代码中,首先创建一个对象,然后把这个对象的引用赋给一个局部变量ct01。
0 new #16 <cn/com/leadfar/ConstructorTest01> 首先在堆中创建了一个对象,并把对象引用压入操作数栈 3 dup 复制操作数栈顶的数据(这样操作数栈中有两个相同的对象引用,一个用于调用其构造方法,另外一个) 4 invokespecial #18 <cn/com/leadfar/ConstructorTest01.<init>> 弹出栈顶的对象引用,并调用其构造方法(构造方法没有返回值) 7 astore_1 弹出栈顶的对象引用,并存储在局部变量区索引号为1的地方(索引号为0的地方是main方法的参数args的引用) 8 return |
new/dup/invokespecial,是new关键字对应的三个指令。
如下例子:
public static void main(String[] args) { MethodCall mc = new MethodCall();
//调用没有返回值的方法 mc.noReturnValueMethodCall();
//调用有返回值的方法 int i = mc.withIntReturnValueMethodCall();
System.out.println(i); } |
其字节码是:
0 new #16 <cn/com/leadfar/MethodCall> 3 dup 4 invokespecial #18 <cn/com/leadfar/MethodCall.<init>> 7 astore_1 8 aload_1 9 invokevirtual #19 <cn/com/leadfar/MethodCall.noReturnValueMethodCall> 12 aload_1 13 invokevirtual #22 <cn/com/leadfar/MethodCall.withIntReturnValueMethodCall> 16 istore_2 17 getstatic #26 <java/lang/System.out> 20 iload_2 21 invokevirtual #32 <java/io/PrintStream.println> 24 return |
1.2 实例方法调用
invokespecial和invokevirtual这两个指令说明了多态的具体实现行为的一切。如果是invokespecial,则表示调用的是哪个具体的方法,在编译时已经确定;如果是invokevirtual,表示具体调用的哪个方法,还得看运行时刻(动态绑定)。