您的位置 首页 java

一文带你深入Java核心技术:对象克隆+接口与回调,还有这种操作

对象克隆

当拷贝一个变量时,原始变量与拷贝变量引用同一个对象,如图6-1所示。这就是说,改变一个变量所引用的对象将会对另一个变量产生影响。

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类的一个proteced方法,也就是说,在用户编写的代码中不能直接调用它。只有Employee类才能够克隆Employee对象。这种限制有一定的道理。让我们查看一下Object类实现的clone方法。

由于这个类对具体的类对象一无所知,所以只能将各个域进行对应的拷贝。如果对象中的所有数据域都属于数值或基本类型,这样拷贝域没有任何问题。但是,如果在对象中包含了子对象的引用,那么拷贝的结果会使得两个域引用同一个子对象,因此原始对象与克隆对象共享这部分信息。

为了能够说明这种现象,请再看一下第4章中介绍的Employee类。图6-2显示了使用Object类的clone方法克隆Employee对象的结果。可以看到,默认的克隆操作是“浅拷贝”,它并没有克隆包含在对象中的内部对象。

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

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

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

1)默认的clone方法是否满足要求。

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

3)是否不应该使用clone。

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

1)实现Cloneable接口。

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

注意 :在Object类中,clone方法被声明为 protected ,因此无法直接调用anObject.clone( )。但是,不是所有子类都可以访问受保护的方法吗?不是每个类都是Object的子类吗?值得庆幸的是,受保护访问的规则更为微妙(参阅第5章)。子类只能调用受保护的clone方法克隆它自己。为此,必须重新定义clone方法,并将它声明为public,这样才能够让所有的方法克隆对象。

在这里,Cloneable接口的出现与接口的正常使用没有任何关系。尤其是,它并没有指定clone方法,这个方法是从Object类继承而来的。接口在这里只是作为一个标记,表明类设计者知道要进行克隆处理。

如果一个对象需要克隆,而没有实现Cloneable接口,就会产生一个已检验异常(checked exception)。

注意 :Cloneable接口是 java 提供的几个标记接口(tagging interface)之一。(有些程序员将它们称为标记接口(marker interface)。)我们知道,通常使用接口的目的是为了确保类实现某个特定的方法或一组特定的方法,Comparable接口就是这样一个例子。而标记接口没有方法,使用它的唯一目的是可以用instanceof进行类型检查:if (obj instanceof Cloneable) . . .

建议在自己编写程序时,不要使用这种技术。

即使clone的默认实现(浅拷贝)能够满足需求,也应该实现Cloneable接口,将clone重定义为public,并调用super.clone( )。下面是一个例子:

注意 :在JDK 5.0以前的版本中,clone方法总是返回Object类型,而在JDK 5.0中,允许克隆方法指定返回类型。

刚才看到的clone方法并没有在Object.clone提供的浅拷贝基础上增加任何新功能,而只是将这个方法声明为public。为了实现深拷贝,必须克隆所有可变的实例域。

下面是一个建立深拷贝clone方法的一个例子:

只要在clone中含有没有实现Cloneable接口的对象,Object类的clone方法就会抛出一个CloneNotSupportException异常。当然,Employee和Date类都实现了Cloneable接口,因此不会抛出异常。但是编译器并不知道这些情况,因此需要声明异常:

如果将上面这种形式替换成捕获异常呢?

这种写法比较适用于final类,否则最好还是在这个地方保留throws说明符。如果不支持克隆,子类具有抛出CloneNotSupportException异常的选择权。

必须谨慎地实现子类的克隆。例如,一旦为Employee类定义了clone方法,任何人都可以利用它克隆Manager对象。Employee的克隆方法能够完成这项重任吗?答案是,这将取决于Manager类中包含哪些域。在前面列举的例子中,由于bonus域属于基本类型,所以不会出现任何问题。但是,在Manager类中有可能存在一些需要深拷贝的域,或者包含一些没有实现Cloneable接口的域。没有人能够保证子类实现的clone一定正确。鉴于这个原因,应该将Object类中的clone方法声明为protected。但是,如果想让用户调用clone方法,就不能这样做。

