• Java高级——方法调用


    概述

    方法调用阶段是确定被调用方法,不涉及方法内部的具体运行过程,分为解析和分派,所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用

    调用方法的字节码指令有:

    • invokestatic:调用静态方法
    • invokespecial:调用()方法、私有方法和父类方法
    • invokevirtual:调用所有的虚方法
    • invokeinterface:调用接口方法,会在运行时再确定一个实现该接口的对象
    • invokedynamic:先动态解析出调用点限定符所引用的方法,然后再执行该方法

    前4条调用指令,分派逻辑都固化在JVM内部,而invokedynamic指令的分派逻辑是由用户设定的引导方法决定

    解析

    在类加载的解析阶段,非虚方法的符号引用转化为直接引用,称为解析(Resolution),非虚方法包括

    • 静态方法、私有方法、构造方法、父类方法、final方法(因历史原因,由invokevirtual调用)

    分派

    虚方法的符号引用转化为直接引用延迟到运行时去完成,称为分派(Dispatch),其可能是静态或动态的

    按照分派依据的宗量数可分为单分派和多分派,两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派

    静态分派(重载)

    所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派,发生在编译阶段,典型应用为重载

    • 《Java虚拟机规范》和《Java语言规范》称为Method Overload Resolution,也可归纳到解析

    对于如下程序

    public class Test {
    
    	static abstract class Human {
        }
    
        static class Man extends Human {
        }
    
        static class Woman extends Human {
        }
    
        public void sayHello(Human guy) {
            System.out.println("hello,guy!");
        }
    
        public void sayHello(Man guy) {
            System.out.println("hello,gentleman!");
        }
    
        public void sayHello(Woman guy) {
            System.out.println("hello,lady!");
        }
    
        public static void main(String[] args) {
            Human man = new Man();
            Human woman = new Woman();
            Test test = new Test();
            test.sayHello(man);
            test.sayHello(woman);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    打印

    hello,guy!
    hello,guy!
    
    • 1
    • 2

    Human称为变量的静态类型,Man则称为实际类型,静态类型在编译时可知,而实际类型在运行时才能确定

    编译器在重载时是通过参数的静态类型而不是实际类型作为判定依据的,故上面选择选择了sayHello(Human)

    但很多情况下,当重载版本不唯一时,只能选择一个相对更合适的方法

    public class Test {
    
    	public static void sayHello(Object arg) {
            System.out.println("hello Object");
        }
        public static void sayHello(int arg) {
            System.out.println("int");
        }
        public static void sayHello(long arg) {
            System.out.println("long");
        }
        public static void sayHello(Character arg) {
            System.out.println("Character");
        }
        public static void sayHello(char arg) {
            System.out.println("char");
        }
        public static void sayHello(char... arg) {
            System.out.println("char ...");
        }
        public static void sayHello(Serializable arg) {
            System.out.println("Serializable");
        }
        public static void main(String[] args) {
            sayHello('a');
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    对如上程序,打印char

    • 注释sayHello(c),打印int,将’a’转为int类型的97
    • 注释sayHello(i),打印long,将’a’转为int再转long
    • 会按照char>int>long>float>double顺序转换,但不会匹配到byte和short类型的重载,因为char转byte或short是不安全的
    • 注释sayHello(l),打印Character,发生自动装箱
    • 注释sayHello(C),打印Serializable,为Character实现接口
    • 注释sayHello(S),打印Object,为Character父类
    • 注释sayHello(O),打印char…,可变参数优先级最低

    动态分派(重写)

    对于如下程序

    public class Test {
    
    	static abstract class Human {
            protected abstract void sayHello();
        }
        static class Man extends Human {
            @Override
            protected void sayHello() {
                System.out.println("man say hello");
            }
        }
        static class Woman extends Human {
            @Override
            protected void sayHello() {
                System.out.println("woman say hello");
            }
        }
        public static void main(String[] args) {
            Human man = new Man();
            Human woman = new Woman();
            man.sayHello();
            woman.sayHello();
            man = new Woman();
            man.sayHello();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    打印

    man say hello
    woman say hello
    woman say hello
    
    • 1
    • 2
    • 3

    使用javap转为字节码,0-15行是创建man和woman并存到局部变量表

    public static void main(java.lang.String[]);
      Code:
       Stack=2, Locals=3, Args_size=1
       0:	new	#16; //class Test$Man
       3:	dup
       4:	invokespecial	#18; //Method Test$Man."":()V
       7:	astore_1
       8:	new	#19; //class Test$Woman
       11:	dup
       12:	invokespecial	#21; //Method Test$Woman."":()V
       15:	astore_2
       16:	aload_1
       17:	invokevirtual	#22; //Method Test$Human.sayHello:()V
       20:	aload_2
       21:	invokevirtual	#22; //Method Test$Human.sayHello:()V
       24:	new	#19; //class Test$Woman
       27:	dup
       28:	invokespecial	#21; //Method Test$Woman."":()V
       31:	astore_1
       32:	aload_1
       33:	invokevirtual	#22; //Method Test$Human.sayHello:()V
       36:	return
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    16-17、20-21分别是man和woman调用sayHello对应的invokevirtual指令,指向常量仍为Human.sayHello,但最终执行的方法不同

    invokevirtual指令的运行时解析过程如下:

    • 找到栈顶元素所指向对象的实际类型,记作C
    • 在C中查找与常量name_index和descriptor_index匹配的方法并进行访问权限校验,若通过返回直接引用,否则返回IllegalAccessError
    • 若C中未找到,则查找其父类
    • 若未找到抛出AbstractMethodError

    字段没有多态,对于如下程序

    public class Test {
    	static class Father {
            public int money = 1;
    
            public Father() {
                money = 2;
                showMeTheMoney();
            }
    
            public void showMeTheMoney() {
                System.out.println("I am Father, i have $" + money);
            }
        }
    
        static class Son extends Father {
            public int money = 3;
    
            public Son() {
                money = 4;
                showMeTheMoney();
            }
    
            public void showMeTheMoney() {
                System.out.println("I am Son, i have $" + money);
            }
        }
    
        public static void main(String[] args) {
            Father gay = new Son();
            System.out.println("This gay has $" + gay.money);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    打印输出

    I am Son, i have $0
    I am Son, i have $4
    This gay has $2
    
    • 1
    • 2
    • 3
    • 先调用父类构造,其中的showMeTheMoney()是虚方法调用,而Son中的money还未初始化,结果为0
    • 再调用子类构造,此时Son中的money初始化为4
    • 最后通过静态类型访问到父类中的money

    动态分派实现

    动态分派选择方法时,需运行时在接收者类型的方法元数据中搜索合适的目标方法,为避免反复搜索,为类型在方法区中建立一个虚方法表(Virtual Method Table,vtable)

    在这里插入图片描述
    上图为上面程序的虚方法表,其存放各个方法的实际入口地址

    • 若未重写父类方法,地址指向父类实现
    • 若已重写父类方法,地址指向子类实现

    相同签名的方法在父类、子类的虚方法表中都有相同索引,在类型变换时只需变更方法表即可找到对应方法

    虚方法表一般在类加载的连接阶段进行初始化

    单分派和多分派

    方法的接收者(调用者)与方法的参数统称为方法的宗量

    • 单分派是根据一个宗量对目标方法进行选择
    • 多分派则是根据多于一个宗量对目标方法进行选择
    public class Test {
    	static class QQ {
        }
    
        static class _360 {
        }
    
        public static class Father {
            public void hardChoice(QQ arg) {
                System.out.println("father choose qq");
            }
    
            public void hardChoice(_360 arg) {
                System.out.println("father choose 360");
            }
        }
    
        public static class Son extends Father {
            public void hardChoice(QQ arg) {
                System.out.println("son choose qq");
            }
    
            public void hardChoice(_360 arg) {
                System.out.println("son choose 360");
            }
        }
    
        public static void main(String[] args) {
            Father father = new Father();
            Father son = new Son();
            father.hardChoice(new _360());
            son.hardChoice(new QQ());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    对于方法hardChoice()

    • 编译时的静态分派,需要确认调用者的静态类型(Father或Son)和方法参数(QQ还是360),有两个宗量
    • 运行时的动态分派,方法参数静态类型、实际类型对方法不会有影响(只需在编译时确保传进来的是QQ和360,而不在乎是不是其子类),此时只有调用者的实际类型一个宗量

    由上可知Java是一门静态多分派,动态单分派的语言

    动态类型语言

    动态类型语言,其关键特征是其类型检查过程在运行时而不是编译时进行,常见的有JavaScript、Python,如下代码

    obj.println("hello world");
    
    • 1
    • 若在java中且obj的静态类型为PrintStream
    • 则obj的实际类型需是PrintStream或其子类才合法
    • 原因是java编译时会生成完整符号引用,如下
    invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
    
    • 1
    • 这个符号引用定义了方法调用者类型、名字、参数和返回值,可翻译出直接引用

    而对于JavaScript,无论obj是何种类型,继承关系如何,只要有相同签名的println(String)方法即可调用成功

    动态类型语言变量无类型而变量值才有类型

    invokedynamic和java.lang.invoke

    JDK7之前要实现动态类型语言,需在编译时留个占位符类型,运行时动态生成字节码实现具体类型替换,但会导致

    • 复杂度增加,带来额外的性能和内存开销
    • 无法确定调用对象具体类型,无法内联

    在此背景下,JDK7推出invokedynamic和java.lang.invoke用于支持动态类型语言

    java.lang.invoke

    invoke通过方法句柄动态确认目标方法,如实现一个带谓词(指由外部传入用于排序时比较大小的动作)的排序函数

    • C/C++中的做法是把谓词定义为函数,用函数指针接收谓词
    void sort(int list[], const int size, int (*compare)(int, int))
    
    • 1
    • 在Java中不能把函数作为参数传递,做法是设计一个带有compare()方法的Comparator接口,其实现对象作为参数
    void sort(List list, Comparator c)
    
    • 1

    如下利用方法句柄模拟invokevirtual指令的执行过程

    import static java.lang.invoke.MethodHandles.lookup;
    import java.lang.invoke.MethodHandle;
    import java.lang.invoke.MethodType;
    
    
    public class Test {
    	static class ClassA {
            public void println(String s) {
                System.out.println(s);
            }
        }
        public static void main(String[] args) throws Throwable {
            Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
            getPrintlnMH(obj).invokeExact("hello");
        }
        private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable {
            MethodType mt = MethodType.methodType(void.class, String.class);
            return lookup().findVirtual(reveiver.getClass(), "println", mt).bindTo(reveiver);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 无论obj是System.out还是ClassA,都可以调用println()
    • MethodType.methodType()传递方法返回值和参数
    • lookup().findVirtual()在指定类中查找相对应的方法句柄,虚方法需传递this、方法名和方法类型,绑定到调用者

    返回的MethodHandle对象可视为对调用方法的引用,由此Java就可实现类似函数指针的功能

    void sort(List list, MethodHandle compare)
    
    • 1

    利用反射也能达到同样效果,但

    • 反射模拟Java代码层次的方法调用(不关心底层细节),而MethodHandle是字节码层次
    • findStatic/findVirtual/findSpecial方法对应invokestatic/invokevirtual(invokeinterface)/invokespecial的执行权限校验行为
    • 反射的Method比MethodHandle包含更多信息
    • 利用MethodHandle可对动态类型语言的支持进行优化
    • 反射只针对Java,而MethodHandle可用于所有运行在JVM的语言

    invokedynamic

    invokedynamic指令与MethodHandle的作用是一样的

    • 解决原invoke*指令将方法分派固定在JVM中的问题
    • 如何查找目标方法由用户代码决定

    invokedynamic指令的位置称为动态调用点,参数为CONSTANT_InvokeDynamic_info常量,其包含

    • 引导方法,存放在BootstrapMethods属性,返回值为java.lang.invoke.CallSite,代表实际方法调用
    • 方法类型和名称

    invokedynamic不能通过javap生成,如下简单举例

    invokedynamic #123, 0
    
    • 1

    上面为invokedynamic指令,第一个参数为常量#123,第二个参数为0用于给实际方法占位

    #121 = NameAndType #33:#30 
    #123 = InvokeDynamic #0:#121
    
    • 1
    • 2

    常量#123是一个CONSTANT_InvokeDynamic_info,#0表示引导方法取Bootstrap Methods属性表的第0项,#121为方法类型和名称

    简单实例

    对于如下继承结构,Son可以通过super访问Father的方法

    class GrandFather {
        void thinking() {
            System.out.println("i am grandfather");
        }
    }
    class Father extends GrandFather {
        void thinking() {
            System.out.println("i am father");
        }
    }
    
    class Son extends Father {
        void thinking() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    但如果要访问GrandFather呢?JDK7之前很难处理,但JDK7后通过MethodHandles实现

    public class Test {
        
        public static void main(String[] args) {
        	new Son().thinking();
    	}
    }
    
    class GrandFather {
        void thinking() {
            System.out.println("i am grandfather");
        }
    }
    
    class Father extends GrandFather {
        void thinking() {
            System.out.println("i am father");
        }
    }
    
    class Son extends Father {
        void thinking() {
            try {
                MethodType mt = MethodType.methodType(void.class);
                Field lookupImpl = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
                lookupImpl.setAccessible(true);
                MethodHandle mh = ((MethodHandles.Lookup) lookupImpl.get(null)).findSpecial(GrandFather.class, "thinking", mt, GrandFather.class);
                mh.invoke(this);
            } catch (Throwable e) {
            
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
  • 相关阅读:
    【Rust 日报】2023-11-19 solars:可视化太阳系
    zookeeper入门到精通07——zookeeper客户端API节点操作与原理
    论文解读:(VPT)Visual Prompt Tuning
    【学习笔记】AGC036
    屏幕亮度调节保护您的眼睛
    经验分享,xps格式转成pdf格式
    Linux命令
    T1025:保留12位小数的浮点数(信息学一本通C++)
    JT808协议介绍 --- 格林恩德 CR202 RTK 高精度车载定位器协议解读
    SpringMVC之JSR303与拦截器
  • 原文地址:https://blog.csdn.net/qq_35258036/article/details/127653233