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() 方法,而继承自 Object PassReferences 类没有重新定义 toString() 方法。所以使用的是 Object toString() 方法,它打印出类名以及存储对象的地址(不是引用,而是实际的对象)。输出看起来像这样:
p inside main(): PassReferences@ad3ba4
h inside f(): PassReferences@ad3ba4
 
可以看到, p h 都指向同一个对象。像这样只是发送一个参数给方法,比复制一个新的 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 ,因此 x y 指向同一个对象。所以, 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 ,之后必须将其转型为合适的类型。此例演示了 ArrayList clone() 方法,它并不自动克隆 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() 方法;否则,你的(现在为 public clone() 虽然能用,其行为却不一定正确(即使由于 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);
   }
}  
DepthReading TemperatureReading 十分相似,它们都只包含基本类型。因此,它们的 clone() 方法也很简单:调用 super.clone() 然后返回结果。注意,这两个类的 clone() 代码相同。
OceanReading DepthReading TemperatureReading 对象组成。所以,如果要做深层拷贝,它的 clone() 必须克隆 OceanReading 内部的所有引用。为此, super.clone() 的结果必须转型为 OceanReading 对象(因此你能够访问 depth temperature 的引用)。
深层拷贝 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 也被复制,但事情并非如此。当 Int2 clone() Int3 clone() 而被调用时,它又调用了 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");
   }
}
 
Thing2 Thing4 都包含成员对象,因此可以做深层拷贝。有趣的是,编写 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 接口、并做异常处理。
值得注意的是,只有真正需要调用 Object clone() 方法时,你才必须实现 Cloneable 接口,因为在运行期会检查你的类是否实现了 Cloneable 接口。不过,为了使具备克隆能力的对象保持一致性(毕竟 Cloneable 是空的),即使不调用 Object clone() 方法,你仍然应该实现此接口。
控制克隆能力
为了移除克隆能力,你也许会建议将 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() 方法,允许子类调用。(你也许会想到,符合逻辑的做法是写一个 protected Fruit 构造器,它以 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() 将其分别变成 Tomato Fruit 。因此,很不幸,在 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 提供了更好的效率,可能仍会产生超出你预期数量的对象。
String StringBuffer
下面总结了 String StringBuffer 都可用的方法,你可以感受到与它们交互的方式。表中并未包含所有方法,只包含了与本讨论相关的重要方法。重载的方法被置于单独一列。

 
首先是 String 类:
方法
参数,重载
用途
Constructor
重载 : 缺省、空参数、 String StringBuffer char 数组、 byte 数组 .
创建 String 对象。
length( )
String 的字符数。
charAt( )
int 索引
String 中指定位置的字符。
getChars( ) , getBytes( )
复制源的起点与终点、 复制的目标数组、目标数组的索引。
复制 char byte 到外部数组。
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( )
重载: char char 和起始索引、 String String 和起始索引 .
如果当前 String 中不包含参数,返回- 1 ,否则返回参数在 String 中的位置索引。 LastIndexOf () 由末端反向搜索。
substring( )
重载 : 起始索引、起始索引和终止索引
返回新的 String 对象,包含特定的字符集。
concat( )
要连接的 String
返回新 String 对象,在原 String 后追加参数字符。
replace( )
搜索的旧字符,用来替代的新字符
返回指定字符被替换后的新 String 对象。如果没有发生替换,返回原 String
toLowerCase( )
返回所有字母大小写修改后

 
toUpperCase( )
的新 String 对象。如果没有改动则返回原 String
trim( )
去除两端的空白字符后,返回新 String 。如果没有改变,则返回原 String
valueOf( )
重载 : Object char[] char[] 和偏移和长度、 boolean char int long float double .
返回一个 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( )
重载 : Object String
参数转为字符串,然后追加

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值