美文网首页
深入Java方法调用

深入Java方法调用

作者: 风干鸡 | 来源:发表于2016-09-02 14:14 被阅读0次

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就是thisfun方法虽然没有参数,但是生成的字节码有一个参数,而这个参数就是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指令,依据以上规则简单总结一下满足那些条件一个方法才能算是虚方法:

  • 不带staticfinalprivate任意一个修饰符。
  • 不是构造方法
  • 不通过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中多了一个参数为Objectfun方法,翻译一下就是。

    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

相关文章

网友评论

      本文标题:深入Java方法调用

      本文链接:https://www.haomeiwen.com/subject/orohettx.html