从对象深入分析 Java 中实例变量和类变量的区别

实例变量 和 类变量

局部变量

特点:作用时间短,存储在方法的栈内存中

种类:

  • 形参:方法签名中定义的局部变量,由方法调用者负责为其赋值,随方法结束而消亡
  • 方法内的局部变量:方法内定义的局部变量,必须在方法内对其进行显示初始化,从初始化后开始生效,随方法结束而消亡
  • 代码块内的局部变量:在代码块中定义的局部变量,必须在代码块中进行显示初始化,从初始化后开始生效,随代码块结束而消亡

成员变量

类体内定义的变量,如果该成员变量没有使用 static 修饰,那该成员变量又被称为非静态变量或实例变量,如果使用 static 修饰,则该成员变量又可被称为静态变量或类变量。

实例变量和类变量的属性

使用 static 修饰的成员变量是类变量,属于该类本身,没有使用 static 修饰的成员变量是实例变量,属于该类的实例,在同一个类中,每一个类只对应一个 Class 对象,但每个类可以创建多个对象。

由于同一个 JVM 内的每个类只对应一个 CLass 对象,因此同一个 JVM 内的一个类的类变量只需要一块内存空间;但对于实例变量而言,该类每创建一次实例,就需要为该实例变量分配一块内存空间。也就是说,程序中创建了几个实例,实例变量就需要几块内存空间。

这里我想到一道面试题目:

1
2
3
4
5
6
7
8
9
10
11
12
public class A{
{
System.out.println("我是代码块");
}
static{
System.out.println("我是静态代码块");
}
public static void main(String[] args) {
A a = new A();
A a1 = new A();
}
}

结果:

1
2
3
我是静态代码块
我是代码块
我是代码块

静态代码块只执行一次,而代码块每创建一个实例,就会打印一次。

实例变量的初始化时机

程序可在3个地方对实例变量执行初始化:

  • 定义实例变量时指定初始值
  • 非静态初始化块中对实例变量指定初始值
  • 构造器中对实例变量指定初始值

上面第一种和第二种方式比第三种方式更早执行,但第一、二种方式的执行顺序与他们在源程序中的排列顺序相同。

同样在上面那个代码上加上一个变量 weight 的成员变量,我们来验证下上面的初始化顺序:

1、定义实例变量指定初始值非静态初始化块对实例变量指定初始值 之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class A{
{
weight = 2.1;
System.out.println("我是代码块");
}
double weight = 2.0;
static{
System.out.println("我是静态代码块");
}
public static void main(String[] args) {
A a = new A();
A a1 = new A();
System.out.println(a.weight);
}
}

结果是:

1
2
3
4
我是静态代码块
我是代码块
我是代码块
2.0

2、定义实例变量指定初始值非静态初始化块对实例变量指定初始值 之前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class A{
double weight = 2.0;
{
weight = 2.1;
System.out.println("我是代码块");
}
static{
System.out.println("我是静态代码块");
}
public static void main(String[] args) {
A a = new A();
A a1 = new A();
System.out.println(a.weight);
}
}

结果为:

1
2
3
4
我是静态代码块
我是代码块
我是代码块
2.1

大家有没有觉得很奇怪?

我来好好说清楚下:

定义实例变量时指定的初始值、初始代码块中为实例变量指定初始值的语句的地位是平等的,当经过编译器处理后,他们都将会被提取到构造器中。也就是说,这条语句 double weight = 2.0; 实际上会被分成如下 2 次执行:

  • double weight; : 创建 Java 对象时系统根据该语句为该对象分配内存。
  • weight = 2.1; : 这条语句将会被提取到 Java 类的构造器中执行。

只说原理,大家肯定不怎么信,那么还有拿出源码来,这样才有信服能力的吗?是不?

这里我直接使用软件将代码的字节码文件反编译过来,看看里面是怎样的组成?

第一个代码的反编译源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class A
{
double weight;
public A()
{
this.weight = 2.1D;
System.out.println("我是代码块");
this.weight = 2.0D;
}
static
{
System.out.println("我是静态代码块");
}
public static void main(String[] args)
{
A a = new A();
A a1 = new A();
System.out.println(a.weight);
}
}

