`
fulerbakesi
  • 浏览: 561279 次
文章分类
社区版块
存档分类
最新评论

java基础教程-对象的传递与返回

 
阅读更多
对象的传递与返回
现在,我们应该已经相当适应这样的概念了:当你传递对象的时候,实际上是在传递对象的引用。
对许多编程语言而言,只需按正规方式传递对象,就能处理绝大多数情况。然而总有些时候,需要以不太正规的方式处理。于是,事情突然变得复杂起来(在C++中,会变得相当复杂),Java也不例外。因此,确切理解在传递对象的时候发生了什么,就显得很重要了。
“Java中是否有指针?有些人认为,指针既难掌握又很危险,所以指针很糟糕。既然Java全部都是优点,还能减轻你的编程负担,所以它不可能包含指针。然而,确切地说,Java有指针。事实上,Java中(除了基本类型)每个对象的标识符就是一个指针。但是它们受到了限制,有编译器和运行期系统监视着它们。或者换个说法,Java有指针,但是没有指针的相关算法。我们称之为引用(reference,你也可以将它们看作安全的指针,就像小学生的安全剪刀,它不尖锐,不故意使劲通常不会伤着人,但是使用起来不但慢而且麻烦。
传引用
将一个引用传入某个方法之后,它仍然指向原来的对象。可以用一个简单的试验证明:
// Passing references around.
public class PassReferences {
public static void main(String[] args) {
PassReferences p = new PassReferences();
System.out.println("p inside main(): " + p);
f(p);
}
public static void f(PassReferences h) {
System.out.println("h inside f(): " + h);
}
}

打印语句自动调用toString()方法,而继承自ObjectPassReferences类没有重新定义toString()方法。所以使用的是ObjecttoString()方法,它打印出类名以及存储对象的地址(不是引用,而是实际的对象)。输出看起来像这样:
p inside main(): PassReferences@ad3ba4
h inside f(): PassReferences@ad3ba4
可以看到,ph都指向同一个对象。像这样只是发送一个参数给方法,比复制一个新的PassReferences对象效率要高很多。但它也引出了一个重要的话题。
别名效应
别名效应是指,多个引用指向同一个对象,参见前例。当某人修改那个对象时,别名带来的问题就会显现出来。例如,如果此对象还有其他的引用指向它,而使用那些引用的人,根本没想到对象会有变化,定然会对此感到十分诧异。可以用一个简单的例子做演示:
public class Alias1 {
private int i;
public Alias1(int ii) {
i = ii;
}
public static void main(String[] args) {
Alias1 x = new Alias1(7);
Alias1 y = x; // Assign the reference
System.out.println("x: " + x.i);
System.out.println("y: " + y.i);
System.out.println("Incrementing x");
x.i++;
System.out.println("x: " + x.i);
System.out.println("y: " + y.i);
}
}
看这一行:
Alias1 y = x; // Assign the reference
它生成了一个新的Alias1引用,但是并没有赋予其用new创建的新对象,而是赋值为已存在的引用。既是将x的内容(x指向的对象的地址)赋给y,因此xy指向同一个对象。所以,x中的i增加时:
x.i++;
y中的i也会受到影响。这可以从输出中看到:
x: 7
y: 7
Incrementing x
x: 8
y: 8
对此有一个很好的解决方案,很简单,就是不要这样做;不要在相同作用域内生成同一个对象的多个引用。这样的代码更易于理解与调试。然而,当你将引用作为参数传递时,它会自动被别名化(这是Java的工作方式)。因为,在方法内创建的局部引用能够修改外部的对象(在方法作用域以外创建的对象)。参见下例:
// Aliasing two references to one object.
public class Alias2 {
private int i;
public Alias2(int ii) {
i = ii;
}
public static void f(Alias2 reference) {
reference.i++;
}
public static void main(String[] args) {
Alias2 x = new Alias2(7);
System.out.println("x: " + x.i);
System.out.println("Calling f(x)");
f(x);
System.out.println("x: " + x.i);
}
}
方法f()改变了它的参数,也就是f()外面的对象。出现这种情况时,你必须考虑清楚,这样做是否有意义,是否如用户所愿,会不会引起其他问题。
一般而言,调用方法是为了产生返回值,或者为了改变被调用者(某对象)的状态。通常不会为了处理其参数而调用方法,那被称作为了副作用而调用方法。因此,如果你创建的方法会修改其参数,你必须清楚地向用户说明它的使用方式,以及潜在危险。考虑到别名带来的困扰和缺点,我们最好能避免修改参数。
如果确实要在方法调用中修改参数,但又不希望修改外部参数,那么就应该在方法内部制作一份参数的副本,以保护原参数。
制作局部拷贝
Java中所有的参数传递,执行的都是引用传递。也就是说,当你传递对象时,真正传递的只是一个引用,指向存活于方法外的对象。所以,对此引用做的任何修改,都是在修改方法外的对象。此外:
别名效应在参数传递时自动发生。
方法内没有局部对象,只有局部引用。
引用有作用域,对象则没有。
Java中,不需要为对象的生命周期操心。
没有提供语言级别的支持(例如常量)以阻止对象被修改,或者消除别名效应的负面影响。不能简单地使用final关键字来修饰参数,它只能阻止你将当前引用指向其他对象而已。
如果你只是从对象中读取信息,而不修改对象,那么传引用就是传递参数最高效的方式。缺省的方式就是最高效的方式,这当然最好。但是,有时必须将参数对象视为局部对象,才能使方法内的修改只影响局部的副本,从而不会改变方法外的对象。很多编程语言支持这种可以自动为参数对象创建一份方法内的局部拷贝的能力。Java虽然不支持此能力,但是它允许你产生同样的效果。
传值
这引出了一个术语问题,对它的争论总是有益的。传值这个术语,及其含义依赖于你如何看待程序的操作。它通常的意义是,对于你传递的东西,得到它的一份局部拷贝。但真正的问题是,如何看待你传递的东西。对于传值的含义,有截然不同的两派观点:
1. Java中传递任何东西都是传值。 如果传入方法的是基本类型的东西,你就得到此基本类型元素的一份拷贝。如果是传递引用,就得到引用的拷贝。因此,所有东西都是传值。当然,前提是你认为(并关心)传递的是引用(而不是对象)。但是,Java的设计是为了帮助你(在大多数时候)忽略你是在使用引用。也就是说,它希望你将引用看作是原本的对象,因为Java会在你调用方法的时候隐式地解除引用。
2. 对于基本类型而言,Java是传值(对此双方没有异议),但是对于对象,则是传引用。大部分人的观点是,引用是对象的另一种称呼罢了,所以不会想到是传引用,而是认为就是在传递对象。既然向方法传递对象时,不会得到局部复制,所以传递对象很明显不是传值。Sun公司似乎比较支持此观点,因此,曾经有一个保留的、但没有实现的关键字“byvalue”(可能永远也不会实现)。
两派观点都陈列出来了,我们认为这依赖于你如何看待引用。此后我将回避此问题。最终你会明白,这个争论并没有那么重要。真正重要的是,你要理解,传引用使得(调用者的)对象的修改变得不可预期。
克隆对象
需要使用对象的局部拷贝的最可能的原因是:你必须修改那个对象,但又不希望改动调用者的对象。如果你决定要制做一份局部拷贝,可以使用clone()方法。这是定义在Object类中的protected方法。如果要使用它,必须在子类中以public方式重载此方法。例如,标准类库中的ArrayList类就重载了clone(),所以我们才能由ArrayList调用clone()方法:
// The clone() operation works for only a few
// items in the standard Java library.
import java.util.*;
class Int {
private int i;
public Int(int ii) {
i = ii;
}
public void increment() {
i++;
}
public String toString() {
return Integer.toString(i);
}
}
public class Cloning {
public static void main(String[] args) {
ArrayList v = new ArrayList();
for (int i = 0; i < 10; i++) {
v.add(new Int(i));
}
System.out.println("v: " + v);
ArrayList v2 = (ArrayList) v.clone();
for (Iterator e = v2.iterator(); e.hasNext();) {
((Int) e.next()).increment();
}
System.out.println("v: " + v);
}
}
clone()方法只能生成Object,之后必须将其转型为合适的类型。此例演示了ArrayListclone()方法,它并不自动克隆ArrayList中包含的每个对象。克隆的ArrayList只是将原ArrayList中的对象别名化。这通常称为浅层拷贝(shallow copy),因为它只复制对象表面的部分。实际的对象由以下几部分组成:对象的表面,由对象包含的所有引用指向的对象,再加上这些对象又指向的对象,等等。通常称之为对象网。将这些全部复制即为深层拷贝(deep copy)。
可以由输出看到浅层拷贝的效果,对v2的操作影响了v
v: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
v: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
不自动对ArrayList包含的所有对象执行clone()是正确的,因为不能保证那些对象都是可克隆的(cloneable)。
使类具有克隆能力
虽然是在所有类的基类Object中定义了克隆方法,但也不是每个类都自动具有克隆能力。这似乎违反直觉,基类的方法在其子类中不是应该自动可用吗?Java的克隆操作确实违背了此思想。如果一个类需要具有克隆能力,你必须专门添加一些代码,它才能够克隆。

使用protected的技巧
为防止所有类缺省地就具备克隆能力,基类中的clone()方法被声明为protected。这意味着,对使用(而非继承)此类的客户端程序员,克隆方法不再是缺省地可用。而且,也不能通过指向基类的引用调用clone()方法。(虽然这在某些情形下似乎很有用,例如以多态方式克隆大量Object型对象。)实际上,此方法让你在编译期就知道,你的对象不具备克隆能力。而且,标准Java类库中的大多数类都不可克隆,这够奇怪吧。因此,如果你这样写:
Integer x = new Integer(1);
x = x.clone();
在编译时就会得到错误信息,说明clone()不可访问(因为Integer没有重载clone(),它缺省是protected方法)。
不过,因为Object.clone()protected,所以在Object的子类(等同于所有类)内部,你有权限去调用它。基类的clone()方法很实用,它在”( bitwise)级别上复制子类的对象,如同通常的克隆操作一样。然而,你必须将你的克隆操作声明为public,它才可以被访问。所以,克隆对象时有两个关键问题:
调用 super.clone()
将自己的克隆方法声明为public
你可能需要重载所有子类的clone()方法;否则,你的(现在为publicclone()虽然能用,其行为却不一定正确(即使由于Object.clone()复制了实际的对象,令其行为有可能正确)。protected的技巧只能用一次:在第一次继承无克隆能力的类,而又希望它的子类具有克隆能力时。继承自你的类的任何子类,其clone()方法都可用,因为Java中,继承无法削减方法的访问权。既是说,如果某个类是可克隆的,它的继承者也都是可克隆的,除非你使用某种机制(稍后会描述)关闭克隆能力。
实现Cloneable接口
要完善一个对象的克隆能力,还需要做一件事:实现Cloneable接口。该接口有点奇怪,因为它是空的!
interface Cloneable {}
实现空接口的原因显然不是为了类型转换,然后调用Cloneable接口的方法。这样使用的接口称为标记接口(tagging interface,因为它像是一种贴在类身上的标记。
Cloneable接口的存在有两个理由。第一,如果某个引用向上类型转换为基类后,你就不知道它是否能克隆。此时,可以使用instanceof关键字(第十章对此有描述)检查该引用是否指向一个可克隆的对象。

if(myReference instanceof Cloneable) // ...
第二个原因与克隆能力的设计有关,这是考虑到也许你不愿意所有类型的对象都是可克隆的。所以Object.clone()会检查当前类是否实现了Cloneable接口。如果没有,它抛出CloneNotSupportedException异常。所以,作为实现克隆能力的一部分,通常你必须实现Cloneable接口。
成功的克隆
只要你明白了clone()方法的实现细节,就能够让你的类很容易地复制本身,以提供一个局部副本:
// Creating local copies with clone().
import java.util.*;
class MyObject implements Cloneable {
private int n;
public MyObject(int n) {
this.n = n;
}
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
System.err.println("MyObject can't clone");
}
return o;
}
public int getValue() {
return n;
}
public void setValue(int n) {
this.n = n;
}
public void increment() {
n++;
}
public String toString() {
return Integer.toString(n);
}
}
public class LocalCopy {
public static MyObject g(MyObject v) {
// Passing a reference, modifies outside object:
v.increment();
return v;
}
public static MyObject f(MyObject v) {
v = (MyObject) v.clone(); // Local copy
v.increment();
return v;
}
public static void main(String[] args) {
MyObject a = new MyObject(11);
MyObject b = g(a);
// Reference equivalence, not object equivalence:
System.out.println("a == b: " + (a == b) + "/na = " + a + "/nb = " + b);
MyObject c = new MyObject(47);
MyObject d = f(c);
System.out.println("c == d: " + (c == d) + "/nc = " + c + "/nd = " + d);
}
}
首先,为了想clone()方法可以被访问,必须将其声明为public。其次,作为你的clone()操作的初始化部分,应该调用基类的clone()。该clone()是在Object中定义的,它是protected方法,在子类中也能访问,所以可以调用。
Object.clone()能够按对象大小创建足够的内存空间,从对象到对象,复制所有比特位。这被称为逐位复制(bitwise copy,它正是你期望的clone()方法的典型行为。但是,Object.clone()在执行操作前,会先检查此类是否可克隆,即检查它是否实现了Cloneable接口。如果没有实现此接口,Object.clone()会抛出CloneNotSupportedException异常,说明它不能被克隆。因此,你必须将super.clone()置于try块内,以捕获不应该发生的异常(因为你已经实现了Cloneable接口)。
LocalCopy中,方法g()f()演示了两种参数传递的区别。方法g()是传引用,它会修改外部的对象,并返回指向此外部对象的引用。方法f()则克隆参数,切断了与原对象的关联,然后就可以随意使用它了,甚至是返回新对象的引用,也不会对原对象有任何影响。注意下面这行有点古怪的语句:
v = (MyObject)v.clone();
这是在创建局部副本。为避免被这种语句迷惑,请记住,这种奇怪的编码习惯在Java中非常合宜,因为对象标识符实际就是引用。v使用clone()克隆出它所指对象的副本,并返回副本对象的Object引用(Object.clone()就是这样定义的),因此必须对返回的引用做适当的类型转换。
main()中,测试了两种参数传递方式的不同效果。请注意,重要的是Java比较对象相等的等价测试并未深入对象的内部。==!=操作符只是简单地比较引用。如果引用代表的内存地址相同,则它们指向同一个对象,因此视为相等。所以,该操作符测试的其实是:不同的引用是否是同一个对象的别名。
Object.clone()的效果
调用Object.clone()时实际会发生什么,致使你重载clone()时必须要调用super.clone()呢?Object类的clone()方法负责创建正确容量的存储空间,并作逐位复制,由原对象复制到新对象的存储空间中。也就是说,它并不是仅仅创建存储空间,然后复制一个Object。它实际是计算出即将复制的真实对象的大小(不只是基类对象,还包括源于它的对象)。由于这都发生在根类定义的clone()方法中(它并不知道谁会继承它),可以猜到,是RTTI机制确定要被克隆的实际对象。通过这种方式,clone()方法能够创建合适的存储空间,正确地逐位复制你的类型。
无论怎样做,克隆过程的第一步通常都是调用super.clone()。它制作出完全相同的副本,为克隆操作建立了基础。在此基础上,你可以执行对完成克隆必要的其他操作。
要确切了解其他操作是指什么,你首先需要知道Object.clone()为你做了什么。特别是,它是否自动将所有引用克隆至目的地?下面的例子是个试验:
// Tests cloning to see if destination
// of references are also cloned.
public class Snake implements Cloneable {
private Snake next;
private char c;
// Value of i == number of segments
public Snake(int i, char x) {
c = x;
if (--i > 0)
next = new Snake(i, (char) (x + 1));
}
public void increment() {
c++;
if (next != null)
next.increment();
}
public String toString() {
String s = ":" + c;
if (next != null)
s += next.toString();
return s;
}
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
System.err.println("Snake can't clone");
}
return o;
}
public static void main(String[] args) {
Snake s = new Snake(5, 'a');
System.out.println("s = " + s);
Snake s2 = (Snake) s.clone();
System.out.println("s2 = " + s2);
s.increment();
System.out.println("after s.increment, s2 = " + s2);
}
}
Snake由许多节组成,每节也是Snake类型。因此,它是个单链链表(singly linked list)。递归生成所有小节,每次递减构造器的第一个参数,到零为止。为给每节Snake赋予唯一的标记,在递归调用构造器时,char类型的第二个参数会递增。
increment()方法递归增加每个标记,便于你看到的变化,而toString()方法递归打印每个标记。从程序输出可以看到,Object.clone()只复制了第一节Snake,因此这是浅层拷贝。如果你想整条Snake(每节Snake)都被复制,既是深层拷贝,为此必须在重载的clone()方法中执行额外的操作。
通常,你会在可克隆类的每个子类中调用super.clone(),以保证基类所有的操作(包括Object.clone())都被执行。然后,对对象中的每个引用,都明确地调用clone()。否则,那些引用会被别名化,仍指向原本的对象。这与调用构造器的方式类似:基类的构造器先执行,然后是其直接继承者,依此类推,直到最末端子类的构造器。可惜clone()不是构造器,这不会自动发生。你必须自己处理这个过程。

克隆一个组合对象
在对组合对象做深层拷贝时,你会遇到一个问题。你必须假设:成员对象的clone()方法会在其引用上依次执行深层拷贝,并依此类推。这相当于一个承诺。它实际上表示,要想让深层拷贝起作用,你就必须控制所有类的代码,或者至少对深层拷贝涉及的类要足够了解,才能知道它们是否正确执行了各自的深层拷贝。
下面的例子演示了在对组合对象做深层拷贝时,你必须要做的事情:
// Cloning a composed object.
// {Depends: junit.jar}
import junit.framework.*;
class DepthReading implements Cloneable {
private double depth;
public DepthReading(double depth) {
this.depth = depth;
}
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
public double getDepth() {
return depth;
}
public void setDepth(double depth) {
this.depth = depth;
}
public String toString() {
return String.valueOf(depth);
}
}
class TemperatureReading implements Cloneable {
private long time;
private double temperature;
public TemperatureReading(double temperature) {
time = System.currentTimeMillis();
this.temperature = temperature;
}
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
public double getTemperature() {
return temperature;
}
public void setTemperature(double temperature) {
this.temperature = temperature;
}
public String toString() {
return String.valueOf(temperature);
}
}
class OceanReading implements Cloneable {
private DepthReading depth;
private TemperatureReading temperature;
public OceanReading(double tdata, double ddata) {
temperature = new TemperatureReading(tdata);
depth = new DepthReading(ddata);
}
public Object clone() {
OceanReading o = null;
try {
o = (OceanReading) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
// Must clone references:
o.depth = (DepthReading) o.depth.clone();
o.temperature = (TemperatureReading) o.temperature.clone();
return o; // Upcasts back to Object
}
public TemperatureReading getTemperatureReading() {
return temperature;
}
public void setTemperatureReading(TemperatureReading tr) {
temperature = tr;
}
public DepthReading getDepthReading() {
return depth;
}
public void setDepthReading(DepthReading dr) {
this.depth = dr;
}
public String toString() {
return "temperature: " + temperature + ", depth: " + depth;
}
}
public class DeepCopy extends TestCase {
public DeepCopy(String name) {
super(name);
}
public void testClone() {
OceanReading reading = new OceanReading(33.9, 100.5);
// Now clone it:
OceanReading clone = (OceanReading) reading.clone();
TemperatureReading tr = clone.getTemperatureReading();
tr.setTemperature(tr.getTemperature() + 1);
clone.setTemperatureReading(tr);
DepthReading dr = clone.getDepthReading();
dr.setDepth(dr.getDepth() + 1);
clone.setDepthReading(dr);
assertEquals(reading.toString(), "temperature: 33.9, depth: 100.5");
assertEquals(clone.toString(), "temperature: 34.9, depth: 101.5");
}
public static void main(String[] args) {
junit.textui.TestRunner.run(DeepCopy.class);
}
}
DepthReadingTemperatureReading十分相似,它们都只包含基本类型。因此,它们的clone()方法也很简单:调用super.clone()然后返回结果。注意,这两个类的clone()代码相同。
OceanReadingDepthReadingTemperatureReading对象组成。所以,如果要做深层拷贝,它的clone()必须克隆OceanReading内部的所有引用。为此,super.clone()的结果必须转型为OceanReading对象(因此你能够访问depthtemperature的引用)。
深层拷贝ArrayList
我们再看看本附录先前的Cloning.java示例。这次Int2类是可克隆的,所以ArrayList可以被深层拷贝:

// You must go through a few gyrations
// to add cloning to your own class.
import java.util.*;
class Int2 implements Cloneable {
private int i;
public Int2(int ii) {
i = ii;
}
public void increment() {
i++;
}
public String toString() {
return Integer.toString(i);
}
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
System.err.println("Int2 can't clone");
}
return o;
}
}
// Inheritance doesn't remove cloneability:
class Int3 extends Int2 {
private int j; // Automatically duplicated
public Int3(int i) {
super(i);
}
}
public class AddingClone {
public static void main(String[] args) {
Int2 x = new Int2(10);
Int2 x2 = (Int2) x.clone();
x2.increment();
System.out.println("x = " + x + ", x2 = " + x2);
// Anything inherited is also cloneable:
Int3 x3 = new Int3(7);
x3 = (Int3) x3.clone();
ArrayList v = new ArrayList();
for (int i = 0; i < 10; i++)
v.add(new Int2(i));
System.out.println("v: " + v);
ArrayList v2 = (ArrayList) v.clone();
// Now clone each element:
for (int i = 0; i < v.size(); i++)
v2.set(i, ((Int2) v2.get(i)).clone());
// Increment all v2's elements:
for (Iterator e = v2.iterator(); e.hasNext();)
((Int2) e.next()).increment();
System.out.println("v2: " + v2);
// See if it changed v's elements:
System.out.println("v: " + v);
}
}
Int3继承自Int2,并添加了新的基本类型成员:int j。也许你认为需要重载clone()方法,以确保j也被复制,但事情并非如此。当Int2clone()Int3clone()而被调用时,它又调用了Object.clone(),后者会判断它操作的是Int3,并且复制Int3对象的所有位(bit)。只要你没有向子类中添加需要克隆的引用,那么无论clone()定义于继承层次中多深的位置,只需调用Object.clone()一次,就能完成所有必要的复制。
可以看到,对ArrayList深层拷贝而言,以下操作是必须的:克隆了ArrayList之后,必须遍历ArrayList中的每个对象,逐一克隆。对HashMap做深层拷贝时,也必须做类似的操作。
此例余下部分用以显示克隆的效果:一旦对象被克隆出来,你就能够在修改它的时候,不对原始对象造成影响。
通过序列化(serialization)进行深层拷贝
当你在思考Java的对象序列化操作时(在第十二章中介绍),可以观察到,如果将对象序列化之后再将其反序列化(deserialized),那么其效果相当于克隆对象。
那么为何不用序列化操作实现深层拷贝呢?下例比较了两种方法的耗时:
import java.io.*;
class Thing1 implements Serializable {
}
class Thing2 implements Serializable {
Thing1 o1 = new Thing1();
}
class Thing3 implements Cloneable {
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
System.err.println("Thing3 can't clone");
}
return o;
}
}
class Thing4 implements Cloneable {
private Thing3 o3 = new Thing3();
public Object clone() {
Thing4 o = null;
try {
o = (Thing4) super.clone();
} catch (CloneNotSupportedException e) {
System.err.println("Thing4 can't clone");
}
// Clone the field, too:
o.o3 = (Thing3) o3.clone();
return o;
}
}
public class Compete {
public static final int SIZE = 25000;
public static void main(String[] args) throws Exception {
Thing2[] a = new Thing2[SIZE];
for (int i = 0; i < a.length; i++)
a[i] = new Thing2();
Thing4[] b = new Thing4[SIZE];
for (int i = 0; i < b.length; i++)
b[i] = new Thing4();
long t1 = System.currentTimeMillis();
ByteArrayOutputStream buf = new ByteArrayOutputStream();
ObjectOutputStream o = new ObjectOutputStream(buf);
for (int i = 0; i < a.length; i++)
o.writeObject(a[i]);
// Now get copies:
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(
buf.toByteArray()));
Thing2[] c = new Thing2[SIZE];
for (int i = 0; i < c.length; i++)
c[i] = (Thing2) in.readObject();
long t2 = System.currentTimeMillis();
System.out.println("Duplication via serialization: " + (t2 - t1)
+ " Milliseconds");
// Now try cloning:
t1 = System.currentTimeMillis();
Thing4[] d = new Thing4[SIZE];
for (int i = 0; i < d.length; i++)
d[i] = (Thing4) b[i].clone();
t2 = System.currentTimeMillis();
System.out.println("Duplication via cloning: " + (t2 - t1)
+ " Milliseconds");
}
}
Thing2Thing4都包含成员对象,因此可以做深层拷贝。有趣的是,编写Serializable类很容易,但要复制它们则要花费更多的工作。克隆类在编写时要多做些工作,但实际复制对象时则相当简单。结果也很有趣,以下是三次运行的输出:
Duplication via serialization: 547 Milliseconds
Duplication via cloning: 110 Milliseconds
Duplication via serialization: 547 Milliseconds
Duplication via cloning: 109 Milliseconds
Duplication via serialization: 547 Milliseconds
Duplication via cloning: 125 Milliseconds
在早期版本的JDK中,序列化需要的时间远大于克隆(大约慢15倍),而且序列化的耗时波动很大。最近版本的JDK加快了序列化操作,其耗时显然也更稳定了。在这里,它比克隆大约慢四倍,作为克隆操作的替代方案,这个耗时已经进入了合理的范围。
向继承体系的更下层增加克隆能力
如果你创建了一个类,其基类缺省为Object,那么它缺省是不具备克隆能力的(下一节会看到)。只要你不明确地添加克隆能力,它就不会具备。但是你可以向任意层次的子类添加克隆能力,从那层以下的子类,也就都具备了克隆能力,就像这样:
import java.util.*;
class Person {
}
class Hero extends Person {
}
class Scientist extends Person implements Cloneable {
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
// This should never happen: It's Cloneable already!
throw new RuntimeException(e);
}
}
}
class MadScientist extends Scientist {
}
public class HorrorFlick {
public static void main(String[] args) {
Person p = new Person();
Hero h = new Hero();
Scientist s = new Scientist();
MadScientist m = new MadScientist();
// ! p = (Person)p.clone(); // Compile error
// ! h = (Hero)h.clone(); // Compile error
s = (Scientist) s.clone();
m = (MadScientist) m.clone();
}
}
在类继承体系中添加克隆能力之前,编译器会阻止你的克隆操作。当Scientist类添加了克隆能力后,Scientist及其所有子类就都具备了克隆能力。
为何采用此奇怪的设计?
是否这一切看起来像是一种奇怪的安排?是的,它确实奇怪。你可能想知道为何如此设计?这种设计背后有何意图?
起初,Java被设计成为一种控制硬件设备的语言,根本没考虑到Internet。现在,Java作为通用性编程语言,程序员需要具备克隆对象的能力。因此,clone()被添加到根类Object中,原本声明为public方法,这样你就能复制任意对象。这似乎是最方便的解决方案,但是之后呢,它会有什么危害吗?

是的,当Java被视为终极的Internet编程语言时,情况就变了。安全问题突显了出来,当然,这都是使用对象所带来的问题,因为你必定不愿意任何人都能克隆你的机密对象。所以你现在看到的设计,是在最初简单而直接的设计上,做了许多修补之后的版本:Object中的clone()被声明为protected。你必须重载它、实现Cloneable接口、并做异常处理。
值得注意的是,只有真正需要调用Objectclone()方法时,你才必须实现Cloneable接口,因为在运行期会检查你的类是否实现了Cloneable接口。不过,为了使具备克隆能力的对象保持一致性(毕竟Cloneable是空的),即使不调用Objectclone()方法,你仍然应该实现此接口。
控制克隆能力
为了移除克隆能力,你也许会建议将clone()方法声明为private。但是这行不通,因为对于基类的方法,无法在子类中削弱其访问能力。然而,我们必须有能力控制某个对象是否可以被克隆。对此你可能会有以下态度:
1. 不关心。你并不做任何克隆操作,即使你的类不可克隆,但是只要愿意,就能向其子类添加克隆能力。这只有在缺省的Object.clone()能够合理地处理类中所有属性时才起作用。
2. 支持clone()。按照标准的惯例:实现Cloneable接口、重载clone()方法。在重载的clone()中,调用super.clone(),并捕获所有异常(所以你重载的clone()不会抛出异常)。
3. 有条件地支持克隆。如果你的类(例如容器类)包含其他对象的引用,它们不一定是可克隆的,但你的clone()方法应该试着克隆它们,如果抛出异常,只需将异常传给程序员。例如,考虑一种特殊的ArrayList,它需要克隆自己包含的所有对象。编写这样的ArrayList时,你并不知道客户端程序员会向你的ArrayList存入何种类型的对象,因此你也不知道它们能否被克隆。
4. 不实现Cloneable接口,但是以protected方式重载clone()方法,为所有属性创建正确的复制行为。于是该类的任何子类,都可以重载clone()并调用super.clone()产生正确的复制行为。注意,你的clone()可以(并且应该)调用super.clone(),即使super.clone()预期的是个Cloneable对象(否则会抛出异常)。没人会直接对你的类的对象调用clone(),只能通过其子类才行,而要想让它正常工作,其子类必须实现Cloneable接口。
5. 不实现Cloneable接口,重载clone()使之抛出异常,以阻止克隆操作。只有此类的所有子类,都在各自的clone()中调用super.clone(),这种阻止克隆的方法才起作用。否则,程序员还是有可能绕开它。
6. 将你的类声明为final以阻止克隆。如果它的任何父类(祖先类)都没有重载clone(),那么此方法就行不通了。如果父类重载了clone(),那么让你的类再次重载clone(),并抛出CloneNotSupportedException。将类声明为final,是唯一有保证的防止克隆的方法。此外,当处理机密对象,或需要控制对象的数量时,应该将所有构造器都设置为private,然后提供一个(或多个)创建对象的专用方法。这些方法可以限制创建对象的数量和条件。(对此有一个特别的例子:singleton模式。可以从www.BruceEckel.com上的《Thinking in Patterns (with Java)》中找到。)

下面的例子演示了各种方法,以实现克隆能力,然后关闭继承体系下层子类的克隆能力:
// Checking to see if a reference can be cloned.
// Can't clone this because it doesn't override clone():
class Ordinary {
}
// Overrides clone, but doesn't implement Cloneable:
class WrongClone extends Ordinary {
public Object clone() throws CloneNotSupportedException {
return super.clone(); // Throws exception
}
}
// Does all the right things for cloning:
class IsCloneable extends Ordinary implements Cloneable {
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
// Turn off cloning by throwing the exception:
class NoMore extends IsCloneable {
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}
class TryMore extends NoMore {
public Object clone() throws CloneNotSupportedException {
// Calls NoMore.clone(), throws exception:
return super.clone();
}
}
class BackOn extends NoMore {
private BackOn duplicate(BackOn b) {
// Somehow make a copy of b and return that copy.
// This is a dummy copy, just to make the point:
return new BackOn();
}
public Object clone() {
// Doesn't call NoMore.clone():
return duplicate(this);
}
}
// You can't inherit from this, so you can't override
// the clone method as you can in BackOn:
final class ReallyNoMore extends NoMore {
}
public class CheckCloneable {
public static Ordinary tryToClone(Ordinary ord) {
String id = ord.getClass().getName();
System.out.println("Attempting " + id);
Ordinary x = null;
if (ord instanceof Cloneable) {
try {
x = (Ordinary) ((IsCloneable) ord).clone();
System.out.println("Cloned " + id);
} catch (CloneNotSupportedException e) {
System.err.println("Could not clone " + id);
}
} else {
System.out.println("Doesn't implement Cloneable");
}
return x;
}
public static void main(String[] args) {
// Upcasting:
Ordinary[] ord = { new IsCloneable(), new WrongClone(), new NoMore(),
new TryMore(), new BackOn(), new ReallyNoMore(), };
Ordinary x = new Ordinary();
// This won't compile; clone() is protected in Object:
// ! x = (Ordinary)x.clone();
// Checks first to see if a class implements Cloneable:
for (int i = 0; i < ord.length; i++)
tryToClone(ord[i]);
}
}
第一个类,Ordinary,代表我们在本书中常见的类型:它不是关闭克隆能力,而是不支持也不阻止克隆。但是,如果你有一个Ordinary子类的对象,其引用向上转型为Ordinary后,你无法分辨它是否可克隆。
WrongClone类演示了实现克隆机制的错误方式。它以public方式重载了Object.clone(),但是没有实现Cloneable,因此调用super.clone()时(最终会调用Object.clone()),会抛出CloneNotSupportException,所以无法克隆。
IsCloneable执行了所有正确的操作:重载clone()方法、实现Cloneable接口。然而,它的clone()方法,以及上例中的其他方法都没有捕获CloneNotSupportedException,而是将它传给方法的调用者,后者必须用try-catch区块包住clone()。在你自己的clone()方法中,通常会在clone()内捕获CloneNotSupportedException,而不是将它传出。如你所见,本例是为演示才将异常传递出来。
NoMore类尝试关闭克隆能力,采用了Java设计者建议的方式:在子类的clone()中抛出CloneNotSupportedException。当TryMore类的clone()调用super.clone()时,会抛出异常,阻止克隆。
但是,在重载的clone()方法中,如果程序员不按正确方法调用super.clone()又怎么样呢?在BackOn中可以看到这是如何发生的。BackOn使用独立的duplication()复制当前对象,在clone()中它取代了super.clone()。这不会抛出异常,而且新的类也是可克隆的。因此,无法依赖抛出异常来防止克隆能力。ReallyNoMore示范了唯一不会有问题的解决方案。类声明为final,就不可以被继承了。这意味着,如果在final的类中,clone()方法抛出异常,由于它不会被子类修改,所以肯定能阻止克隆。(不能从继承体系任意层

的类直接显示明确地调用Object.clone(),只能调用super.clone()访问其直接父类。)因此,如果你的对象需要考虑安全因素,可以将类声明为final
CheckCloneable类的第一个方法tryToClone()Ordinary对象为参数,使用instanceof检查是否可以被克隆。如果可以,将对象转型为IsCloneable,调用clone(),再将结果类型转回给Ordinary,其间会捕获任何异常。注意,这里使用了运行期类型识别机制(RTTI,参见第十一章)打印类名,以便于你看到程序进展。
main()中,创建了不同类型的Ordinary对象,向上转型为Ordinary后存贮在数组中。这之后的两行代码,创建了一个平凡的Ordinary对象,并试图克隆它。然而,这行代码不能通过编译,因为clone()Object中是protected方法。余下的代码遍历数组,尝试克隆每个对象,并报告每次克隆成功与否。
现在做个小结,如果你希望一个类可以被克隆:
1. 实现Cloneable接口。
2. 重载clone()
3. 在你的clone()中调用super.clone()
4. 在你的clone()中捕获异常。
做到这些即可获得令人满意的效果。
拷贝构造器
克隆机制的建立似乎是很复杂的过程,也许应该有别的解决方案。如前所述,使用序列化是一种方法。你可能还会想到另一种方法(特别是,如果你是C++程序员的话),做一个特殊的构造器,它的工作就是复制对象。在C++中这称为拷贝构造器(copy constructor)。乍看起来,这似乎是显而易见的解决方案,可实际上这行不通。见下例:
// A constructor for copying an object of the same
// type, as an attempt to create a local copy.
import java.lang.reflect.*;
class FruitQualities {
private int weight;
private int color;
private int firmness;
private int ripeness;
private int smell;
// etc.
public FruitQualities() { // Default constructor
// Do something meaningful...
}
// Other constructors:
// ...
// Copy constructor:
public FruitQualities(FruitQualities f) {
weight = f.weight;
color = f.color;
firmness = f.firmness;
ripeness = f.ripeness;
smell = f.smell;
// etc.
}
}
class Seed {
// Members...
public Seed() { /* Default constructor */
}
public Seed(Seed s) { /* Copy constructor */
}
}
class Fruit {
private FruitQualities fq;
private int seeds;
private Seed[] s;
public Fruit(FruitQualities q, int seedCount) {
fq = q;
seeds = seedCount;
s = new Seed[seeds];
for (int i = 0; i < seeds; i++)
s[i] = new Seed();
}
// Other constructors:
// ...
// Copy constructor:
public Fruit(Fruit f) {
fq = new FruitQualities(f.fq);
seeds = f.seeds;
s = new Seed[seeds];
// Call all Seed copy-constructors:
for (int i = 0; i < seeds; i++)
s[i] = new Seed(f.s[i]);
// Other copy-construction activities...
}
// To allow derived constructors (or other
// methods) to put in different qualities:
protected void addQualities(FruitQualities q) {
fq = q;
}
protected FruitQualities getQualities() {
return fq;
}
}
class Tomato extends Fruit {
public Tomato() {
super(new FruitQualities(), 100);
}
public Tomato(Tomato t) { // Copy-constructor
super(t); // Upcast for base copy-constructor
// Other copy-construction activities...
}
}
class ZebraQualities extends FruitQualities {
private int stripedness;
public ZebraQualities() { // Default constructor
super();
// do something meaningful...
}
public ZebraQualities(ZebraQualities z) {
super(z);
stripedness = z.stripedness;
}
}
class GreenZebra extends Tomato {
public GreenZebra() {
addQualities(new ZebraQualities());
}
public GreenZebra(GreenZebra g) {
super(g); // Calls Tomato(Tomato)
// Restore the right qualities:
addQualities(new ZebraQualities());
}
public void evaluate() {
ZebraQualities zq = (ZebraQualities) getQualities();
// Do something with the qualities
// ...
}
}
public class CopyConstructor {
public static void ripen(Tomato t) {
// Use the "copy constructor":
t = new Tomato(t);
System.out.println("In ripen, t is a " + t.getClass().getName());
}
public static void slice(Fruit f) {
f = new Fruit(f); // Hmmm... will this work?
System.out.println("In slice, f is a " + f.getClass().getName());
}
public static void ripen2(Tomato t) {
try {
Class c = t.getClass();
// Use the "copy constructor":
Constructor ct = c.getConstructor(new Class[] { c });
Object obj = ct.newInstance(new Object[] { t });
System.out.println("In ripen2, t is a " + obj.getClass().getName());
} catch (Exception e) {
System.out.println(e);
}
}
public static void slice2(Fruit f) {
try {
Class c = f.getClass();
Constructor ct = c.getConstructor(new Class[] { c });
Object obj = ct.newInstance(new Object[] { f });
System.out.println("In slice2, f is a " + obj.getClass().getName());
} catch (Exception e) {
System.out.println(e);
}
}
public static void main(String[] args) {
Tomato tomato = new Tomato();
ripen(tomato); // OK
slice(tomato); // OOPS!
ripen2(tomato); // OK
slice2(tomato); // OK
GreenZebra g = new GreenZebra();
ripen(g); // OOPS!
slice(g); // OOPS!
ripen2(g); // OK
slice2(g); // OK
g.evaluate();
}
}
乍看之下有点奇怪,水果(fruit)当然有品质(qualities),为什么不将那些品质作为Fruit类的属性成员直接放到Fruit类中呢?有两个可能的原因。
第一个原因是,你希望更容易地插入或修改品质。注意,Fruit有一个protected addQualities()方法,允许子类调用。(你也许会想到,符合逻辑的做法是写一个protectedFruit构造器,它以FruitQualities为参数。但是构造器不能继承,它在第二层或更底层的子类中就不可用了。)通过将水果品质做成独立的类FruitQualities,然后使用组合,获得了更好的灵活性,可以在特殊的Fruit对象的生命周期中间改变其品质。
FruitQualities独立成为对象的第二个原因是,你可以通过继承和多态机制添加新的品质,或是改变其行为。请注意GreenZebra(我种过的一种番茄,很漂亮),它的构造器调用addQualities(),并且传入一个ZebraQualities对象,ZebraQualities继承自FruitQualities,因此可以在基类中转型为FruitQualities。当然,GreenZebra使用FruitQualities时,必须向下转型为正确类型(如evaluate()中所见),不过它知道正确的类型是ZebraQualities
还有Seed类,Fruit(定义为携带有自己的种子)4包含一个Seed数组。
最后,注意每个类都有拷贝构造器,它们必须负责调用基类和成员对象的拷贝构造器,以达到深层拷贝的效果。CopyConstructor类测试拷贝构造器。ripen()方法接收Tomato参数,对其执行拷贝构造器,以生成对象副本。
t = new Tomato(t);
slice()以更通用的Fruit对象为参数,对它进行复制。

f = new Fruit(f);
main()中测试了各种不同的Fruit。从程序输出可以看到问题所在。在slice()内,对Tomato做拷贝构造之后,其结果不再是Tomato对象,而只是Fruit。它丢失了所有Tomato特有的信息。而测试GreenZebra时,ripen()slice()将其分别变成TomatoFruit。因此,很不幸,在Java中使用拷贝构造器创建对象的局部拷贝是不可行的。
为什么在C++中可行?
拷贝构造器是C++的基础功能,因为它自动生成对象的局部拷贝。而前例证明这在Java中不可行。为什么?在Java中,我们操控的都是引用;而在C++中,不但有类似引用的东西,甚至可以直接传递对象。C++中拷贝构造器的目的是:通过传值的方式传递对象时,复制此对象。所以此机制在C++中运作的很好。但你应该记住,它在Java中行不通,所以不要使用。
只读类
虽然在适当的情况下,clone()生成的局部拷贝可以满足我们的需求,但这也是个典型,它强制程序员(clone()方法的作者)必须负责避免别名的负面效应。当你开发的类库具有通用目的、被广泛使用,使你不能假设你的类总是在恰当的位置被克隆时,又会怎么样吗?或者更可能的情况是,如果你为了效率而允许出现别名——为了避免不必要的复制对象,但你不想因为别名而产生负面影响,这是又会怎样呢?
一种解决方法是创建恒常对象(immutable objects,它属于只读类。在你的类中,不要定义会修改对象内部状态的方法。对于这样的类,出现别名也不会有影响,因为你只能读取对象的内部状态,即使很多代码都读取同一个对象,也没有问题。
作为恒常对象的简单示例,可以参考Java标准类库中所有基本类型的包装类。你可能已经发现了,如果你想在容器中存储int,例如ArrayList(只接受Object引用),可以先将int用标准类库的Integer类包装:
// The Integer class cannot be changed.
import java.util.*;
public class ImmutableInteger {
public static void main(String[] args) {
List v = new ArrayList();
for (int i = 0; i < 10; i++)
v.add(new Integer(i));
// But how do you change the int inside the Integer?
}
}
Integer类(其他包装类也同样)已很简单的方式实现了恒常性:它没有让你去修改对象内容的方法。
如果你确实需要一个对象,它包含基本类型成员,并且此成员可以修改。你就必须创建自己的类。幸运的是,这很简单。下面的类采用了JavaBean的命名惯例:
// A changeable wrapper class.
import java.util.*;
class IntValue {
private int n;
public IntValue(int x) {
n = x;
}
public int getValue() {
return n;
}
public void setValue(int n) {
this.n = n;
}
public void increment() {
n++;
}
public String toString() {
return Integer.toString(n);
}
}
public class MutableInteger {
public static void main(String[] args) {
List v = new ArrayList();
for (int i = 0; i < 10; i++)
v.add(new IntValue(i));
System.out.println(v);
for (int i = 0; i < v.size(); i++)
((IntValue) v.get(i)).increment();
System.out.println(v);
}
}
如果不需要保持私有性,缺省初始化为零就可以满足要求(那就不需要构造器了),并且你不关心打印问题(那就不需要toString()了),那么IntValue 还可以更简化:
class IntValue { int n; }
取出元素并对其进行类型转换操作,虽然显得有点笨拙,但那是ArrayList的功能,而不是IntValue的。

创建只读类
你可以创建自己的只读类。见下例:
// Objects that cannot be modified are immune to aliasing.
public class Immutable1 {
private int data;
public Immutable1(int initVal) {
data = initVal;
}
public int read() {
return data;
}
public boolean nonzero() {
return data != 0;
}
public Immutable1 multiply(int multiplier) {
return new Immutable1(data * multiplier);
}
public static void f(Immutable1 i1) {
Immutable1 quad = i1.multiply(4);
System.out.println("i1 = " + i1.read());
System.out.println("quad = " + quad.read());
}
public static void main(String[] args) {
Immutable1 x = new Immutable1(47);
System.out.println("x = " + x.read());
f(x);
System.out.println("x = " + x.read());
}
}
所有数据都是private,而且你看到了,没有要去修改这些数据的public方法。实际上,虽然multiply()方法修改了对象,但它创建了一个新的Immutable1对象,并没有修改原始对象。

f()方法以Immutable1对象为参数,对其执行了各种操作,而main()中的输出说明,f()没有修改x。因此,x对象可以有很多别名,也不会造成伤害,因为Immutable1类的设计保证了对象不会被修改。
恒常性(immutability)的缺点
创建恒常的类,初看起来似乎是一种优雅的解决方案。然而,无论何时当你需要一个被修改过的此类的对象的时候,必须要承受创建新对象的开销,也会更频繁地引发垃圾回收。对某些类而言,这不成问题,但对另一些类(例如String类),其代价可能昂贵得让人不得不禁止这么做。
解决之道是创建一个可以被修改的伴随类(companion class)。当你需要做大量修改动作时,可以转为使用可修改的伴随类,修改操作完毕后,再转回恒常类。
前面的例子在修改之后能够展示这种方法:
// A companion class to modify immutable objects.
class Mutable {
private int data;
public Mutable(int initVal) {
data = initVal;
}
public Mutable add(int x) {
data += x;
return this;
}
public Mutable multiply(int x) {
data *= x;
return this;
}
public Immutable2 makeImmutable2() {
return new Immutable2(data);
}
}
public class Immutable2 {
private int data;
public Immutable2(int initVal) {
data = initVal;
}
public int read() {
return data;
}
public boolean nonzero() {
return data != 0;
}
public Immutable2 add(int x) {
return new Immutable2(data + x);
}
public Immutable2 multiply(int x) {
return new Immutable2(data * x);
}
public Mutable makeMutable() {
return new Mutable(data);
}
public static Immutable2 modify1(Immutable2 y) {
Immutable2 val = y.add(12);
val = val.multiply(3);
val = val.add(11);
val = val.multiply(2);
return val;
}
// This produces the same result:
public static Immutable2 modify2(Immutable2 y) {
Mutable m = y.makeMutable();
m.add(12).multiply(3).add(11).multiply(2);
return m.makeImmutable2();
}
public static void main(String[] args) {
Immutable2 i2 = new Immutable2(47);
Immutable2 r1 = modify1(i2);
Immutable2 r2 = modify2(i2);
System.out.println("i2 = " + i2.read());
System.out.println("r1 = " + r1.read());
System.out.println("r2 = " + r2.read());
}
}
Immutable2内的一些方法,与之前相同,每当需要做修改时,就生成一个新对象,以保证原对象的恒常性。例如add()multiply()方法。Immutable2的伴随类是Mutable,它也有add()multiply()方法,但它们是直接修改Mutable对象,而不是生成新对象。此外,Mutable有一个方法,它使用自己的数据生成一个Immutable2对象,反之亦然。
两个static方法modify1()modify2(),演示了两种不同的方法,得到同样的结果。在modify1()中,所有的工作都在Immutable2类中完成。可以看到,在这个过程中创建了四个新的Immutable2对象。(每次对val重新赋值,前一个对象就成为了垃圾。)

modify2()中可以看到,第一个动作是接收Immutable2 y,并且由y生成Mutable对象。(这与先前调用clone()相似,但是创建了不同类型的对象。)然后使用Mutable对象,无需创建新对象即可执行大量的修改操作。最后,转回Immutable2对象。这个过程产生了两个新对象(Mutalbe对象,以及作为结果的Immutable2对象),而不是四个。
当下列情况发生时,此方法十分有用:
1. 你需要恒常的对象,而且
2. 你经常需要做大量的修改,或者
3. 创建新的恒常对象代价昂贵。
恒常的String
考虑下面的代码:
public class Stringer {
public static String upcase(String s) {
return s.toUpperCase();
}
public static void main(String[] args) {
String q = new String("howdy");
System.out.println(q); // howdy
String qq = upcase(q);
System.out.println(qq); // HOWDY
System.out.println(q); // howdy
}
}
q传递给upcase()时,其实是传入q的引用的副本。引用指向的对象仍然在它原来的位置上。传递引用时,即复制了引用。
upcase()的定义可以看到,传入的引用名为s,只在upcase()运行时它才存在。一旦upcase()执行完毕,局部引用s也就消失了。将原始字符串转为大写字符后,upcase()返回此结果。当然,它其实是返回指向结果的引用。不过,它返回的引用是指向一个新对象,原先的q被放在一边。这是怎么发生的呢?

隐式的常量
如果你这么写:
String s = "asdf";
String x = Stringer.upcase(s);
你真的想用upcase()方法修改参数吗?通常,你不会这样。因为对于使用参数的方法而言,参数通常只是提供信息,很少需要修改。这是很重要的保证,它使得代码易于编写和理解。
C++中,此保证的可用性很重要,以至于C++添加了const这个特殊的关键字,让程序员确保一个引用(C++中的指针或引用)不可以被用来修改源对象。不过C++程序员必须非常细心,记住使用const的每一处。这很容易令人混淆且容易忘记。
重载 ’+’ StringBuffer
String类的对象被设计为恒常的,并使用了前面介绍的伴随类技术。如果查看JDK文档中的String类(稍后会对此进行总结),你会发现,类中每个设计修改String的方法,在修改的过程中,确实生成并返回了一批新的String对象。最初的String并没有受到影响。C++const提供由编译器支持的对象恒常性,Java没有这样的功能。想要获得恒常对象,你必须自己动手,就像String那样。
由于String对象是恒常的,对某个String可以随意取多个别名。因为它是只读的,任何引用也不可能修改该对象,也就不会影响其他引用。所以,只读对象很好地解决了别名问题。
这似乎可以解决所有问题。每当需要修改对象时,就创建一堆修改过的新版对象,如同String那样。然而,对某些操作而言,这太没有效率了。String的重载操作符’+’就是个重要的例子。重载是指,对特定的类,’+’被赋予额外的含义。(为String重载的’+’’+=’,是Java唯一重载的操作符,而且Java不允许程序员重载其他操作符。)5
使用String对象时,’+’用来连接String对象:
String s = "abc" + foo + "def" + Integer.toString(47);
你可以想象它是如何工作的。String “abc”可能有一个append()方法,它生成了一个连接了”abc”foo的新的String对象。新的String连接”def”之后,生成另一个新的String,依此类推。

这当然可以运行,但它需要大量的中间String对象,才能生成最终的新String,而那些中间结果需要作垃圾回收。我怀疑Java的设计者起先就是这么做的(这是软件设计中的一个教训,你无法知道所有事情,直到形成代码,并运作起来)。我猜他们发现了这种做法有着难以忍受的低效率。
解决之道是可变的伴随类,类似前面演示的例子。String的伴随类称作StringBuffer,在计算某些表达式,特别是String对象使用重载过的’+’’+=’时,编译器会自动创建StringBuffer。下面的例子说明其中发生了什么:
// Demonstrating StringBuffer.
public class ImmutableStrings {
public static void main(String[] args) {
String foo = "foo";
String s = "abc" + foo + "def" + Integer.toString(47);
System.out.println(s);
// The "equivalent" using StringBuffer:
StringBuffer sb = new StringBuffer("abc"); // Creates String!
sb.append(foo);
sb.append("def"); // Creates String!
sb.append(Integer.toString(47));
System.out.println(sb);
}
}
在生成String s的过程中,编译器使用sb执行了大致与下面的工作等价的代码:创建一个StringBuffer,使用append()向此StringBuffer对象直接添加新的字符串(而不是每次制作一个新的副本)。虽然这种方法更高效,但是对于引号括起的字符串,例如”abc””def”,它并不起作用,编译器会将其转为String对象。所以,尽管StringBuffer提供了更好的效率,可能仍会产生超出你预期数量的对象。
StringStringBuffer
下面总结了StringStringBuffer都可用的方法,你可以感受到与它们交互的方式。表中并未包含所有方法,只包含了与本讨论相关的重要方法。重载的方法被置于单独一列。

首先是String类:
方法
参数,重载
用途
Constructor
重载: 缺省、空参数、 StringStringBufferchar 数组、byte 数组.
创建 String 对象。
length( )
String的字符数。
charAt( )
int 索引
String 中指定位置的字符。
getChars( ), getBytes( )
复制源的起点与终点、 复制的目标数组、目标数组的索引。
复制 charbyte到外部数组。
toCharArray( )
生成char[],包含 String的所有字符。
equals( ), equals-IgnoreCase( )
做比较的 String
两个String的等价测试。
compareTo( )
做比较的 String
根据词典顺序比较String与参数,结果为负值、零、或正值。大小写有别。
regionMatches( )
当前String的偏移位置、另一个 String 、及其偏移、要比较的长度。重载增加了忽略大小写
返回boolean, 代表指定区域是否相匹配。
startsWith( )
测试起始String 。重载增加了参数的偏移。
返回boolean,代表是否此 String 以参数为起始。
endsWith( )
测试后缀String
返回boolean,代表是否此 String 以参数为后缀。
indexOf( ), lastIndexOf( )
重载: charchar 和起始索引、StringString和起始索引.
如果当前String中不包含参数,返回-1,否则返回参数在String中的位置索引。LastIndexOf()由末端反向搜索。
substring( )
重载: 起始索引、起始索引和终止索引
返回新的String对象,包含特定的字符集。
concat( )
要连接的String
返回新String对象,在原String后追加参数字符。
replace( )
搜索的旧字符,用来替代的新字符
返回指定字符被替换后的新String对象。如果没有发生替换,返回原String
toLowerCase( )
返回所有字母大小写修改后

toUpperCase( )
的新String对象。如果没有改动则返回原String
trim( )
去除两端的空白字符后,返回新String。如果没有改变,则返回原String
valueOf( )
重载: Objectchar[]char[] 和偏移和长度、booleancharintlongfloatdouble.
返回一个String,内含参数表示的字符。
intern( )
为每一个唯一的字符序列生成一个且仅生成一个String引用。
可以看到,当必须修改字符串的内容时,String的每个方法都会谨慎地返回一个新的String对象。还要注意,如果内容不需要修改,方法会返回指向源String的引用。这节省了存储空间与开销。
以下是StringBuffer类:
方法
参数, 重载
用途
Constructor
重载: 空参数、要创建的缓冲区长度、String的来源.
创建新的StringBuffer对象。
toString( )
由此StringBuffer生成String
length( )
StringBuffer中的字符个数。
capacity( )
返回当前分配的空间大小。
ensure- Capacity( )
代表所需容量的整数
要求 StringBuffer至少有所需的空间。
setLength( )
代表缓冲区中字符串长度的整数
截短或扩展原本的字符串。如果扩展,以null填充新增的空间。
charAt( )
代表所需元素位置的整数。
返回char 在缓冲区中的位置。
setCharAt( )
代表所需元素位置的整数,和新的char
修改某位置上的值。
getChars( )
复制源的起点与终点、复制的目的端数组、目的端数组的索引。
复制char到外围数组。没有String 中的getBytes( )
append( )
重载: ObjectString
参数转为字符串,然后追加

char[]char[] 和偏移和长度、booleancharintlongfloatdouble.
到当前缓冲区的末端,如果必要,缓冲区会扩大。
insert( )
被重载过,第一个参数为插入起始点的偏移值: ObjectStringchar[]booleancharintlongfloatdouble.
第二个参数转为字符串,插入到当前缓冲区的指定位置。如果必要,缓冲区会扩大。
reverse( )
逆转缓冲区内字符的次序。
最常用的方法是append(),当计算包含’+’’+=’操作符的String表达式时,编译器会使用它。Insert()方法有类似的形式,这两个方法都会在缓冲区中进行大量操作,而不需创建新对象。
String是特殊的
到目前为止,你已经看到了,String类不同于Java中一般的类。 String有很多特殊之处,它不仅仅只是一个Java内置的类,它已经成为Java的基础。而且事实上,双引号括起的字符串都被编译器转换为String对象,还有专门重载的’+’’+=’操作符。此外,你还能看到其他特殊之处:使用伴随类StringBuffer精心建构的恒常性,以及编译器中的一些额外的魔幻式的功能。
总结
Java中,所有对象标识符都是引用,而且所有对象都在堆(heap)上创建,只有当对象不再使用的情况下,垃圾回收器才工作。所有这些都改变了对象的操作方式,特别是传递与返回对象。例如,在CC++中,如果在方法中要初始化一块存储空间,可能需要用户向方法传入那块存储空间的地址。否则,你必须为谁负责回收那块存储空间而操心。如此,接口和对此方法的理解将变得更复杂了。但是在Java中,你永远也无需担心承担此责任,也不用操心需要某个对象时它是否存在。因为Java会帮你分担。你可以在需要对象时(立刻)创建它,而无需因为要为对象负责而操心传递的技巧。你只需简单地传递引用。有时,这种简化微不足道,有时则令人难以置信。
所有这些神奇,带来了两个缺点:
1. 由于额外的内存管理,你总会付出效率上的代价(虽然代价可能很小),而且在程序耗时上总会有细微的不确定性(因为当内存不足时,垃圾回收器会强行介入)。对大多数应用程序而言,是好处大于缺点,而且还有专门提高程序运行速度的hotspot技术,使之不再是个大问题。
2. 别名效应:有时你会意外地赋予同一个对象两个引用,只有当两个引用以为指向了不同的对象时,这才会成为问题。这是需要你多注意的地方,必要时,使用clone()或者复制对象以防止其他引用对出乎意料的改变感到惊讶。另一种情况是,你为了效率而支持别名,创建恒常的对象,其操作可返回同类型的(或不同类型)新对象,但永远不会修改源对象,所以此对象的任何别名都不会改变对象。
有些人认为Java的克隆机制是一份修修补补的设计,永远都不应使用,所以他们实现自己版本的克隆,而且永不调用Object.clone()方法,如此,就消除了实现Cloneable以及捕获CloneNotSupportedException异常的需求。这肯定是合理的方法,而且因为标准Java库很少支持clone(),所以它显然是安全的。
分享到:
评论

相关推荐

    Java 语言基础 —— 非常符合中国人习惯的Java基础教程手册

    然 new 运算符返回对一个对象的引用,但与 C、C++中的指针不同,对象的引用是指 向一个中间的数据结构,它存储有关数据类型的信息以及当前对象所在的堆的地址, 而对于对象所在的实际的内存地址是不可操作的,这就保证...

    C++大学教程

    1.9 Java、Internet与万维网--------------------------------------------7 1.10 其它高级语言------------------------------------------------------8 1.11 结构化编程-----------------------------------...

    Java基础入门及提高.pdf

    《Java基础入门及提高》,整理:yyc、spirit。PDF 格式,大小 4.8 MB,非影印版。 前言: 同人类任何语言一样,Java 为我们提供了一种表达思想的方式。如操作得当,同其他方式相比,随着问题变得愈大和愈复杂,这种...

    新版Android开发教程.rar

    ----------------------------------- Android 编程基础 1 封面----------------------------------- Android 编程基础 2 开放手机联盟 --Open --Open --Open --Open Handset Handset Handset Handset Alliance ...

    JAVA_Thinking in Java(中文版 由yyc,spirit整理).chm

    A.1.3 传递和使用Java对象 A.1.4 JNI和Java违例 A.1.5 JNI和线程处理 A.1.6 使用现成代码 A.2 微软的解决方案 A.3 J/Direct A.3.1 @dll.import引导命令 A.3.2 com.ms.win32包 A.3.3 汇集 A.3.4 编写回调函数 A.3.5 ...

    Thinking in Java(中文版 由yyc,spirit整理).chm

    A.1.3 传递和使用Java对象 A.1.4 JNI和Java违例 A.1.5 JNI和线程处理 A.1.6 使用现成代码 A.2 微软的解决方案 A.3 J/Direct A.3.1 @dll.import引导命令 A.3.2 com.ms.win32包 A.3.3 汇集 A.3.4 编写回调函数 A.3.5 ...

    C++大学教程,一本适合初学者的入门教材(part1)

    1.9 Java、Internet与万维网 1.10 其他高级语言 1.11 结构化编程 1.12 典型C++环境基础 1.13 C++与本书的一般说明 1.14 C++编程简介 1.15 简单程序:打印一行文本 1.16 简单程序:两个整数相加 1.17 内存的...

    易语言程序免安装版下载

    6) 修改MYSQL支持库跨静态编译的EXE和DLL传递连接句柄和记录集句柄无效的BUG(改动较大,可能会产生兼容性问题,我们已经仔细测试,也请使用到此库的用户帮助我们多多测试,以便及早发现问题,谢谢) 7) 其它修改 ...

    C++大学教程,一本适合初学者的入门教材(part2)

    1.9 Java、Internet与万维网 1.10 其他高级语言 1.11 结构化编程 1.12 典型C++环境基础 1.13 C++与本书的一般说明 1.14 C++编程简介 1.15 简单程序:打印一行文本 1.16 简单程序:两个整数相加 1.17 内存的...

    Visual C++ 2010入门经典(第5版)--源代码及课后练习答案

     ·使用visual c++ 2010支持的两种c++语言技术讲述c++编程的基础知识  ·分享c++程序的错误查找技术,并介绍通用的调试原则讨论每一个windows应用程序的结构和基本元素  ·举例说明如何使用mfc开发本地windows...

    Visual C++ 2005入门经典--源代码及课后练习答案

    Ivor Horton还著有Beginning Visual C++ 6、Beginning C Programming和Beginning Java 2等多部入门级好书。 目录 封面 -18 前言 -14 目录 -9 第1章 使用Visual C++ 2005编程 1 1.1 .NET Framework 1 1.2 CLR 2...

    Visual C#2010 从入门到精通(Visual.C#.2010.Step.By.Step).完整去密码锁定版 I部分

    无论是刚开始接触面向对象编程的新手,还是打算转移到c#的具有c,c++或者java基础的程序员,都可以从本书中吸取到新的知识。 作译者 john sharp,content master首席技术专家。content master隶属于cm集团,cm集团...

Global site tag (gtag.js) - Google Analytics