5. 接口与内部类
- 接口(interface):描述类具有什么功能,而并不给出每个功能的具体实现。一个类可以实现(implement)一个或多个接口,并在需要接口的地方,随时使用实现了相应接口的对象。
- 克隆(深拷贝):创建一个新对象,且新对象的状态与原始对象的状态相同。当对克隆的新对象进行修改时,不会影响原始对象的状态。
- 内部类(inner class):内部类定义在另一个类的内部,其中的方法可以访问包含它们的外部类的域。
- 代理(proxy):一种实现任意接口的对象。是一种非常专业的构造工具,可以用来构建系统级的工具。
5.1. 接口
- 接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。如,Arrays类中的sort方法承诺可以对对象数组进行排休,但要求对象所属的类必须实现了Comparable接口。
- 接口中所有的方法自动地属于public。因此在接口中声明方法时,不必提供关键字public。但在实现接口时,必须把方法声明为public。
- 接口绝不能含有实例域,也不能在接口中实现方法。提供实例域和方法实现的任务应该由实现接口的那个类来完成。因此,接口可以看做没有实例域的抽象类。
- 让类实现一个接口:1.将类声明为实现给定的接口(使用关键字implements)。 2.对接口中的所有方法进行定义。
接口的特性
- 接口不是类,尤其不能使用new运算符实例化一个接口。
- 不能构造接口的对象。但能声明接口的变量,接口变量必须引用实现了接口的类对象。
Comparable x=new Employee(...);
- 可以使用instanceof检查一个对象是否实现了某个特定的接口:
if(anObject instanceof Comparable)
- 接口可以被扩展。
public interface Name extends Comparable
- 接口中不能包含实例域或静态方法,但可以包含常量(自动被设为public static final)
- 类可以实现多个接口
class Employee implements Comparable,Clonealbe
(实现Cloneable接口,Object类中的clone方法就可以创建类对象的一个拷贝)
接口与抽象类
Java中不直接将接口设计为抽象类的原因:
- 只能继承于一个类
- 有些语言中允许一个类有多个超类,即多继承(multiple inheritance)。但语言会变得复杂和低效。
- 使用接口可以提供多继承的大多数好处,同时避免多继承的复杂性和低效性。
5.2. 对象克隆
- 拷贝一个变量时,原始变量与拷贝变量引用同一个对象。改变一个变量所引用的对象将会对另一个变量产生影响。
- 如果创建一个对象新的copy,但以后将可以各自改变各自的状态,需要使用clone方法。
- 使用clone方法默认是浅拷贝,即并没有克隆包含在对象中的内部对象(如Employee对象中的hireDay这个Date对象)。
- 即使clone的默认实现(浅拷贝)能够满足需求,也应该实现Cloneable接口,将clone重定义为public,并调用super.clone()。
- 实现深拷贝必须:1.实现Cloneable接口。 2.使用public访问修饰符重新定义clone方法。
class Employee implements Cloneable
{
//如果clone中含没有实现Cloneable接口的对象,就会抛出异常
public Employee clone() throws CloneNotSupportedException
{
//call Object.clone()
Employee cloned=(Employee)super.clone();
//call mutable fields
cloned.hireDay=(Date)hireDay.clone();
return cloned;
}
}
5.3. 接口与回调
- 回调(callback)是一种常见的程序设计模式。在这种模式中,可以指出某个特定事件发生时应该采取的动作。如java.swing包中有一个Timer类,可以使用它在到达给定的时间间隔时发出通告。
//程序启动后,会立即显示一个包含“Quit program?”字样的对话框,10秒钟之后,第一条定时器消息才会显示出来
public class TimerTest {
public static void main(String[] args) {
ActionListener listener= new TimePrinter();
//构造一个定时器,每隔10秒通告listener一次
Timer t=new Timer(10000, listener);
//启动定时器。启动成功后,定时器将调用监听器的actionPerformed。
t.start();
//显示一个包含一条消息和OK按钮的对话框。
JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
class TimePrinter implements ActionListener{
@Override
public void actionPerformed(ActionEvent event) {
Date now=new Date();
System.out.println("At the tone,the time is "+ now);
//获得默认的工具箱并发出一声铃响
Toolkit.getDefaultToolkit().beep();
}
}
5.4. 内部类
内部类(inner class)是定义在另一个类中的类。
使用内部类的原因:
- 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
- 内部类可以对同一个包中的其他类隐藏起来。
- 当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous)内部类比较便捷。
使用内部类访问对象状态
class TalkingClock{
private int interval;
private boolean beep;
...
//TalkingClock的内部类。但不意味着每个TalkingClock都有一个TimePrinter实例域
public class TimePrinter implements ActionListener{
public void actionPerformed(ActionEvent e) {
Date now=new Date();
System.out.println("At the tone,the time is "+now);
//内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域
if (beep) {
Toolkit.getDefaultToolkit().beep();
}
}
}
}
内部类的特殊语法规则
- 外围内部类的引用:OuterClass.this 如
if(TalkingClock.this.beep)
- 构造内部对象:outerObject.new InnerClass(constructing parameters) 如
ActionListener listener=this.new TimePrinter();
- 在外围类的作用域之外,引用内部类:OuterClass.InnerClass
内部类是否有用、必要和安全
- 内部类是一种编译器现象,与虚拟机无关。编译器会把内部类翻译成用美元符号分隔外部类名与内部类名的常规类文件。例如,TalkingClock类内部的TimePrinter类将被翻译成类文件TalkingClock$TimePrinter.class。
- 内部类拥有访问特权,与常规类比较起来功能更加强大。
- 如果内部类访问了私有数据域,就有可能通过附加在外围类所在包中的其他类访问它们,但做这些事需要高超的技巧和极大的决心。程序员不可能无意之中就获得对类的访问权限。
局部内部类
- 定义在方法中
- 局部类不能用public或private访问修饰符声明。它的作用域被限定在声明这个局部类的块中。
- 局部类对外部世界可以完全地隐藏起来。即使TalkingClock类中的其他代码也不能访问它。除start方法外,没有任何方法指导TimePrinter类的存在。
public void start()
{
class TimePrinter implements ActionListener
{
...
}
...
}
由外部方法访问final变量
- 局部类不仅能够访问包含它们的外部类,还可以访问局部变量。但那些局部变量必须被声明为final。
public void start(int interval,final boolean beep)
{
class TimePrinter implements ActionListener
{
...
if(beep)...
}
}
匿名内部类
- 将局部内部类的使用再深入一步。假如只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类(anonymous inner class)。
- 创建一个实现ActionListener接口的类的新对象,需要实现的方法actionPerformed定义在括号内。通常的语法格式:
new superType(construction parameters){ inner class methods and data }
其中,superType可以是接口,也可以是类。 - 因为构造器的名字必须与类名相同,而匿名类没有类名,所以,匿名类不能有构造器。取而代之的是,将构造器参数传递给超类构造器。尤其是在内部类实现接口的时候,不能有任何构造参数。
技巧:双括号初始化(double brace initialization)
加入想构造一个数组列表,并将它传递到一个方法:
invite(new ArrayList<String>() {{ add("Harry");add("Tony"); }} )
外层括号建立了ArrayList的一个匿名子类。内层括号则是一个对象构造块。
匿名子类使用equals方法时候要特别注意。if(getClass() !=other.getClass()) return false;
匿名子类做这个测试会失败。
静态内部类
- 有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要引用外围类对象。对此,可以将内部类声明为static,以便取消产生的引用。
- 另外,静态方法中构造的内部类对象必须使用静态内部类。
- 声明在接口中的内部类自动成为static和public类。
5.5. 代理(部分)
利用代理可以在运行时创建一个实现了一组给定接口的新类。这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用。
假设有一个表示接口的class对象,它的确切类型在编译时无法知道。要想构造一个实现这些接口的类,就需要使用newInstance方法或反射找出这个类的构造器。但是,不能实例化一个接口,需要在程序处于运行状态时定义一个新类。代理机制能够解决这个问题。