本文将阐述关于Java语言中变量是如何存储的。
Java中数据的存储位置
- 寄存器
最快的存储区,位于处理器的内部,但是数量极其有限。所以寄存器根据寻求进行自动分配,无法直接人为控制。
- 栈内存
位于RAM当中,通过堆栈指针可以从处理器获得直接支持。堆栈指针向下移动,则分配新的内存;向上移动,则释放那些内存。这种存储方式速度仅次于寄存器。(常用的存放对象引用和基本数据类型,而不用存储对象)
- 堆内存
一种通用的内存池,也位于RAM当中。其中存放的数据由JVM自动进行管理。堆相对于栈的好处来说:编译器不需要知道存储的数据在堆里存活多久。当需要一个对象时,使用new创建对象(如new List()),当执行这行代码时,会自动在堆里进行存储分配。同时,因为以上原因,用堆进行数据的存储分配和清理,需要花费更多的时间。
- 常量池(public static final)
常量(字符串常量和基本类型常量)通常直接存储在程序代码内部(常量池)。这样做是安全的,因为它们的值在初始化时就已经被确定,并不会改变。常量池在Java用于保存在编译期已确定的,已编译的class文件中的一份数据。它包括了关于类,方法,接口等中的常量,也包括字符串常量,如String。
- 静态域
存放静态成员(static定义的)
- 非RAM存储(硬盘等永久存储的空间)
如果数据完全存活于程序外,那么它可以不受程序的任何控制,在程序没有运行时也可以存在。其中两个基本例子是:流对象和持久化对象。
这里主要侧重于栈,堆和常量池,对于栈和常量池中的对象可以共享,对于堆中的对象不可以共享。栈中的数据大小和声明周期是可以确定的,当没有引用指向数据时,这个数据就会消失。堆中的对象由垃圾回收器负责回收,因此大小和生命周期不需要确定,具有很大的灵活性。
对于字符串,其对象的引用都是存储在栈中的,如果是编译期已经创建好的(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的)才能确定的就存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。
public class StringTest {
String s1 = "best";
String s2 = "best";
String s3 = "best";
String ss1 = new String("best");
String ss2 = new String("best");
String ss3 = new String("best");
}
对于通过new产生的一个字符串(上文的“best”)时,会先去常量池中查找是否已经有了“best”对象,如果没有则在常量池中创建一个此字符串对象,然后堆中在创建一个常量池“best”对象的拷贝对象。譬如面试题:String s = new String("abc");产生了几个对象?如果常量池中原来没有"abc",就是两个。对于基础类型的变量和常量,变量和引用存储在栈中,常量存储在常量池中。
public class StringTest {
int i1 = 8;
int i2 = 8;
int i3 = 8;
public static final int INT1 = 9;
public static final int INT2 = 9;
public static final int INT3 = 9;
}
对于成员变量和局部变量,成员变量就是方法外部,类的内部定义的变量;局部变量就是方法或语句块内部定义的变量。局部变量必须初始化。形式参数是局部变量,局部变量的数据存在于栈内存中。栈内存中的局部变量随着方法的消失而消失。
public class BirthDate {
private int day;
private int month;
private int year;
public BirthDate(int d, int m, int y) {
this.day = d;
this.month = m;
this.year = y;
}
// get, set方法省略
}
public class BirthTest {
public static void main(String[] args) {
int date = 9;
BirthTest test = new BirthTest();
test.change(date);
BirthDate day = new BirthDate(9,9,2000);
}
public void change(int i) {
i = 123;
}
}
对于上述代码,date为局部变量,day,month,year为成员变量,下面分析一下代码执行时候的变化:
- main方法开始执行:int date = 9;date局部变量,基础类型,引用和值都存在栈中。
- BirthTest test = new BirthTest(); test为对象引用,存在栈中,对象(new BirthTest()存在堆中)。
- test.change(date); change方法中 i 为局部变量,引用和值存在栈中。当方法change执行完毕后,i 就会从栈中消失。
- BirthDate day = new BirthDate(9,9,2000); day 为对象引用,存在栈中,对象(new BirthDate(9,9,2000))存在于堆中,其中 d,m,y 为局部变量存储在栈中,且它们的类型为基本类型,因此它们的数据也存储在栈中。day,month,year为成员变量,它们存储在堆中(new BirthDate()里面)。当BirthDate构造方法执行完毕后,d,m,y 将从栈中消失。
- main方法执行完毕后,date 变量,test, day 引用将从栈中消失,new BirthTest(),new BirthDate() 将等待垃圾回收。
Java变量定义注意事项
- 常量定义的基本注意事项
- 在Java语言中,主要是利用final关键字(Java类中灵活使用static关键字)来定义常量。
- 当常量被设定后,一般情况下就不允许再进行更改。如可以利用如下的形式来定义一个常量:final double PI = 3.14;
- 在定义常量时,需要注意如下内容:
- 常量在定义的时候就需要对常量进行初始化。也就是说,必须要在常量声明的时候对其进行初始化这跟局部变量或者成员变量不同。当在常量定义的时候初始化过后,在应用程序中就再也无法对这个常量进行赋值。如果强行赋值的话,会跳出错误的信息,并拒绝接受这一个新的值。
- final关键字使用的范围。这个final关键字不仅可以用来修饰基本数据类型的常量,还可以用来修饰对象的引用或者方法。为此可以使用final关键字来定义一个常量的数组。这就是Java语言中的一个很大的特点。一旦一个数组对象被final关键字设置为常量数组之后,它只能够恒定的指向一个数组对象,无法将其改变指向另外一个对象,也无法更改数组中的值。
- 需要注意常量的命名规则。在Java语言中,定义常量的时候,也有自己的一套规则。如在给常量取名的时候,一般都用大写字符。另外,在常量中,往往通过下划线来分割不同的字符。而不像对象名或者类名那样,通过首字符大写的方式来进行分割。
总之,Java开发人员需要注意,被定义为final的常量需要采用大写字母命名,并且中间最好使用下划线作为分割符来进行连接多个单词。在定义final的数据不论是常量、对象引用还是数组,在主函数中都不可以更改。否则的话,会被编译器拒绝并提示错误信息。
public class FinalDemo {
public final int MAX = 300;
public FinalDemo() {
System.out.println("FinalDemo构造函数调用");
}
}
public class FinalTest {
public static void main(String[] args) {
FinalDemo f = new FinalDemo();
System.out.println(f.MAX);
}
}
============
结果
============
FinalDemo构造函数调用
300
如图片所示,在主函数修改常量时,编译器无法通过。总之,常量定义以后,再次赋值会报错。
- final关键字与static关键字同时使用
由于Java是面向对象的语言,所以在定义常量的时候与其他编程语言还是有区别的。如一段程序代码从编译到最后执行,需要两个过程,分别为代码的装载与对象的建立。不同的过程对于常量的影响是不同的。现在假设有如下的代码:
private static Random rd1 = new Random();
private final int int1 = rd1.nextInt(10);
private static final int int2 = rd1.nextInt(10);
注意,上述代码第2,3行的区别。第三行代码使用了static关键字,当使用static修饰一个变量的时候,在创建实例对象之前就会为这个变量在内存中创建一个存储空间。以后创建对象,如果需要用到这个静态变量,那么就会共享这个变量的存储空间。也就是说,再次创建对象的时候,如果用到这个变量,那么系统不会为其再分配一个存储空间,而只是将这个内存存储空间的地址赋值给它。如此做的好处就是可以让多个对象采用相同的初始变量。当需要改变多个对象中变量的值得时候,只需要改变一次即可。从这个特性上说,其跟常量的作用比较类似。不过并不能够取代常量的作用。
那么上述代码第2,3条语句有差别吗?首先,private final int int1 = rd1.nextInt(10); 这条语句。虽然 int1 也是一个常量,但是其是在对象建立的时候初始化的。如果现在需要创建两个对象,那么需要对这个变量初始化两次。而在两次初始化的过程中,由于生成的随机数不同,所以常量初始化的值也不同。最后的结果是,虽然 int1 是常量,但是在不同的对象中,其值有可能是不同的。可见定义为final的常量并不是恒定不变的。因为默认情况下,定义的常量是在对象建立的时候被初始化。如果在建立常量的时候,直接赋值一个固定的值,而不是通过其他对象或者函数来赋值,那么这个常量的值就是恒定不变的,即在多个对象中值也是相同的。但是如果在给常量赋值的时候,采用的是一些函数或者对象(如生成随机数的Random函数),那么每次建立对象时其给对象的初始化就有可能不同。这往往是程序开发人员不愿意看到的。有时候程序开发人员希望建立再多的对象,其在多个对象中引用常量的值都是相同的。
要实现这个需求的话,有两个方法。一是在给常量赋值的时候,直接赋予一个固定的值,如123等等。而不是一个会根据环境变化的函数或者对象。像生成随机数的对象,每次运行时其结果都有可能不同。利用这个对象来对变量进行初始化的时候,那么结果可能在每次创建对象时这个结果都有可能不同。最后这个常量只能够做到一个对象内是恒定不变的,而无法做到在一个应用程序内是恒定不变的。另外一个方法,就是将关键字static与关键字final同时使用。一个被定义为final的对象引用或者常量只能够指向唯一的一个对象,不能再指向其他对象。但是,正如上面举的一个随机数的例子,对象本身的内容的值是可以改变的。为了做到一个常量在一个应用程序内真的不被改变,就需要将常量声明为static final的常量。意思是,当执行一个应用程序的时候,可以分为两步,分别为代码装载和对象创建。为了确保在所有情况下(即创建多个对象情况下)应用程序还能够得到一个相同值得常量,那么就最好告诉编译器,在代码装载的时候就初始化常量的值。然后在后续创建对象的时候,只引用这个常量对象的地址,而不是对其进行再次的初始化。如此,在后续的多次创建对象后,这个常量int2的值都是相同的。因为在创建对象的时候,只是引用这个常量,而不会对这个常量再次进行初始化。
public class FinalDemo {
public static Random rd1 = new Random();
public final int int1 = rd1.nextInt(10);
public static final int int2 = rd1.nextInt(10);
}
public class FinalTest {
public static void main(String[] args) {
FinalDemo f1 = new FinalDemo();
FinalDemo f2 = new FinalDemo();
System.out.println("f1.int1: " + f1.int1);
System.out.println("f2.int1: " + f2.int1);
System.out.println("----------------");
System.out.println(f1.int2);
System.out.println(f2.int2);
}
}
============
结果
============
f1.int1: 0
f2.int1: 1
----------------
6
6
由于加上这个static关键字之后,相当于改变了常量的作用范围。为此程序开发人员需要了解自己的需求,然后选择是否需要使用这个关键字。在初始化常量的时候,如果采用函数(如系统当前时间)或者对象(如生成随机数的对象)来初始化常量,可以预见到在每次初始化这个常量时可能得到不同的值,就需要考虑是否要采用这个static关键字。一般情况下,如果只需要保证在对象内部采用这个常量的话,那么这个关键字就可有可无的。但是反过来,如果需要在多个对象中引用这个常量,并且需要其值相同,那么就必须要采用 static 这个关键字了。以确保不同对象中都只有一个常量的值。或者说,不同对象中引用的常量其实指向的是内存中的同一块区域。
网友评论