众所周知:类对象整个内存只有一份,静态变量和静态方法属于类而不依赖与实例对象,所以必须通过类名引用。那为什么使用实例对象强制调用静态变量和方法,编译也能通过并且也可以正常运行❓❓❓
首先定义一个用户类,为了区分,设置了实例成员变量name
和静态成员变量age
,以及实例成员方法test1()
和静态成员方法test2()
,test1()
内调用了静态方法test2()
public class RayUser {
public String name = "ray";
public static Integer age = 486;
public void test1(){
System.out.println("this is test1()");
test2();
}
public static String test2(){
System.out.println("this is static test2()");
return "ok";
}
}
在使用IDEA编写测试类的时候就能发现:通过实例对象user调用时,默认只提示了实例成员变量name
和实例成员方法test1()
,并没有提示静态变量age
和静态方法test2()
。
但是可以通过手动强制输入,会发现可以使用实例对象调用静态变量age
和静态方法test2()
(未报错,反常现象),但是通过类名.
的方式调用实例成员变量name
和实例方法test1()
(报错,正常现象),如下图所示:
我们把报错的正常现象先进行注释,保留反常但未报错的代码行然后运行,发现编译竟然通过并且可以运行⁉️
public static void main(String[] args) {
RayUser user = new RayUser();
String name = user.name;
Integer age1 = user.age;
Integer age2 = RayUser.age;
// RayUser.name;
user.test1();
user.test2();
// RayUser.test1();
RayUser.test2();
}
按常规思路,静态变量和静态方法
属于类而不依赖于实例对象
,所以必须通过类名引用,即Integer age2 = RayUser.age;
和RayUser.test2();
才是正确的调用方式。但是通过测试发现Integer age1 = user.age;
和user.test2();
也能正常编译,并且能够正常运行test1()
方法,这就很奇怪了
尝试从底层角度思考,JDK1.8运行时数据区如下图:
类加载器将类的信息(即类的结构信息、常量、静态成员变量static、静态方法、编译器编译后的代码等)加载到内存中,Java虚拟机在方法区(InstanceKlass)和堆区(java.lang.Class)中各分配一个对象去保存类的信息,程序员一般用到的是java.lang.Class,也就是Java虚拟机会将字节码中的信息保存到内存的【方法区】中,所以类对象整个内存只有一份,又因为静态变量和静态方法属于类而不依赖于类的实例对象,所以静态变量和静态方法必须通过类名引用
,即通过类对象访问。而创建出来的对象(即通过new出来的对象)都存在于堆
上。普通成员变量在对象实例化时分配在堆内存中的对象实例中,每个对象实例都有自己的一组实例变量,存储在堆内存中。普通方法的字节码被加载到方法区中,但方法的执行是在栈内存中进行的。每个线程都有自己的栈,栈中包含了方法的调用栈帧,用于存储局部变量和方法调用的相关信息。
所以:静态成员变量和静态方法的生命周期与类的生命周期一致,而普通成员变量和普通方法的生命周期与该类的对象实例的生命周期相关。
从JDK1.8开始,方法区与堆内存开始共享同一个物理内存(但方法区是堆外内存)这种改变是为了提高内存利用率和性能。虽然堆内存和元空间在物理内存上不是完全共享的,但它们之间存在一些交互
。例如,当一个对象实例调用一个静态方法时,JVM会查找该对象实例的类方法区(位于堆内存中的每个对象),然后跳转到元空间中相应的静态方法字节码(由该类的元数据定义),因此,从技术上讲,堆中的实例方法可以调用方法区(元空间)中的static静态方法,因为它们之间存在交互和共享
。所以RayUser中的实例方法test1()
可以调用静态方法test2()
,即运行test1()
不会报错。
虽然静态方法和实例方法分别存储在不同的内存区域(元空间和堆内存),但它们之间可以通过JVM的内部机制进行交互,当一个实例方法调用一个静态方法时,JVM会查找当前对象的类方法区(位于堆内存),然后跳转到元空间中的静态方法字节码(由该类的元数据定义)
虽然通过 JVM 说明了实例方法可以访问静态方法,但还是不能说明实例对象可以调用static的变量和方法,毕竟,一个在堆中(实例对象),一个在方法区(static),实例对象从堆中怎么调的到方法区(堆外内存)中的类对象信息?
从内存的角度分析本问题失败
既然通过已知的知识无法推导出原因,那就从字节码文件去看看到底是怎么回事,是不是 jvm 虚拟机在搞事。通过使用 jclasslib 工具反编译.class
文件看到测试类字节码信息如下:
破案了!可喜可贺!通过字节码文件我们可以看出,编译器会帮忙转译为类名调用,看不懂字节码的小伙伴没关系,可以简单理解为:编译器把user.age;
转化为 -> RayUser.age;
把user.test2();
转化为 -> RayUser.test2();
1️⃣静态方法(static修饰的方法)不能直接调用实例方法,静态变量和静态方法必须通过类名引用;但实例方法可以调用静态方法和静态成员变量,原因在于静态方法属于类层面的方法,所有实例对象都拥有该方法。而实例方法则存在于堆中,每创建一个实例就会产生一个对应的实例方法,它们之间是独立的。如果在静态方法中直接调用非静态方法可能会造成错误,例如访问未初始化的变量。所以由于实例方法属于实例对象,而静态方法依赖于类只有一个,所以静态方法调用实例方法就会不知道调用哪个实例对象的实例方法,所以不允许这么做;但是实例方法无论是哪一个,都能调用唯一的静态方法,这是允许的,它们位于不同的内存区但是共享同一个物理内存
。然而,如果需要在静态方法中调用实例方法,可以通过创建类的实例,然后通过这个实例来访问其实例方法。这种方式就可以避免访问未初始化的实例变量的问题。
2️⃣从底层角度:实例对象不能调用静态变量和静态方法❗️即使使用实例对象调用静态变量和方法,编译器也会帮忙转译为类名调用
❗️但会无谓增加编译器解析成本,所以即使能够编译且运行也不推荐这么做
如果觉得这篇文章对您有所帮助的话,请动动小手点波关注,你的点赞收藏⭐️转发评论都是对博主最好的支持~