Java中的对象类型像引用还是指针,谁是谁非?
作者:海枫
作为一名程序员,我们应该对新知识和新技术刨根问底,而不应泛泛而谈。我未曾接触到Java的时候,我想听得最多的东西还是关于Java中不存在指针的问题。此时,我会不断地想:如果Java不存在指针的话,那么是如何实现复杂的数据结构?这样的语言与VB有什么差别?如果一个静态过程式或面向对象语言,如果不存在指针的话,那它如何会得到程序员的喜爱呢?功能是何其的有限?幸好有机会和一位师弟在交流C#,当时我也没有接触过C#,不过从他的观点令我看清楚了一些问题。我还是很清楚地记得他说了一句:C#中同样有指针,不过是有限制的指针,因此在C#中把它称为引用(references)。经过对Java的学习后,我非常赞同他的那句话。即是Java中也存在指针,不过是限制的指针,因此在Java语言的规范说明里把它称为引用。下在从C++中的对象类型为依据,谈谈Java语言中的引用到底与C++中的哪种对象类型更类似。
Java的对象类型
在这里,我不泛谈程序语言原理方面的知识,如何为引用,何为指针。只以C++的对象类型为蓝本,讨论C++中对象类型与Java对象类型的异同。
C++的对象类型分为三种:对象变量,对象指针和对象引用(这里特指是C++的引用)。对象变量,与基本数据类型变量一样,分配在栈中,对象在栈的生命空间结束后,系统会自动释放对象所占用的空间;对象指针,与C语言中的指针一样,都是一个地址,它指向栈中或堆中的一个对象。对象引用是C++中与C不同之外,形象地说,引用就是一个别名,定义引用的时候必须一起初始化,不能引用不存在的对象。下面是C++中定义三种对象类型的代码:
String a(“string a”);
String *pA = &a;
String *pB = new String(“string b”);
String &c = a;
语句1中定义一个String变量a,它的内容是”string a”;语句2中定义一个String对象指针,它指向对象a(栈对象);语句3中定义一个String对象指针,不过它指向一个分配在堆中的对象。最后一个语句是定义一个String对象的引用c,它引用a,也即是说c是a的别名,即同一个变量,两个不同的名字而已。只要改变a或c,该变量内容都会发生改变的。
Java中的对象类型只有一种,那就是引用(注意是Java的引用,而非C++的引用)。下面是定义一个引用的代码。
String s = new String(“string”);
在执行此代码时,在内存空间生成的结构如下面所示:
+--------+ +-------------------------+
|引用s |--------------------à|对象内容为“string”|
+-------- + +-------------------------+
上面的语句中其实做了两件事情,在堆中创建了一个String对象,内容为”string”,在栈中创建了一个引用s,它指向堆中刚创建好的String对象。并且引用s值的改变不影响它所指的对象,只有通过它调用对象的方法对可能改变对象的内容。请看如下语句:
s = new String(“abc”);
在上面这个语句中,只改变s的值,因此不会对内容为”string”对象造成影响(不考虑垃圾回收情况)。只不过是s指向堆中的新对象而已,从指针上来说,就是s的值改变了而已。
从上面来看,Java的引用,并不与C++的引用相同,因此它不是一个别名;与对象变量也不同,它只是表示一个内存位置,该位置就是存在一个对象的位置,而不是真实的对象变量。并且从指针的意义角度来说,C/C++的指针与Java的引用却是不谋而合。它们都表是一个内存地址,都可以通过这个内存地址来操纵它所对应的对象。因此Java的引用更像C++中的指针,在下文中把它称为Java中的指针,同样也可称为Java中的引用。
Java中的指针与C++中的指针
本文开始提到Java中的指针是限制的指针,那么在这里分析Java中的指针可以执行什么运算符。
在C++的对象指针里面,出现的指针运算符主要有以下几个:*,->。
运算符*是返回指针所指向的对象,而->是返回指针所指向对象的数据成员或方法成员。由于不存在对象变量,而是通过指针来访问对象的,因此Java中不需要提供*运算符,这是Java优化了C++的一个指针问题。对于->运行符,Java的指针是提供的,不过是采用.运算符的方式提供,看起来与C++中对象变量的.运算符一样,其实意义是有不一样的地方。
如String s = new String(“abc”);
s.indexOf(“a”);
与下面C++代码等价的
String *s = new String(“abc”);
s->indexOf(“a”);
对于C++中对象变量出现的复制构造函数,“=”运算符以及“==”运算符问题同样在Java中出现。
在C++中,如果类没有定义它的“=”运算符,那么有可能会出现浅度复制,而非深度复制,Java也有类似的问题。两者要程序员采用深度复制的策略编写构造函数;如果C++中的对象没有定义“==”运算法,那么它会依次对两个变量比较它的数据成员,看是否都相等。如果定义则按用户的比较方式进行比较。在Java,Object对象定义了equals方法,这个方法是用来比较两个对象如果相等的,Object中的equals方法只是比较两个指针的值是否相等而已,要根据所指向的对象内容进行比较,那应重写该类的equals方法。
正因为Java中存在指针,所以使用Java同样能写出丰富的数据结构,java中的集合框架就是最好的例子。
上面只要谈到Java中指针与C++中指针相同或类似的部分,我觉得两者之间是有差别的,差别主要有三个。
C++中的指针是可以参与和整数的加减运算的,当一个指对指向一个对象数组时,可以通过自增操作符访问该数组的所有元素;并且两个指针能进行减运算,表示两个指表所指向内存的“距离”。而Java的指针是不能参与整数运算和减法运算的。
C++中的指针是通过new运算或对象变量取地址再进行赋值而初始化的,可以指向堆中或栈中的内存空间。Java同样是类似的,通new运算得到初始化。Java中指针只能是指向堆中的对象,对象只能生存在堆中。C语言是可以通过远指针来指向任意内存的地址,因而可以该问任意内存地址(可能会造成非法访存);Java中的指针向的内存是由JVM来分配的中,由于new运算来实现,不能随所欲为地指向任意内存。
C++中通过delete和delete[] 运算符进行释放堆中创建的对象。如果对象生存周期完结束,但没有进行内存释放,会出现内存泄露现象。在Java中,程序员不用显式地释放对象,垃圾回收器会管理对象的释放问题,不用程序员担心。Java使用了垃圾回收机制使得程序不用再管理复杂的内存机制,使软件出现内存泄露的情况减少到最低。
小结
通过上面的分析,我们可以得到这样的结论:Java中的引用与C++中的引用是不同的,并且Java中的引用更像C++中的指针。因此,可以认为Java中的引用就是指针,是一种限制的指针,不能参与整数运行和指向任意位置的内存,并且不用显示回收对象。除了Java外,就本文开头提到的C#以及VB.NET中出现的引用,都类似于C++中的指针。Java中的采用引用的说法,其实是想程序员忘记指针所带来的痛苦;Java的引用比C++中的指针好用得多了,也容易管理,同时提供内存管理机制,让大家用得安心,写得放心而已。