您的位置:首页 > 编程语言 > Java开发

Java核心技术第6章(2)

2015-10-14 13:04 555 查看

6.2 对象克隆

   当拷贝一个变量时,原始变量与拷贝变量引用同一个对象,这就是说,改变一个变量所引用的对象将会对另一个变量产生影响.
Employee original = new Employee("John public", 50000);
Employee copy = original;
copy.raiseSalary(10);       // oops-also changed original
    如果创建一个对象的新copy,它的最初状态与original一样,但以后将可以各自改变各自的状态,那就需要使用clone方法.
Employee copy = original.clone();
copy.raiseSalary(10);       // ok-original unchanged
    不过,事情并没有那么简单,clone方法是Object类的一个 protected 方法,也就是说,在用户编写的代码中不能直接调用它.只有Employee类才能够克隆Employee对象.这种限制有一定的道理.这里查看一下Object类实现的clone方法.由于这个类对具体的类对象一无所知,所以只能将各个域进行对应的拷贝.如果对象中的所有数据域都属于数值或基本类型,这样拷贝域没有任何问题.但是,如果在对象中包含了子对象的引用,拷贝的结果会使两个域引用同一个子对象,因此原始对象与克隆对象共享这部分信息.
    默认的克隆操作是浅拷贝,它并没有克隆包含在对象中的内部对象.

    如果进行浅拷贝会发生什么呢?这要根据具体情况而定.如果原始对象与浅拷贝对象共享的子对象是不可变的,将不会产生任何问题.也确实存在这种情形.例如,子对象属于像String类这样的不允许改变的类;也有可能子对象在其生命周期内不会发生变化,既没有更改它们的方法,也没有创建对它引用的方法.

    然而,更常见的情况是子对象可变,因此,必须重新定义clone方法,以便实现克隆子对象的深拷贝.例如Employee类的hireDay域属于Date类,这就是一个可变的子对象.

    对于每一个类,都需要做出下列判断:

    1.默认的clone方法是否满足需求

    2.默认的clone方法是否能够通过调用可变子对象的clone得到修补

    3.是否不应该使用clone

    实际上,选项3是默认的.如果选择1或2,类必须:

    1.实现Cloneable接口

    2.使用 public 访问修饰符重新定义clone方法.

注释
:在Object类中,clone方法被声明为 protected,因此无法直接调用anObject.clone() .但是,所有子类都可以访问受保护的方法,并且每个类都是Object的子类.子类调用受保护的clone方法克隆它自己.为此,必须重新定义clone方法,并将它声明为 public,这样才能够让所有的方法克隆对象.

    在这里,Cloneable接口的出现与接口的正常使用没有任何关系.尤其是,它并没有指定clone方法,这个方法是从Object类继承而来的.接口在这里只是作为一个标记,表明类设计者知道要进行克隆处理.如果一个对象需要克隆,而没有实现Cloneable接口,就会产生一个已检验异常.

    注释:Cloneable接口是Java提供的几个标记接口(tagging interface)之一.通常使用接口的目的是为了确保类实现某个特定的方法或一组特定的方法,Comparable接口就是这样一个示例.而标记接口没有方法,使用它的唯一目的是可以用 instanceof 进行类型检查:
if (obj instanceof Cloneable) ...
    即使clone的默认实现(浅拷贝)能够满足需求,也应该实现Cloneable接口,将clone重定义为 public,并调用super.clone()
.下面是一个示例:
class Employee implements Cloneable
{
// raise visibility level to public, change return type
public Employee clone() throws CloneNotSupportedException
{
return (Employee)super.clone();
}
}
    上面这个clone方法并没有在Object.clone提供的浅拷贝基础上增加任何新功能,而只是将这个方法声明为 public .为了实现深拷贝,必须克隆所有可变的实例域.

    下面是一个建立深拷贝clone方法的一个实例:
class Employee implements Cloneable
{
...
public Employee clone() throws CloneNotSupportedException
{
// call Object.clone()
Employee cloned = (Employee)super.clone();
// clone mutable fields
clone.hireDay = (Date)hireDay.clone();
return cloned;
}
}
    只要在clone中含有没有实现Cloneable接口的对象,Object类的clone方法就会抛出一个CloneNot-SupportException异常.当然,Employee和Date类都实现了Cloneable接口,因此不会抛出异常.但是编译器并不知道这些情况,因此需要声明异常.
