静态变量、实例变量和局部变量以及final所修饰的变量的默认初始值问题
问题:
(1)这里我们来探讨一下成员变量和局部变量的默认初始值问题,当成员变量和局部变量声明之后不进行初始化赋值操作,就在接下去的代码中使用它们,比如说输出它们,会出现一个什么问题?
(2)如果成员变量被final关键字所修饰,只声明而不显式初始化赋值,JVM依然会帮成员变量隐式初始化吗?
前提:
在解决问题之前,我们先来理清楚一些问题,什么是成员变量?什么是局部变量?
- 成员变量分为静态变量(类变量)和实例变量。
- 静态变量就是在类中,方法之外被static关键字所修饰的变量。
- 实例变量就是对象的变量,在类中,方法之外的普通成员变量。
- 局部变量就是方法内的变量。
- 局部变量包括形参,方法局部变量,代码块局部变量,存在于方法的参数列表,方法体内和代码块(包括静态代码块)中,属于方法的范畴。
了解了这些概念的前提下,我们就可以看以下的代码了。
测试一
分别对成员变量和局部变量进行只声明而不初始化的测试
代码(一):
成员变量测试
public class MemberVar {
//静态变量只声明而没有赋值,JVM会对静态变量进行隐式的默认值初始化
static int sI;
static Integer sInteger;
static char sC;
static String sString;
static boolean sB;
static Boolean sBoolean;
//实例变量只声明而没有初始化,JVM也会对实例变量进行隐式的默认值初始化
int oInt;
char oChar;
public static void main(String[] args) {
System.out.println("静态变量 - int的基本类型和包装类型:");
System.out.println("sI="+sI);
System.out.println("sInteger="+sInteger);
System.out.println("静态变量 - char型和String型:");
System.out.println("sC="+sC);
System.out.println("sString="+sString);
System.out.println("静态变量 - boolean型和Boolean型:");
System.out.println("sB="+sB);
System.out.println("sBoolean="+sBoolean);
System.out.println("实例变量 - int,char的基本类型");
MemberVar memberVar = new MemberVar();
System.out.println(memberVar.oInt);
System.out.println(memberVar.oChar);
}
}
输出结果:
静态变量 - int的基本类型和包装类型:
sI=0
sInteger=null
静态变量 - char型和String型:
sC='\u0000' (结果无法复制,实际效果是一个小方块空格)
sString=null
静态变量 - boolean型和Boolean型:
sB=false
sBoolean=null
实例变量 - int,char的基本类型
0
'\u0000'
代码(二):
局部变量测试
package variable;
public class LocalVar {
public static void main(String[] args) {
//基本类型
int a = 0;
int b = 0;
int c; //局部变量只声明没初始化
//引用类型
Integer i = null;
Integer j = null;
Integer k ; //局部变量只声明没初始化
System.out.println(a);
System.out.println(b);
//System.out.println(c); //编译器报错,The local variable c may not have been initialized
System.out.println(i);
System.out.println(j);
//System.out.println(k); //编译器报错,The local variable c may not have been initialized
}
}
结论:
- 成员变量(静态变量,实例变量)只声明而不显式初始化,在JVM加载对应的类时,会给未初始化的成员变量进行隐式的默认值赋值。因为成员变量都是存储在内存区的堆中,堆中的数据是会进行默认值的赋值。
- 局部变量只声明而不显式初始化,虚拟机是不会为局部变量隐式初始化的,因为局部变量存储在内存区的栈区中,而不是堆区。
注意:
局部变量没有初始化情况下,是不允许被调用的,因为该变量没有指向任何值。但是也分为两种情况
- 局部变量只声明而不初始化,就被使用的情况下,是无法通过编译器的,会出现错误(本地变量需要初始化),因为本地变量是存放在内存区的栈中,所以要调用前必须显式初始化。
- 局部变量只声明而不初始化,在接下去的代码中没有使用,是可以通过编译器检查的,是不会报错的,因为该变量没有在其他地方得到调用,在编译期间编译器是会将该声明当做垃圾而删除(后面会证实)
测试二
假如成员变量和局部变量被final所修饰,默认值赋值的情况会是怎么样?
public class FinalVar {
final static int a = 0;
final static Integer b = 0;
//final static int c; //只声明而不赋值,就报错,The blank final field c may not have been initialized
//final static Integer d;
final int i = 0;
final Integer j = 0;
//final int k; //只声明而不赋值,就报错,The blank final field k may not have been initialized
//final Integer l;
public static void main(String[] args) {
final int localA; //局部变量只声明没有初始化,在没有使用的情况下是不会报错的,与final无关。
final Integer localB; //与final成员变量不同,编译器允许局部变量延缓进行初始化,但是final局部变量也只能被赋值一次
localA = 0;
//localA = 1; //The final local variable localA may already have been assigned,final变量不允许第二次赋值
}
}
结论
- final成员变量是不可变的变量,所以JVM是不会隐式初始化,所以必须显式地进行初始化操作,final成员变量初始化只有两种选择,第一种是在声明时进行初始化赋值。第二种是声明之后,不赋值,通过构造器去this.引用的方式初始化。否则即使没有被调用,也是会报编译错误的
- final局部变量与局部变量一样,都是不会隐式初始化,必须显示地进行初始化操作,但是可以声明之后不赋值,可以延缓在之后代码中进行第一次初始化赋值操作,但第一次赋值之后则不再允许赋值了。同样没经任何调用的final局部变量在编译时该声明会被删除。
测试三
通过jd-gui工具对编译后的class文件进行反编译,查看区别
源代码:
public class FinalVar {
final static int a = 0;
final static Integer b = 0;
static int c;
static Integer d;
final int i = 0;
final Integer j = 0;
int k;
Integer l;
public static void main(String[] args) {
final int localA;
localA = 0;
final Integer localB;
int localC;
localC = 0;
Integer localD;
}
}
编译后的代码:
public class FinalVar
{
static final int a = 0;
static final Integer b = Integer.valueOf(0);
static int c;
static Integer d;
final int i = 0;
final Integer j = Integer.valueOf(0);
int k;
Integer l;
public static void main(String[] args)
{
int localA = 0;
int localC = 0
}
}
结论:
- 声明了而没初始化的局部变量,无论是有无
final
修饰,在编译期间,是会被编译器当做垃圾代码删除掉的,如localB
,localD
两个局部变量的声明。 - 局部变量的延迟初始化的代码,会被编译器所优化。如
int localA; localA = 0
会别优化成一句话int localA = 0
- Integer的装箱问题,这里先不坐出讨论。
(所以我们可以知道为什么局部变量可以延迟初始化而成员变量不允许了。因为成员变量未初始化,虚拟机也会为其隐式初始化,而编译器也是不会认为未初始化且未使用的成员变量是垃圾代码。这一点则不同于局部变量。举个例子,一个final的成员变量没有使用构造器初始化,也没有在声明时用赋值语句初始化,在之后的代码中,我们也不为它进行初始化。该成员变量因为是final变量,所以虚拟机也不会为其初始化,同时编译器也不会删除成员变量的声明。那么这么一个未存储任何值的全局成员变量在类加载的时候会被分配到内存中。这就有点奇怪了)
最终结论:
- 成员变量(静态变量,实例变量)具有默认值,只声明不初始化,JVM会为其隐式初始化。因为成员变量存储在堆中
- 局部变量没有默认值,不会被JVM隐式初始化,因为局部变量存储在栈中
- 被
final
修饰的变量,无论是成员变量还是局部变量,都是不可变的变量,都必须显式地进行初始化操作,且只能被赋值一次。 - 没有被初始化的局部变量,在没有被使用的情况下,是可以通过编译。但是如果在接下去的代码中使用到了,则是无法通过编译的,会提示该成员变量需要初始化。
- 没有被初始化的局部变量(无论有无final修饰),在编译期间,该声明都会被编译器当做垃圾代码删除。
补充:
- 基本类型变量都有属于自己的默认值,而引用类型的默认值都是null
DataType | Default Vaule |
---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
char | ‘\u0000’ |
boolean | false |
引用型变量 | null |