java是当今使用最广泛的编程语言之一。自1995年发布以来,一直被用户高度评价为“消除了C++缺点的优秀编程语言”。不过,随着它的广泛使用,其缺点也在逐步地表现出来。
Java的缺点公认有如下三点:(1)存在非对象的数据类型;(2)不能够用一种描述方法来表达各种类(Class);(3)无法继续2个以上的类的装配。虽然也有人认为编程语言应该是一个什么样子会因人而异,不应该算成缺点。不过,上述三点却可以导致编程人员使用混乱,降低源码的可读性及程序的可维护性。
存在非对象的数据类型
表1●Java的原始类型(PRimitive)。原始类型包括表示真假的布尔型(Boolean)、字符型和数值型等(点击放大)
第一缺点是指虽说Java是面向对象的编程语言,但却存在非对象的数据类型。
“面向对象”的定义虽然有很多种,但无论何种定义,其最基本的概念都是利用包含数据和步骤的“对象”来表达系统。即便在Java领域,也会使用名为类的模型生成对象,并通过调用它的方法组织程序。
但是,其中却混杂着非对象的内容。原始类型又被称为基本数据类型(表1)。用于处理文字的char(字符型)、表示真假的boolean(布尔型)、int以及float等数值型就属于这种数据类型。
原始类型的内存治理方法不同
图1●Java的内存治理方法。Java内存区包括保存本地变量的内存堆栈区(stack)和保存对象的数据的内存堆区(heap)。堆栈区中存放的是用于引用(reference)对象时所需的信息(点击放大)
原始类型和对象型的内存治理方法不同。Java虚拟机所治理的内存区包括内存堆栈区和内存堆区(图1)。内存堆栈区用于存放本地变量的数据。按堆出的顺序保存本地变量的数据。一旦脱离变量的有效范围,该数据马上就被释放。
而内存堆区则用于存放对象本身。生成对象型的变量后,首先在内存堆栈区中为其预备存放位置。然后利用new运算符在这个位置生成新的对象后,对象及其数据就被存放到内存堆区。接着,内存堆区中的对象位置就会作为对象型的变量数据而被写入内存堆栈区。由于这些是引用对象时所用的信息,因此对象型变量被称为“引用型”。
而实际数据本身被写入内存堆栈区的是原始类型。采用这种内存治理方法的类型称为“数值型”。
引用型变量改变以后,就会引用保存内存堆区中的实际的对象数据,重新改写数据。比如,在方法的引数(arguments)中描述对象型变量时传递给方法的就是存放在这个位置中的信息。所以在方法内追加的变更还会被反映到调用的原始对象上。另一方面,数值型变量传递的是它的值。即便在方法内部进行了变更,也不会反映到原始变量中。
无法使用对象所具有的功能
LIST 1●将数值数据保存在Java的矢量类中的程序。生成Integer类,然后封装(Wrap)数值(点击放大)
点击查看大图LIST 1●将数值数据保存在Java的矢量类中的程序。生成Integer类,然后封装(Wrap)数值(点击放大)
由于原始类型与对象型的内存治理方法不同,因此就无法生成统一两种数据的类库。比如,假如只是对象型数据就能够构筑包含任意数据的类库。
可变长的数组类就是其中的一个例子。它是作为名为java.util.Vector的类而生成的。可以将任意的对象追加到数组中,还可以提取或删除。能够以此为引数指定任意的对象。但是,由于原始类型数据不是对象,因此无法直接引入。
因此在Java中还存在相当于原始类型的类。比如int型变量就可以使用java..lang.Integer类。重新生成Integer类,然后保存数据,就可以追加到Vector矢量类中(LIST 1)。
但是稍微想一想就会明白,这种方法并不是很灵活的做法。由于加入了多余的代码,因此看起来感觉比较乱。而且还会浪费内存空间。原来的值暂且不说,还必须确保新建对象所需的内存。不仅存在表面上的问题,还存在实质上的问题。就是说无法保证数据的同一性。作为对象型保存的值与作为原始类型而保存的值完全不同。即便改变了原始类型的值,也不会反映到原来的int型数据。
C#利用Boxing(装箱)解决的只是一部分
这一问题并非是Java特有的。比如,作为与Java类似的语言为用户熟知的C#也存在相同的问题。C#利用称为Boxing的方法部分地解决了这个问题。但是所解决的也只是可以不写多余代码的部分。内存问题和同一性问题仍然存在。
即便C#,int、double和char等数据类型也无法作为对象进行处理。这些数据类型与Java的原始类型相同,也是数值型变量。
C#可以将其值代入到对象中。LIST 2中显示了具体的代码。已经将int型的值代入了对象型变量。此时先进行装箱,之后就开始静静地把基本数据类型的数据转换成对象型数据。在内存堆区中确保相应的内存,然后将数值型数据保存这里(图2)。对象型变量引用的就是这些数据。
点击查看大图LIST 2●执行装箱的C#代码。将数值直接代入对象中。运行代码后,输出0和1。也就是说变量a和o没有同一性(点击放大)
图2●C#中的装箱法。对存放于内存堆栈区中的int型结构体(strUCts)装箱时,就会静静地在内存堆区中生成对象。因此就无法确保与初始值的匹配性。(点击放大)
笔者利用装箱法,用C#试着写了一段与在Java的Vector矢量类中保存数值类似的代码(LIST 3)。虽然ArrayList类要引数中提取对象型变量,但这里由于通过直接int型变量,因此代码非常整洁。
不过,并没有解决多余的内存消耗和数值的同一性问题。因为只是单纯地实现了自动向对象的转换(图3)。
LIST 3●与LIST 1起相同作用的C#代码。由于具有装箱法,因此可以直接向ArrayList中追加数值
点击查看大图图3●利用Java和C#,将int型变量转换成对象的方法。尽管内部处理基本相同,但C#的特点是隐式转换
假如考虑到实用,也算得上是优点
从上述所讲来看,就会生出这样的疑问:为什么最新的Java和C#语言还存在着这样的问题呢?实际上这是因为对其性能的重视。
由于原始数据型数据在编程时使用得最多,因此利用能够对其进行快速处理的原始类型,性能就会提高。而对象型数据在生成对象,以及使用位置信息去引用内存堆区中的数据时则会产生一定的开销。另一个问题是内存堆区。假如全部是对象型,比如,只要执行简单的for循环语句,就会在内存堆区中生成大量的对象。由于内存堆区的消耗速度就会急剧上升,并且频繁地进行资源回收处理,因此性能就会降低。
Java和C#是考虑到性能问题才生成原始数据型数据的。因此并不能说是“纯粹的”面向对象语言。也许可以说是考虑到实用性的稳妥做法吧。(记者:大森 敏行、八木 玲子)