Java学习day040 接口示例(接口与回调、Comparator接口、对象克隆)

使用的教材是java核心技术卷1,我将跟着这本书的章节同时配合视频资源来进行学习基础java知识。

day040   接口示例(接口与回调、Comparator接口、对象克隆)


1.接口与回调

回调(callback)是一种常见的程序设计模式。在这种模式中,可以指出某个特定事件发生时应该采取的动作。例如,可以指出在按下鼠标或选择某个菜单项时应该采取什么行动。

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

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

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

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

public interface ActionListener 
{
     void actionPerfonned(ActionEvent event); 
}

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

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

class TimePrinter implements ActionListener
{
    public void actionPerformed(ActionEvent event)
    {
        System.out.println ("At the tone,the time is"+newOate());
        Toolkit.getDefaultToolkit().beep();
    }
}

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

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

ActionListener listener = new TimePrinter();
Timer t = new Timer(10000,listener);

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

最后,启动定时器:

t.start();

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

At the tone,the time is Wed Apr13 23:29:08 PDT 2016

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

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

需要注意,这个程序除了导入javax.swing.*和java.util.*外,还通过类名导入了javax.swing.Timer。这就消除了javax.swing.Timer与java.util.Timer之间产生的二义性。这里的java.util.Timer是一个与本例无关的类,它主要用于调度后台任务。

/**
 *@author  zzehao
 */
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.Timer;
//to resolve conflict with java.util.Timer

public class TimerTest
{
	public static void main(String[] args)
	{
		ActionListener listener = new TimePrinter();

		//construct a timer that calls the listener
		//once every 10 seconds
		Timer t = new Timer(10000,listener);
		t.start();

		JOptionPane.showMessageDialog(null, "Quit program?"); 
		System.exit(0);
	}
}

class TimePrinter implements ActionListener
{
	public void actionPerformed(ActionEvent event)
	{
		System.out.println("At the tone, the time is " +new Date());
		Toolkit.getDefaultToolkit().beep();
	}	
}

运行的结果:

 


2.Comparator接口

之前,我们已经了解了如何对一个对象数组排序,前提是这些对象是实现了Comparable接口的类的实例,例如,可以对一个字符串数组排序,因为String类实现了Comparable<String>,而且String.compareTo方法可以按字典顺序比较字符串。

现在假设我们希望按长度递增的顺序对字符串进行排序,而不是按字典顺序进行排序。肯定不能让String类用两种不同的方式实现compareTo方法—更何况,String类也不应由我们来修改。

要处理这种情况,Arrays.sort方法还有第二个版本,有一个数组和一个比较器(comparator)作为参数,比较器是实现了Comparator接口的类的实例。

public interface Comparators<T>
{
    int compare(T first,T second);
}

要按长度比较字符串,可以如下定义一个实现Comparator的类:

class LengthComparator implements Comparator<String>
{
    public int compare(String first,String second)
    {
        return first.length()-second.length();
    }
}

具体完成比较时,需要建立一个实例:

Comparator<String> comp = new LengthComparator();
if(comp.compare(words[i],words[j])>0)...

将这个调用与words[i].compareTo(words[j])做比较。这个compare方法要在比较器对象上调用,而不是在字符串本身上调用。

尽管LengthComparator对象没有状态,不过还是需要建立这个对象的一个实例。我们需要这个实例来调用compare方法它不是一个静态方法。

要对一个数组排序,需要为Arrays.sort方法传人一个LengthComparator对象:

String[] friends={"Peter","Paul","Mary"};
Arrays.sort(friends,new LengthComparator());

现在这个数组可能是["Paul","Mary","Peter"]或["Mary","Paul","Peter"]。


3.对象克隆

Cloneable接口,这个接口指示一个类提供了一个安全的clone方法。

要了解克隆的具体含义,先来回忆为一个包含对象引用的变量建立副本时会发生什么。原变量和副本都是同一个对象的引用,如下图。这说明,任何一个变量改变都会影响另一个变量。

Employee original = new Employee("John Public",50000);
Employee copy = original;
copy.raiseSalary(l0);
//oops-also changed original

如果希望copy是一个新对象,它的初始状态与original相同,但是之后它们各自会有自己不同的状态,这种情况下就可以使用clone方法。

Employee copy = original.clone();
copy.raiseSalary(l0);//OK original unchanged

                                        

不过并没有这么简单。clone方法是Object的一个protected方法,这说明你的代码不能直接调用这个方法。只有Employee类可以克隆Employee对象。这个限制是有原因的。想想看Object类如何实现clone。它对于这个对象一无所知,所以只能逐个域地进行拷贝。如果对象中的所有数据域都是数值或其他基本类型,拷贝这些域没有任何问题、但是如果对象包含子对象的引用,拷贝域就会得到相同子对象的另一个引用,这样一来,原对象和克隆的对象仍然会共享一些信息。