在自己定义的类中应该实现clone方法吗?如果客户需要深拷贝就应该实现它。有些人认为应该全面地避免使用clone,并通过实现其他的方法达到此目的。我们同意这种观点,clone的确显得有点笨拙,但改用其他方法实现这项操作也会遇到同样的问题。至少,克隆的应用并不像人们想象的那样普遍。在标准类库中,只有不到5%的类实现了clone。

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

注意:将在第12章中介绍另一种克隆对象的机制,其中使用了Java的序列化功能。这种机制很容易实现并且也很安全,但效率较低。

例6-2 CloneTest.java

接口与 回调

回调(callback)是一种常见的程序设计模式。在这种模式中,可以指出某个特定事件发生时应该采取的动作。例如,可以指出在按下鼠标或选择某个菜单项时应该采取什么行动。然而,由于至此还没有介绍如何实现用户接口,所以只能讨论一些与上述操作类似,但比较简单的例子。

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

在构造定时器时,需要设置一个时间间隔,并告知定时器,当到达时间间隔时需要做些什么操作。

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

当然,定时器需要知道调用哪一个方法,并要求传递的对象所属的类实现了java.awt.event包的ActionListener接口。下面是这个接口:

当到达指定的时间间隔时,定时器就调用actionPerformed方法。

假设希望每隔10秒种打印一条信息“At the tone, the time is . . .”,然后响一声(beep),就应该定义一个实现ActionListener接口的类,然后将需要执行的语句放在actionPerformed方法中。

需要注意actionPerformed方法的ActionEvent参数。这个参数提供了事件的相关信息,例如,产生这个事件的源对象。有关这方面的详细内容请参阅第8章。在这个程序中,事件的信息并不重要,因此,可以放心地忽略这个参数。

接下来,构造这个类的一个对象,并将它传递给Timer 构造器

ActionListener listener = new TimePrinter( );

Timer t = new Timer(10000, listener);

Timer构造器的第一个参数是发出通告的时间间隔,它的单位是毫秒。我们希望每隔10秒通告一次。第二个参数是监听器对象。

最后,启动定时器:

t.start( );

每隔10秒钟,下列信息显示一次,然后响一声铃。

At the tone, the time is Thu Apr 13 23:29:08 PDT 2000

在例6-3中,给出了定时器和监听器的操作行为。在定时器启动以后,程序将弹出一个消息对话框,并等待用户点击Ok按钮来终止程序的执行。在程序等待用户操作的同时,每隔10秒显示一次当前的时间。

运行这个程序时要有一些耐心。程序启动后,将会立即显示一个包含“Quit program?”字样的对话框,10秒钟之后,第1条定时器消息才会显示出来。

需要注意,这个程序除了导入javax.swing.*和java.util.*外,还通过类名导入了javax.swing.Timer。

这就消除了javax.swing.Timer与java.util.Timer之间产生的二义性。这里的java.util.Timer是一个与本例无关的类,它主要用于调度后台任务。

例6-3 TimerTest.java

javax.swing.JOptionPane 1.2

• static void showMessageDialog(Component parent,Object message)

显示一个包含一条消息和OK按钮的对话框。这个对话框将位于parent组件的中央。如果

parent为null,对话框将显示在屏幕的中央。

javax.swing.Timer 1.2

• Timer(int interval, ActionListener listener)

构造一个定时器,每隔interval毫秒通告listener一次。

• void start( )

启动定时器。一旦启动成功,定时器将调用监听器的actionPerformed。

• void stop( )

终止定时器。一旦终止成功,定时器将不再调用监听器的actionPerformed。

javax.awt.Toolkit 1.0

• static Toolkit getDefaultToolkit( )

获得默认的工具箱。工具箱包含有关GUI环境的信息。

• void beep( )

发出一声铃响。

觉得文章不错的话,可以转发此文关注小编!!!

每天好技术,大家乐学习!!!

文章来源:智云一二三科技

文章标题:一文带你深入Java核心技术:对象克隆+接口与回调,还有这种操作

文章地址:https://www.zhihuclub.com/176218.shtml

关于作者: 智云科技

热门文章

网站地图