public Employee clone() throws CloneNotSupportedException
    如果将上面这种形式替换成捕获异常呢?
public Employee clone()
{
try
{
return (Employee)super.clone();
}
catch(CloneNotSupportedException e) { return null; }
// this won't happen, since we are Cloneable
}
    这种写法比较适用于 final 类,否则最好还是在这个地方保留 throws 说明符.

    必须谨慎地实现子类的克隆,例如,一旦为Employee类定义了clone方法,任何人都可以利用它克隆Manager对象. Employee的克隆方法能够完成这项重任吗?这将取决于Manager类中包含哪些域.在Manager类中有可能存在一些需要深拷贝的域,或者包含一些没有实现Cloneable接口的域.鉴于这个原因,应该将Object类中的clone方法声明为 protected,但是如果想让用户调用clone方法,就不能这样做.

    在程序6-3中,克隆了一个Employee对象,然后调用了两个改变域值的办法,raiseSalary方法改变了salary域值,setHireDay方法改变了hireDay域的状态.由于clone实现的是深拷贝,所以对这两个域值的改变并没有影响原始对象.

    注释:所有的数组类型均包含一个clone方法,这个方法被设为 public,而不是 protected ,可以利用这个方法创建一个包含所有数据元素拷贝的一个新数组.例如:
int[] luckyNumbers = {2, 4, 5};
int[] cloned = luckyNumbers.clone();
cloned[1] = 8;      // don't change luckyNumbers[1]
    clone/CloneTest.java如下所示:
package clone;

/**
* This program demonstrates cloning
*/
public class CloneTest
{
public static void main(String[] args)
{
try
{
Employee original = new Employee("John", 50000);
original.setHireDay(2000, 1, 1);
Employee copy = original.clone();
copy.raiseSalary(10);
copy.setHireDay(2012, 12, 31);
System.out.println("original = " + original);
System.out.println("copy = " + copy);
}
catch(CloneNotSupportedException e)
{
e.printStackTrace();
}
}
}
    clone/Employee.java如下所示:
package clone;

import java.util.Date;
import java.util.GregorianCalendar;

public class Employee implements Cloneable
{
private String name;
private double salary;
private Date hireDay;

public Employee(String n, double s)
{
name = n;
salary = s;
hireDay = new Date();
}
public Employee clone() throws CloneNotSupportedException
{
// call Object.clone()
Employee cloned = (Employee)super.clone();
// clone mutable fields
cloned.hireDay = (Date)hireDay.clone();

return cloned;
}

public void setHireDay(int year, int month, int day)
{
Date newHireDay = new GregorianCalendar(year, month - 1, day).getTime();
// Example of instance fields mutation
hireDay.setTime(newHireDay.getTime());
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
public String toString()
{
return "Employee[name = " + name + ", salary = " + salary + ", hireDay = " + hireDay + "]";
}
}
    运行结果如下所示:



6.3 接口与回调

    回调(callback)是一种常见的程序设计模式.在这种模式中,可以指出某个特定事件发生时应该采取的动作.

    在java.swing包中有一个Timer类,可以使用它在到达给定的时间间隔时发出通告.例如,假如程序中有一个时钟,就可以请求每秒钟获得一个通告,以便更新时钟的画面.

    如何告之定时器要做什么呢?在很多程序设计语言中,可以提供一个函数名,定时器周期地调用它.但是在Java标准类库中的类采用的是面相对象方法.它将某个类的对象传递给定时器,然后定时器调用这个方法,由于对象可以携带一些附加的信息,所以传递给一个对象比传递一个函数灵活地多.

    当然,定时器需要知道调用哪一个方法,并要求传递的对象所属的类实现了java.awt.event包的ActionListener接口.下面是这个接口.
public interface ActionListener
{
void actionPerformed(ActionEvent event);
}
    当到达指定的时间间隔时,定时器就调用actionPerformed方法.

    程序6-5给出了定时器和监听器的操作行为.

    timer/TimerTest.java如下所示:
package timer;

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.Timer;

public class TimerTest
{
public static void main(String[] args)
{
ActionListener listener = new TimePrinter();
Timer t = new Timer(1000, listener);
t.start();

JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
Date now = new Date();
System.out.println("At the tone, the time is " + now);
Toolkit.getDefaultToolkit().beep();
}
}
    运行结果如下所示:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: