前言
今天分析一下为什么静态方法不能调用非静态方法,但是反过来却可以调用。本文将从字节码的角度分析底层原因。
原理分析
这里先说结论:静态方法属于类,方法的参数列表没有默认隐含的
this
引用,这也说明了调用静态方法不需要当前对象的引用this
,但是调用非静态方法需要当前对象的引用才能调用,所以这就是不能调用非静态方法的主要原因,非静态方法属于实例对象,方法的参数列表有默认隐含的this
引用,同时调用静态方法不需要this
,因此可以调用。
示例代码
package com.github.xfc.staticmethod;
/**
* 为什么静态方法不能调用非静态方法
*
* @author xf.chen
* @date 2021/9/2 07:31
* @since 1.0.0
*/
public class StaticMethodTest {
/**
* 非静态方法
*/
public void normalMethod(String data) {
// 非静态方法调用静态方法
staticMethod(data);
}
/**
* 静态方法
*/
public static void staticMethod(String data) {
System.out.println("static:" + data);
}
public static void main(String[] args) {
// 静态方法调用
StaticMethodTest.staticMethod("hello");
// 实例化
final StaticMethodTest palindrome = new StaticMethodTest();
// 非静态方法调用
palindrome.normalMethod("xf");
// 通过实例对象调用非静态方法调用
palindrome.staticMethod("chen");
}
}
上述代码非常简单,主要定义了一个静态方法和一个非静态方法,同时增加了一个main方法用于测试效果。
上述代码执行结果如下
字节码
Last modified 2021-9-2; size 1074 bytes
MD5 checksum df4162d6ec0e9c271672f25e14c6b50e
Compiled from "StaticMethodTest.java"
public class com.github.xfc.staticmethod.StaticMethodTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #16.#36 // java/lang/Object."<init>":()V
#2 = Methodref #11.#37 // com/github/xfc/staticmethod/StaticMethodTest.staticMethod:(Ljava/lang/String;)V
#3 = Fieldref #38.#39 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Class #40 // java/lang/StringBuilder
#5 = Methodref #4.#36 // java/lang/StringBuilder."<init>":()V
#6 = String #41 // static:
#7 = Methodref #4.#42 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #4.#43 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Methodref #44.#45 // java/io/PrintStream.println:(Ljava/lang/String;)V
#10 = String #46 // hello
#11 = Class #47 // com/github/xfc/staticmethod/StaticMethodTest
#12 = Methodref #11.#36 // com/github/xfc/staticmethod/StaticMethodTest."<init>":()V
#13 = String #48 // xf
#14 = Methodref #11.#49 // com/github/xfc/staticmethod/StaticMethodTest.normalMethod:(Ljava/lang/String;)V
#15 = String #50 // chen
#16 = Class #51 // java/lang/Object
#17 = Utf8 <init>
#18 = Utf8 ()V
#19 = Utf8 Code
#20 = Utf8 LineNumberTable
#21 = Utf8 LocalVariableTable
#22 = Utf8 this
#23 = Utf8 Lcom/github/xfc/staticmethod/StaticMethodTest;
#24 = Utf8 normalMethod
#25 = Utf8 (Ljava/lang/String;)V
#26 = Utf8 data
#27 = Utf8 Ljava/lang/String;
#28 = Utf8 staticMethod
#29 = Utf8 main
#30 = Utf8 ([Ljava/lang/String;)V
#31 = Utf8 args
#32 = Utf8 [Ljava/lang/String;
#33 = Utf8 palindrome
#34 = Utf8 SourceFile
#35 = Utf8 StaticMethodTest.java
#36 = NameAndType #17:#18 // "<init>":()V
#37 = NameAndType #28:#25 // staticMethod:(Ljava/lang/String;)V
#38 = Class #52 // java/lang/System
#39 = NameAndType #53:#54 // out:Ljava/io/PrintStream;
#40 = Utf8 java/lang/StringBuilder
#41 = Utf8 static:
#42 = NameAndType #55:#56 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#43 = NameAndType #57:#58 // toString:()Ljava/lang/String;
#44 = Class #59 // java/io/PrintStream
#45 = NameAndType #60:#25 // println:(Ljava/lang/String;)V
#46 = Utf8 hello
#47 = Utf8 com/github/xfc/staticmethod/StaticMethodTest
#48 = Utf8 xf
#49 = NameAndType #24:#25 // normalMethod:(Ljava/lang/String;)V
#50 = Utf8 chen
#51 = Utf8 java/lang/Object
#52 = Utf8 java/lang/System
#53 = Utf8 out
#54 = Utf8 Ljava/io/PrintStream;
#55 = Utf8 append
#56 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#57 = Utf8 toString
#58 = Utf8 ()Ljava/lang/String;
#59 = Utf8 java/io/PrintStream
#60 = Utf8 println
{
public com.github.xfc.staticmethod.StaticMethodTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 31: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/github/xfc/staticmethod/StaticMethodTest;
public void normalMethod(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=2
0: aload_1
1: invokestatic #2 // Method staticMethod:(Ljava/lang/String;)V
4: return
LineNumberTable:
line 38: 0
line 39: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/github/xfc/staticmethod/StaticMethodTest;
0 5 1 data Ljava/lang/String;
public static void staticMethod(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #4 // class java/lang/StringBuilder
6: dup
7: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
10: ldc #6 // String static:
12: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_0
16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: return
LineNumberTable:
line 46: 0
line 47: 25
LocalVariableTable:
Start Length Slot Name Signature
0 26 0 data Ljava/lang/String;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: ldc #10 // String hello
2: invokestatic #2 // Method staticMethod:(Ljava/lang/String;)V
5: new #11 // class com/github/xfc/staticmethod/StaticMethodTest
8: dup
9: invokespecial #12 // Method "<init>":()V
12: astore_1
13: aload_1
14: ldc #13 // String xf
16: invokevirtual #14 // Method normalMethod:(Ljava/lang/String;)V
19: aload_1
20: pop
21: ldc #15 // String chen
23: invokestatic #2 // Method staticMethod:(Ljava/lang/String;)V
26: return
LineNumberTable:
line 52: 0
line 54: 5
line 56: 13
line 58: 19
line 59: 26
LocalVariableTable:
Start Length Slot Name Signature
0 27 0 args [Ljava/lang/String;
13 14 1 palindrome Lcom/github/xfc/staticmethod/StaticMethodTest;
}
字节码比较多,我们一段一段拆开分析。这里也推荐使用比较可读的jclasslib工具。本质与上述字节码文件内容一致。
方法概览
这个java代码重要就是如下四个方法,一个默认的无参数构造方法,以及源码里面实现的三个方法。
非静态方法normalMethod
字节码
public void normalMethod(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=2
0: aload_1
// #2 查询常量池如下#2 com/github/xfc/staticmethod/StaticMethodTest.staticMethod:实际上就是通过类直接调用静态方法
1: invokestatic #2 // Method staticMethod:(Ljava/lang/String;)V
4: return
LineNumberTable:
line 38: 0
line 39: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/github/xfc/staticmethod/StaticMethodTest;
0 5 1 data Ljava/lang/String;
如上述字节码内容,我们的方法实际上只有一个参数,内容如下normalMethod(String data)
,但是上述字节码提示locals=2代表有两个本地变量。所以我们看本地变量表LocalVariableTable
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/github/xfc/staticmethod/StaticMethodTest;
0 5 1 data Ljava/lang/String;
如上述代码所示,第0个变量为this引用。第一个为代码里面的
data字段,所以第0个指令是aload_1,意思是将第2个引用类型本地变量推送至栈顶。这就是因为第0个是
this` 引用。通过上述的内容,说明了非静态方法是含有隐含的对象引用的。
静态方法staticMethodc
字节码
public static void staticMethod(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #4 // class java/lang/StringBuilder
6: dup
7: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
10: ldc #6 // String static:
12: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_0
16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: return
LineNumberTable:
line 46: 0
line 47: 25
LocalVariableTable:
Start Length Slot Name Signature
0 26 0 data Ljava/lang/String;
引用代码中使用了打印语句同时使用了字符串拼接,因此字节码命令变得比较多,这里我们关注重点locals=1
和LocalVariableTable
。本地变量表如下
LocalVariableTable:
Start Length Slot Name Signature
0 26 0 data Ljava/lang/String;
如上本地变量表可知,只有一个data变量,没有this
引用,因此无法调用上面的非静态方法。到此基本知道了为什么静态方法不能调用非静态方法。
接下来扩展分析下为什么通过实例对象可以调用静态方法?
直接说结论:如果你通过实例对象调用静态方法,在编译阶段就会被优化为静态方法调用
代码回顾
同样通过字节码内容分析,这里直接看main方法字节码的关键命令即可
如上图所示,直接调用了静态方法,因此可以说明JVM会优化为直接通过类调用静态方法的形式。如下为反编译内容
结论
- 静态方法属于类,方法的参数列表没有默认隐含的
this
引用,这也说明了调用静态方法不需要当前对象的引用this
,但是调用非静态方法需要当前对象的引用才能调用,所以这就是不能调用非静态方法的主要原因。 - 非静态方法属于实例对象,方法的参数列表有默认隐含的
this
引用,同时调用静态方法不需要this
,因此可以调用。 - 如果强行通过对象调用静态方法,编译器会优化为直接调用静态方法。
个人思考:平时在开发过程中,没有去对很多问题思考和验证,都是想当然的使用。因此偶尔遇到了不明所以的问题,不知道从哪里入手。以后要多思考,多实践。
补充一个指令文档:https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-6.html#jvms-6.5.aload_n