java笔记:关于复杂数据存储的问题--基础篇:数组以及浅拷贝与深拷贝的问题(下)
我们首先看看下面代码:
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变量的值都被改变了。这种被串行修改的情况我们大多时候都不愿意发生。解决这个问题的方法很简单:不要在同一个作用域里生成多个对象的引用,特别是传值的时候,我们不要随意用局部变量存储传来的值。
但是在一个局部环境里(局部环境就是一个方法内的作用域),我们从外部传参数进来总会产生这样的问题。那么在一个局部环境也就是在一个方法内的作用域里,变量和对象到底是什么样的情况,很高兴有些人总结了这里面的情况:
- 当我们向方法里传递参数别名的效应就会发生。这个很好理解了,当传参数时候,局部环境外部有个引用和局部环境内部的参数的引用都是指向了同一个对象,多个引用指向同一个对象就是别名效应了。
- 在一个局部环境里,我们构建一个对象,这个对象有引用,引用是属于这个局部环境的,离开了局部环境引用就被销毁,但是创建的对象还会存在,特别是我们用了return关键字返回时候,外部会有另外一个引用使用到这个对象,所以局部环境内没有局部对象只有局部引用。
- 上面的问题还引出了一种情况:在局部环境里引用是有作用域的,但是对象是没有的,对象的销毁是在没有可以使用该对象的引用后在一定时间内被垃圾回收机制所回收。
- 在java里我们关心的是引用的生命周期,而不是对象的生命周期,对象的生命周期是java自己管理的。
引用传递虽然存在很多问题,但是引用传递是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包的使用了。这个是我要讲的重点了。