我们首先看看下面代码:
package cn.com.sxia;
public class RefObj {
public static void main(String[] args) {
String str1;//@1
String str2 = new String();//@2
System.out.println(str2.equals(""));
System.out.println(str1.equals(""));
}
}
代码是在定义字符串,我想所有学过编程语言的人都知道这是怎么回事,但是二者是有区别的:@1这里只是创建了字符串对象的引用,而实际的对象是没有被创建的,@2既创建了字符串对象的引用也创建了引用对应的对象(其实就是我们经常做的初始化操作),大家看到代码如果我们使用str2对象程序可以正常运行,但是如果我们同样去使用str1时候,程序会报编译错误,错误内容如下:
The local variable str1 may not have been initialized
那么在java里引用到底是怎么定义的呢?
引用(reference):能操作java对象的标示符被称为引用。
我们再看上面的例子,引用就是指str1和str2两个标示符,有人一定会奇怪:我们要的是对象,你说现在我们看到只是对象的标示符,那么对象到哪里去了啊?要回答这个问题就要说说对象和引用的关系了,他们的关系是:对象和引用的关系我们可以想象成遥控器(引用)和电视机(对象),用户通过掌握遥控器来控制对电视机的使用,我们要转移对电视机的操作权也就是转移遥控器的控制权而已。
这样的存储和操作对象的方式在编程语言里并不神秘,其实java里的方式还算比较简单的,总比C或者C++语言里的指针要简单许多。这里我也要强调下,有很多人认为java里的引用就是C里面的指针,但是很多经典的java的书籍里都否认这样的说法,我以前面试有人这么问过我,但是那时我就回答的是指针,虽然很多时候面试并没有否定,但也有人会刨根问底,最后搞的你不知道如何回答是好,其实java里的引用可以说成指针,但是这个指针是有限的指针,用户无法控制的指针,我想要是再碰到这样的问题我就说java里的引用就是被阉割的指针。
编程语言里变量的存储归结到计算机的底层都是内存的分配问题,知道java语言里数据在内存的分配方式,对我们编写程序一定会有帮助的,在java里有六种不同的存储方式:
寄存器:这是最快的保存区域,因为它位于和其他所有保存方式不同的地方:处理器内部。然而,寄存 器的数量十分有限,所以寄存器是根据需要由编译器分配。我们对此没有直接的控制权,也不可能在自己的 程序里找到寄存器存在的任何踪迹。
堆栈:驻留于常规 RAM (随机访问存储器)区域,但可通过它的“堆栈指针”获得处理的直接支持。堆 栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。这是一种特别快、特别有效的数据保存 方式,仅次于寄存器。创建程序时,Java 编译器必须准确地知道堆栈内保存的所有数据的“长度”以及“存 在时间”。这是由于它必须生成相应的代码,以便向上和向下移动指针。这一限制无疑影响了程序的灵活 性,所以尽管有些 Java 数据要保存在堆栈里——特别是对象句柄,但Java 对象并不放到其中。
堆:一种常规用途的内存池(也在 RAM 区域),其中保存了Java 对象。和堆栈不同,“内存堆”或 “堆”(Heap )最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要 在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用new 命 令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然 会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!
静态存储:这儿的“静态”(Static)是指“位于固定位置”(尽管也在RAM 里)。程序运行期间,静 态存储的数据将随时等候调用。可用 static 关键字指出一个对象的特定元素是静态的。但 Java 对象本身永 远都不会置入静态存储空间。
常数存储:常数值通常直接置于程序代码内部。这样做是安全的,因为它们永远都不会改变。有的常数 需要严格地保护,所以可考虑将它们置入只读存储器(ROM)。
非RAM 存储:若数据完全独立于一个程序之外,则程序不运行时仍可存在,并在程序的控制范围之外。 其中两个最主要的例子便是“流式对象”和“固定对象”。对于流式对象,对象会变成字节流,通常会发给 另一台机器。而对于固定对象,对象保存在磁盘中。即使程序中止运行,它们仍可保持自己的状态不变。对 于这些类型的数据存储,一个特别有用的技巧就是它们能存在于其他媒体中。一旦需要,甚至能将它们恢复 成普通的、基于RAM 的对象。
Java里基本类型和引用都是存放到堆栈里,因为它们所占的空间比较少,没有必要放到堆内存中去,而存在堆栈里使用起来会更高效。这里还要说的是数组,数组可以和对象等同,哪怕你在创建一个基本类型的数组也是如此,数组可以完全当做对象来使用。
现在我们可以回到我们在上篇要谈的内容:java里对象的传递和返回都是通过引用传递和返回的,这就是导致System.arraycopy方法是浅拷贝的症结了。下面的代码显示了java是按引用传递的:我们把一个引用传入到某个方法里,我们在方法内部发现传入的引用指向的还是原来的对象,大家看下面的代码:
package cn.com.sxia;
public class PassRef {
public static void f(PassRef h){
System.out.println("h inside f():" + h);
}
public static void main(String[] args) {
PassRef p = new PassRef();
System.out.println("p inside main():" + p);
f(p);
}
}
运行结果如下:
p inside main():cn.com.sxia.PassRef@119298d
h inside f():cn.com.sxia.PassRef@119298d
大家看到了p和h引用都是指向了同一个对象,因为他们的地址是一样的。
Java里还有一个别名效应的问题。
别名效应:是指多个引用指向同一个对象。这种情况就是一台电视机多台遥控器,突然一个遥控器换了台,其他的遥控器可能并不想换台,这就有问题了。
大家看下面的代码:
package cn.com.sxia;
public class Alias {
private int i;
public Alias(int ii){
i = ii;
}
public static void main(String[] args) {
Alias x = new Alias(7);
Alias y = x;
System.out.println("x: " + x.i);
System.out.println("y: " + y.i);
System.out.println("增加变量x里的i的数值:");
x.i++;
System.out.println("x: " + x.i);
System.out.println("y: " + y.i);
}
}
运行结果如下:
x: 7
y: 7
增加变量x里的i的数值:
x: 8
y: 8
大家可以看到x和y变量的值都被改变了。这种被串行修改的情况我们大多时候都不愿意发生。解决这个问题的方法很简单:不要在同一个作用域里生成多个对象的引用,特别是传值的时候,我们不要随意用局部变量存储传来的值。
但是在一个局部环境里(局部环境就是一个方法内的作用域),我们从外部传参数进来总会产生这样的问题。那么在一个局部环境也就是在一个方法内的作用域里,变量和对象到底是什么样的情况,很高兴有些人总结了这里面的情况:
引用传递虽然存在很多问题,但是引用传递是java默认的传递方式,因为大多数情况引用不会影响到我们程序的正确运行,而且传递参数只传递引用也就是只对堆栈的内存进行读取,效率会更高,因此有时我们也可以片面的理解引用是java为程序的高效性所做的妥协了。
下面我就写个浅拷贝的代码,我这个小系列里不会对如果写出正确拷贝问题作出深入的分析,因为我想着重学习的是java里复杂的数据存储方式,我只是由浅拷贝引出一个很重要的观点:java里传递的是引用这个真理,这个对我后面写复杂数据存储问题很有帮助。不过在我后面的内容中也会进一步阐释深拷贝和浅拷贝的问题,解决正确拷贝的问题会穿插到我后面文章的内容之中。好了,大家看下面的代码吧:
package cn.com.sxia;
import java.util.Arrays;
class Int{
private int i;
public Int(int j){
i = j;
}
public void increment(){
i++;
}
public String toString(){
return Integer.toString(i);
}
}
public class Cloning {
public static void main(String[] args) {
Int[] arr1 = new Int[10];
for (int i = 0;i < 10;i++){
arr1[i] = new Int(i);
}
System.out.println("arr1:" + Arrays.toString(arr1));
Int[] arr2 = arr1.clone();
for (int j = 0;j < arr2.length;j++){
arr2[j].increment();
}
System.out.println("arr1:" + Arrays.toString(arr1));
}
}
运行结果如下:
arr1:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
arr1:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
我们使用clone方法进行复制,arr1复制到了arr2,但是arr2的改变影响到了arr1的值,这就说明这个拷贝是浅拷贝,arr2只是复制了arr1的引用,而arr1指向的对象是没有被拷贝的。实际上java里的对象由以下几个部分所组成:对象的引用,引用所指向的对象,这些对象指向的另外一些对象,一直指向下去这就构成了一个对象网络图,深拷贝就是将这整个对象网路图全拷贝下来。
下面的内容我就开始讲解多java.util包的使用了。这个是我要讲的重点了。