3

前言

《Java的构造函数与默认构造函数(深入版)》介绍Java对象初始化过程时,提到了实例变量。本文介绍Java中包括实例变量在内的几种变量,以及它们的作用域。(咳,本文结语有一个小总结哦~)

(若文章有不正之处,或难以理解的地方,请多多谅解,欢迎指正)

变量

初学Java时,我们一般会将变量分为成员变量和局部变量,类中方法外的变量是成员变量,类中方法内的变量是局部变量。举个栗子:

public class Test{  
     int a = 100;  
     public void test(){  
        int b = 200;  
     }  
}

结合《Java的构造函数与默认构造函数(深入版)》提到的对象创建的过程可以知道,成员变量在使用对象之前就加载好,而局部变量需要在类或对象调用方法时才会创建

个人认为,这种分类方式有点粗糙,以下是比较详细的变量分类方式:
变量分类2.jpg
可能会有读者觉得,凭什么这么分呢?有没有依据?

有滴,按照作用域和加载顺序

按作用域划分变量类型

成员变量

成员变量.jpg
在这里,成员变量分为类变量和实例变量。类变量是类加载过程中的准备阶段就已经分配内存了,直至类被销毁,类变量的内存才会释放。而实例变量是在类的实例创建(创建对象)时存在直至实例被销毁

访问类变量的方式有两种:类.类变量、实例.类变量。除了类本身可以对类变量进行修改外,类的实例也会对类变量进行修改,且其他实例也会看到变化

访问实例变量的方式就只有一种:实例.实例变量。每个实例的实例变量都不对其他实例可见

其实类变量和实例变量,有点像是《火影忍者》里面的影分身之术。类是本体,而实例是类的分身,实例变量随着实例同生共死,类变量随着类同生共死。而实例之间的实例变量也可以不同。

举个栗子:

public class Test {  
     public static void main(String[] args) {  
         Naruto naruto1 = new Naruto();  //鸣人影分身1  
         Naruto naruto2 = new Naruto();  //鸣人影分身2  
         naruto1.a += 5;  
         naruto1.b += 5;  
         naruto2.b += 10;  
         System.out.println(naruto2.a);  
         System.out.println(naruto1.b);  
         System.out.println(naruto2.b);  
     }  
}  
class Naruto {  //鸣人  
     static int a = 100;  
     int b = 200;  
}

运行结果为:

105  
205  
210

局部变量

局部变量2.jpg
局部变量在此分为形参、方法局部变量和代码块局部变量。形参是方法签名上的局部变量,当对象调用方法时传入了实参,但是传入方法的过程中会创建一个形参,作为值传递的副本方法局部变量是在方法中创建变量。而代码块局部变量,即类中定义好的代码块

而代码块局部变量中,类代码块和实例代码块的区别在于加载顺序。举个栗子:

public class Test {  
     {  
        System.out.println("我是实例代码块");  
     }  
     static{  
        System.out.println("我是类代码块");  
     }  
     public static void main(String\[\] args) {  
        new Test();  
     }  
}

运行结果为:

我是类代码块  
我是实例代码块

可以看到,类代码块在实例代码块被加载之前就已经加载了,原因与类变量和实例变量的区别相似,类代码块是在在类加载过程中就已经被加载了,而实例变量是在类加载之后、对象初始化过程中才加载。

咦,是不是要讲按加载顺序划分变量类型了?是的!不过在解释之前,请做下这道题,其输出结果是什么:

public class Test{  
     static int a = 100;  
     int b = 200;  
     {  
         int c = 400;  
         System.out.println(c);  
     }  
     static{  
         int d = 500;  
         System.out.println(d);  
     }  
     public void test(int f){  
         int e = 300;  
         System.out.println(e);  
         System.out.println(f);  
     }  
     public static void main(String[] args){  
         Test t = new Test();  
         System.out.println(a);  
         System.out.println(t.b);  
         t.test(600);  
     }  
}

运行结果为:

500  
400  
100  
200  
300  
600

通过上述对变量的介绍,可以得到答案。先看主函数有没有创建对象,有创建对象的话看对应类中代码块有没有输出语句,然后返回主函数,依次执行语句和访问方法。可以看出,这六个变量的加载顺序如下(至于为什么类变量和类代码块为什么写在一起,详情请看《Java的构造函数与默认构造函数(深入版)》):
变量加载顺序2.jpg
但是这六种变量为什么是这样的加载顺序?

按虚拟机加载顺序划分变量类型

小编大胆猜测各位看官没点击上面的链接(心情复杂.jpg)。为了下文更加容易理解,把那篇文章的结论之一放这儿:虚拟机是按类变量和类代码块在源码中的编写顺序来加载的,同理可得,实例变量和实例代码块也是按照源码中的编写顺序来加载的。

下文中,我们用类加载->创建对象->调用方法的顺序来介绍变量的加载顺序。

类加载

对象创建过程2.jpg
《Java的继承(深入版)》中提到,在类加载过程中的准备阶段,会为static修饰的变量分配内存,而static修饰的代码块也是一样的,都是由invokespecial指令来对其进行调用。也就是说,类变量和类代码块在类加载过程就已经加载到内存中了。

创建对象

对象初始化3.jpg

实例变量和实例代码块是在创建对象后,进行对象初始化的时候才加载到内存中

调用方法

《“Java有值传递和引用传递”为什么错了?》有提到过在Java中只有值传递

在调用有参函数的时候,虚拟机会将实参复制后,生成形参,实参和形参的值相同,但是内存地址不同,即形参相对于实参来说,只是另一个有着同样的值的变量

所以在有参函数调用的过程中,形参先于方法局部变量被加载
变量加载顺序2.jpg

我们再来回顾这张图,这个过程不就是对象调用方法的这一过程的加载顺序吗?

结语

这篇文章里小编穿插了很多之前写过的文章,因为其实这些知识点都是相互穿插的,于是“点成线、线成面”这样形成一个体系,虽然这些文章里面多少有点虚拟机的内容,对于一些刚接触Java的读者可能超纲了(没事,收藏以后用得着的)。

如果觉得文章不错,请点一个赞吧,这会是我最大的动力~

接下来是《Java基础——面向对象篇》的小总结

什么是面向对象

面向对象与面向过程

面向对象的三大基本特征和五大基本原则

平台无关性

Java的平台无关性是怎么实现的?

不来了解下JVM支持的语言有哪些?

值传递

“Java有值传递和引用传递”为什么错了?

封装、继承、多态

Java的继承(深入版)

Java的多态(深入版)

Java的构造函数与默认构造函数(深入版)

以及本文《Java的成员变量、局部变量(深入版,附总结)》

参考资料:

java变量之成员变量和局部变量以及它们的运行机制

《深入理解Java虚拟机》


NYforSF
60 声望18 粉丝