为了更直观地说明这个问题,考虑介绍过的Employee类。下图显示了使用Object类的clone方法克隆这样一个Employee对象会发生什么。可以看到,默认的克隆操作是“浅拷贝”,并没有克隆对象中引用的其他对象。(这个图显示了一个共享的Date对象。出于某种原因(稍后就会解释这个原因),这个例子使用了Employee类的老版本,其中的雇用日期仍用Date表示。)

浅拷贝会有什么影响吗?这要看具体情况。如果原对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的。如果子对象属于一个不可变的类,如String,就是这种情况。或者在对象的生命期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况下同样是安全的。  

                          

不过,通常子对象都是可变的,必须重新定义clone方法来建立一个深拷贝,同时克隆所有子对象。在这个例子中,hireDay域是一个Date,这是可变的,所以它也需要克隆。(出于这个原因,这个例子使用Date类型的域而不是LocalDate来展示克隆过程。如果hireDay是不可变的LocalDate类的一个实例,就无需我们做任何处理了。)

对于每一个类,需要确定:

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

2)是否可以在可变的子对象上调用clone来修补默认的clone方法;

3)是否不该使用clone0

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

1)实现Cloneable接口;

2)重新定义clone方法,并指定public访问修饰符。

Object类中clone方法声明为protected,所以你的代码不能直接调用anObject.clone()。但是,不是所有子类都能访问受保护方法吗?不是所有类都是Object的子类吗?幸运的是,受保护访问的规则比较微妙(前面有介绍)。子类只能调用受保护的clone方法来克隆它自己的对象。必须重新定义clone为public才能允许所有方法克隆对象。

在这里,Cloneable接口的出现与接口的正常使用并没有关系。具体来说,它没有指定done方法,这个方法是从Object类继承的。这个接口只是作为一个标记,指示类设计者了解克隆过程。对象对于克隆很“偏执”,如果一个对象请求克隆,但没有实现这个接口,就会生成一个受査异常。

即使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();
    }
    ...
}

与Object.clone提供的浅拷贝相比,前面看到的clone方法并没有为它增加任何功能。这里只是让这个方法是公有的。要建立深拷贝,还需要做更多工作,克隆对象中可变的实例域。下面来看创建深拷贝的clone方法的一个例子:

class Employee implements Cloneable
{
    ...
    public Employee clone() throws CloneNotSupportedException 
    {
        //call Object.clone()
        Employee cloned = (Employee)super.clone();

        //clone mutable fields
        cloned.hireDay=(Date)hireDay.clone();

        return cloned;
    }
}

如果在一个对象上调用clone,但这个对象的类并没有实现Cloneable接口,Object类的clone方法就会拋出一个CloneNotSupportedExceptionD当然,Employee和Date类实现了Cloneable接口,所以不会抛出这个异常。不过,编译器并不了解这一点,因此,我们声明了这个异常:

public Employee clone() throws CloneNotSupportedException

捕获这个异常是不是更好一些?

public Employee clone()
{
    try 
        {
             Employee cloned = (Employee) super.clone();
             ...
        } 
    catch (CloneNotSupportedException e) { return null; } 
    //this won't happen, since we are Cloneable 
}

这非常适用于final类。否则,最好还是保留throws说明符。这样就允许子类在不支持克隆时选择抛出一个CloneNotSupportedException。

必须当心子类的克隆。例如,一旦为Employee类定义了clone方法,任何人都可以用它来克隆Manager对象。Employee克隆方法能完成工作吗?这取决于Manager类的域。在这里是没有问题的,因为bonus域是基本类型。但是Manager可能会有需要深拷贝或不可克隆的域。不能保证子类的实现者一定会修正clone方法让它正常工作。出于这个原因,在Object类中clone方法声明为protected。不过,如果你希望类用户调用clone,就不能这样做。

要不要在自己的类中实现clone呢?如果你的客户需要建立深拷贝,可能就需要实现这个方法。有些人认为应该完全避免使用clone,而实现另一个方法来达到同样的目的。clone相当笨拙,这一点我们也同意,不过如果让另一个方法来完成这个工作,还是会遇到同样的问题。毕竟,克隆没有你想象中那么常用。标准库中只有不到5%的类实现了clone。

下面程序1中的程序克隆了Employee类(程序2)的一个实例,然后调用两个更改器方法。raiseSalary方法会改变salary域的值,而setHireDay方法改变hireDay域的状态。这两个更改器方法都不会影响原来的对象,因为clone定义为建立一个深拷贝。

程序1:

/**
 *@author  zzehao
 */

public class CloneTest
{
	public static void main(String[] args)
	{
		try
		{
			Employee original = new Employee("]ohnQ. Public", 50000);
			original.setHireDay(2000, 1, 1 ); 
			Employee copy = original.clone(); 
			copy.raiseSalary(10); 
			copy.setHireDay(2002, 12, 31 ); 
			System.out.println("original=" +original); 
			System.out.println("copy=" +copy);
		}
		catch (CloneNotSupportedException e)
		{
			e.printStackTrace();
		}
	}
}

程序2:

/**
 *@author  zzehao
 */
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 name,double salary)
	{
		this.name = name;
		this.salary = salary;
		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 field 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+"]";
	}
}

运行的结果:


 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值