第二个代码反编译源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class A
{
double weight;
public A()
{
this.weight = 2.0D;
this.weight = 2.1D;
System.out.println("我是代码块");
}
static
{
System.out.println("我是静态代码块");
}
public static void main(String[] args)
{
A a = new A();
A a1 = new A();
System.out.println(a.weight);
}
}

这下子满意了吧!

通过反编译的源码可以看到该类定义的 weight 实例变量时不再有初始值,为 weight 指定初始值的代码也被提到了构造器中去了,但是我们也可以发现之前规则也是满足的。

他们的赋值语句都被合并到构造器中,在合并过程中,定义的变量语句转换得到的赋值语句,初始代码块中的语句都转换得到的赋值语句,总是位于构造器的所有语句之前,合并后,两种赋值语句的顺序也保持了它们在 Java 源代码中的顺序。

大致过程应该了解了吧?如果还不怎么清楚的,建议还是自己将怎个过程在自己的电脑上操作一遍,毕竟光看不练假把式。

类变量的初始化时机

JVM 对每一个 Java 类只初始化一次,因此 Java 程序每运行一次,系统只为类变量分配一次内存空间,执行一次初始化。程序可在两个地方对类变量执行初始化:

  • 定义类变量时指定初始值
  • 静态初始化代码块中对类变量指定初始值

这两种方式的执行顺序与它们在源代码中的排列顺序相同。

还是用上面那个示例,我们在其基础上加个被 static 修饰的变量 height:

1、定义类变量时指定初始值静态初始化代码块中对类变量指定初始值 之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class A{
double weight = 2.0;
{
weight = 2.1;
System.out.println("我是代码块");
}
static{
height = 10.1;
System.out.println("我是静态代码块");
}
static double height = 10.0;
public static void main(String[] args) {
A a = new A();
A a1 = new A();
System.out.println(a.weight);
System.out.println(height);
}
}

运行结果:

1
2
3
4
5
我是静态代码块
我是代码块
我是代码块
2.1
10.0

2、定义类变量时指定初始值静态初始化代码块中对类变量指定初始值 之前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class A{
static double height = 10.0;
double weight = 2.0;
{
weight = 2.1;
System.out.println("我是代码块");
}
static{
height = 10.1;
System.out.println("我是静态代码块");
}
public static void main(String[] args) {
A a = new A();
A a1 = new A();
System.out.println(a.weight);
System.out.println(height);
}
}

运行结果:

1
2
3
4
5
我是静态代码块
我是代码块
我是代码块
2.1
10.1

其运行结果正如我们预料,但是我们还是看看反编译后的代码吧!

第一种情况下反编译的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class A
{
double weight;
public A()
{
this.weight = 2.0D;

this.weight = 2.1D;
System.out.println("我是代码块");
}
static
{
System.out.println("我是静态代码块");
}
static double height = 10.0D;
public static void main(String[] args)
{
A a = new A();
A a1 = new A();
System.out.println(a.weight);
System.out.println(height);
}
}

第二种情况下反编译的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class A
{
static double height = 10.0D;
double weight;
public A()
{
this.weight = 2.0D;

this.weight = 2.1D;
System.out.println("我是代码块");
}
static
{
height = 10.1D;
System.out.println("我是静态代码块");
}
public static void main(String[] args)
{
A a = new A();
A a1 = new A();
System.out.println(a.weight);
System.out.println(height);
}
}

通过反编译源码,可以看到第一种情况下(定义类变量时指定初始值静态初始化代码块中对类变量指定初始值 之后):

我们在 静态初始化代码块中对类变量指定初始值 已经不存在了,只有一个类变量指定的初始值 static double height = 10.0D; , 而在第二种情况下(定义类变量时指定初始值静态初始化代码块中对类变量指定初始值 之前)和之前的源代码顺序是一样的,没啥区别。

上面的代码中充分的展示了类变量的两种初始化方式 :每次运行该程序时,系统会为 A 类执行初始化,先为所有类变量分配内存空间,再按照源代码中的排列顺序执行静态初始代码块中所指定的初始值和定义类变量时所指定的初始值。

×

纯属好玩

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 实例变量 和 类变量
    1. 1.1. 局部变量
    2. 1.2. 成员变量
    3. 1.3. 实例变量和类变量的属性
    4. 1.4. 实例变量的初始化时机
    5. 1.5. 类变量的初始化时机
,