Java核心技术 第六章 接口与内部类
接口:
任何实现Comparable接口的类都需要包含compareTo方法,并且这个方法的参数必须是一个Object对象,返回一个整数数值。
在Java SE 5.0中,Comparable接口已经改进为泛型类型。
接口中所有的方法自动的属于public。因此,在接口中声明方法时,不必提供关键字public。
接口中决不能含有实例域,也不能在接口中实现方法。
要让一个类使用排序服务,必须让它实现compareTo方法,因此必须实现Comparable接口。
接口的特性:
不能使用new运算符实例化一个接口。
可以声明接口的变量:
Comparable x ;
X = new Employee() ;
可以使用instanceof检查一个对象是否属于某个特定接口。
If(anObject instanceof Comparable)
与可以建立类的继承关系一样,接口也可以被扩展。
public interface Moveable {
void move(double x, double y) ;
}
public interface Powered extends Moveable {
double milePerGallon() ;
}
虽然接口中不能包含实例域或静态方法,但却可以包含常量。
public interface Powered extends Moveable {
double milePerGallon() ;
double SPEED_LIMIT = 95 ;
}
与接口中的方法都自动的被设置为public一样,接口中的域将被自动设为public static final .
尽管每个类只能拥有一个超类,但却可以实现多个接口。使用逗号将实现的各个接口隔开。
对象克隆:
Employee original = new Employee(“John Public”, 5000) ;
Employee copy = original ;
copy.raiseSalary(10) ;
copy 和 original引用的是一个变量,改变copy将改变original。
如果创建一个对象的新的copy, 它的最初状态与original一样,但以后将可以各自改变各自的状态,那就需要使用clone方法。
Employee copy = original.clone() ;
copy.raiseSalary(10) ;
clone 方法是Object类的一个protected方法。所以只有Employee类才能克隆Employee对象。克隆是对对象中的所有的数据域进行克隆。若果所有的数据域属于数值或基本类型,这样拷贝域没有任何问题。但是,如果在对象中包含了子对象的引用,拷贝的结果会使得两个域引用同一个子对象,因此原始对象与克隆对象共享这部分信息。
进行浅拷贝,如果原始对象与克隆对象共享的子对象是不可变的,将不会产生任何问题。然而更常见的情况是子对象可变,因此这种情况下需要重新定义clone方法,以便实现克隆子对象的深拷贝。
对每一个类,都需要作出下列判断:
- 默认的克隆方法是否满足要求
- 默认的clone方法是否能够通过调用可变子对象的clone得到修补
- 是否不应该使用clone
实际上,选项3是默认的。如果要选择1或2,类必须:
1.实现Cloneable接口
2.使用public访问修饰符重新定义clone方法
在这里,Cloneable接口的出现于接口的正常使用没有任何关系。尤其是,它并没有指定clone这个方法,这个方法是从Object类继承而来的。接口在这里只是作为一个标记,表明类设计者知道要进行克隆处理。如果一个对象需要克隆,而没有实现Cloneable接口,就会产生一个已检查异常。
即使clone的默认实现(浅拷贝能够满足要求),也应该实现Cloneable接口,将clone重新定义为public,并调用super.clone()。 例:
class Employee implements Cloneable {
public Employee clone() throws CloneSupportedException {
return (Employee) super.clone() ;
}
}
上面实现的是浅拷贝,若想实现深拷贝,必须克隆所有可变的实例域。
下面是一个建立深拷贝clone方法的一个示例:
class Employee implements Cloneable {
...
public Employee clone() throws CloneNotSupportedException {
Employee cloned = (Employee) super.clone() ;
cloned.hireDay = (Date) hireDay.clone() ;
return cloned ;
}
}
只要在clone中含有没有实现Cloneable接口的对象,Object类的clone方法就会抛出一个CloneNotSupportedException 异常。当然Employee和Date类都实现了Cloneable 接口,因此不会抛出异常。但是编译器并不知道这些情况,因此需要声明异常:
public Employee clone() throws CloneNotSupportedException
一旦为Employee类定义了clone方法,任何人都可以利用它克隆Manager对象。Employee的克隆方法能否完成这项重任,取决于Manager类中包含哪些域。如果Manager中包含一些需要深拷贝的域或者包含一些没有实现Cloneable接口的域,无法保证拷贝正确。
在标准类库中,只有不到5%的类实现了clone。
接口与回调:
回调是一种常见的设计模式。在这种模式中,可以指出摸个特定事件发生时应该采取的动作。
内部类:
内部类是定义在另一个类中的类。
需要内部类的原因:
l 内部类方法可以访问该类定义所在的作用域中的数据,包括私有数据
l 内部类可以对同一个包中的其他类隐藏起来
l 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷。
使用内部类访问对象状态:
public class TalkingClock {
private int interval ;
private boolean beep ;
public TalkingClock(int interval, boolean beep) {...}
public void start() {...}
public class TimerPrinter implements ActionListener {
public void actionPerformed(ActionEvent event) {
Date now = new Date() ;
System.out.println(“At the tone, the time is” + now) ;
if (beep) Toolkit.getDefaultToolkit().beep() ;
}
}
}
需要注意,这里的TimerPrinter类位于TalkingClock类内部。
内部类的特殊语法规则:
使用外围类的表达式:
OuterClass.this
例:
public void actionPerformed(ActionEvent event) {
...
if (TalkingClock.this.beep) Toolkit.getDefaultToolkit().beep() ;
}
反过来,可以采用下列语法格式更加明确地编写内部对象的构造器:
outerObject.new InnerClass(construction parameters)
例如:
ActionListener listener = this.new TimePrinter() ;
通常this是多余的
TalkingClock jabberer = new TalkingClock(1000, true) ;
TalkingClock.TimePrinter listener = jabberer.new TimePrinter() ;
内部类是否有用、必要和安全:
内部类是一种编译器现象,与虚拟机无关。编译器将会把内部类翻译成$分隔外部类名与内部类名的常规文件,而虚拟机对此一无所知。
例如:在TalkingClock类内部的TimePrinter类将被翻译成类文件TalkingClock$TimePrinter.class.
编译器在外围类添加静态方法access$0.它将返回作为参数传递给它的对象域beep。(方法名取决于编译器)
if(beep) 相当于if(access$0(outer))
局部内部类:
TimePrinter这个类名只在start方法中创建这个类型的对象时使用了一次。当遇到这种情况时,可以在一个方法中定义局部类。
public void start() {
public class TimerPrinter implements ActionListener {
public void actionPerformed(ActionEvent event) {
Date now = new Date() ;
System.out.println(“At the tone, the time is” + now) ;
if (beep) Toolkit.getDefaultToolkit().beep() ;
}
}
ActionListener listener = new TimePrinter() ;
Timer t = new Timer(interval, listener) ;
t.start() ;
}
局部类不能用public或private访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。
局部类有一个优势,即对外部世界可以完全地隐藏起来。即使TalkingClock类中的其它代码也不能访问它。除了start方法外,没有任何方法知道TimePrinter的存在。
由外部方法访问final变量:
与其他内部类相比较,局部类还有一个优点。它们不仅能够访问包含它们的外部类,还可以访问局部变量。不过那些局部变量必须被声明为final。
例:将TalkingClock构造器的参数interval和beep移至start方法中。
public void start(int interval, final boolean beep) {
class TimePrinter implements ActionListener {
public void actionPerformed(ActionEvent event) {
Date now = new Date() ;
System.out.println(“At the tone, the time is” + now) ;
if (beep) Toolkit.getDefaultToolkit().beep() ;
}
}
ActionListener listener = new TimePrinter() ;
Timer t = new Timer(interval, listener) ;
t.start() ;
}
由于beep是局部变量,start方法结束,beep参数变量不复存在,为了能够让actionPerformed能够正常工作,TimerPrinter类在beep域释放之前将beep域用start方法的局部变量进行备份。final boolean val$beep ;该域放在TalkingClock¥TimePrinter中。
final关键字可以用于局部变量、实例变量和静态变量。在所有这些情况下,它们的含义都是:在创建这个变量后,只能够为之赋值一次。此后再也不能修改它的值了,这就是final。
不过在定义final变量的时候,不必进行初始化。
final int[] counter = new int[] ;
这样可以更新counter
匿名内部类:
将局部类的使用再深一步。加入只创建这个类的一个对象,就不必命名了。这种情况称为匿名内部类。
public void start(int interval, final boolean beep) {
ActionListener listener = new ActionListener()
{
public void actionPerformed(ActionEvent event) {
Date now = new Date() ;
System.out.println(“At the tone, the time is” + now) ;
if (beep) Toolkit.getDefaultToolkit().beep() ;
}
} ;
Timer t = new Timer(interval, listener) ;
t.start() ;
}
它的含义是:创建一个实现ActionListener接口的类的新对象,需要实现的方法actionPerformed定义在括号{}内。
通常的语法格式为:
new SuperType(construction parameters) {
inner class methods and data
}
其中,SuperType可以是ActionListener这样的接口,于是内部类就要实现这个接口。SuperType也可以是一个类,于是内部类就要扩展它。
由于构造器的名字必须与类名相同,而匿名类没有类名,所以,匿名类不能有构造器。取而代之的是将构造器参数传递给超类构造器。
得到类名:
System.out.println(“Something awful happened in” + getClass()) ;
不过对于静态方法无效。getClass()调用的是this.getClass(),静态方法没有this。
静态方法应使用下面的表达式:
new Object(){}.getClass().getEnclosingClass()
在这里,new Object(){}会建立Object的一个匿名子类的一个匿名对象,getEnclosingClass则得到其外围类,也就是包含这个静态方法的类。
静态内部类:
有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。为此,可以将内部类声明为static,以便取消产生的引用。
当然,只有内部类可以声明为static
静态内部类除的对象除了没有对生成它的外围对象的引用特权,与其他所有内部类完全一样。因为静态类生成在外部引用类形成之前。
外围类的静态方法中使用内部类可以使用外围类中定义的内部类,但给内部类必须是静态的。因为静态方法只能使用定义该静态方法的类的静态变量。
class ArrayAlg {
...
public static class Pair {
...
}
public static Pair minmax(double[] values) {
....
return new Pair(min, max) ;
}
}
代理:
代理是Java SE 1.3新增的特性。
利用代理可以在运行时创建一个实现了一组给定接口的新类。
代理类可以在运行时创建全新的类,这样的代理类能够实现指定的接口。尤其是,它具有下列方法:
l 指定接口所需要的全部方法
l Object类中的全部方法,例如,toString、equals等
然而,不能在运行时定义这些方法的新代码。而是要提供一个调用处理器。调用处理器是实现了InvocationHandler接口的对象。这个接口中只有一个方法:
Object invoke(Object proxy, Method method, Object[] args)
无论何时调用代理对象的方法,调用处理器的invoke方法都会被调用,并且向其传递Method对象和原始的调用参数。调用处理器必须给出处理器调用方式。
要想创建一个代理对象,需要使用Proxy类的newProxyInstance方法。这个方法有三个参数:
l 一个类加载器。目前,用null表示默认的类加载器
l 一个Class对象数组,每个元素都是需要实现的接口
l 一个调用处理器
使用代理的原因:
l 路由对远程服务器的方法调用
l 在程序运行期间,将用户接口事件与动作关联起来
l 为调试,跟踪方法调用
要点:
传入一组接口类对象,运行时才能确定接口的实现方法。这样可以直接使用接口,解决接口不能直接实例化的问题。
代理类的特性:
所有的代理类都扩展于Proxy类。一个代理类只有一个实例域——调用处理器,它定义在Proxy的超类中。为了履行代理对象的职责,所需要的任何附加数据都必须存储在调用处理器中。
所有的代理类都覆盖了Object类中的方法toString、equals和hashCode。如同所有的代理方法一样,这些方法仅仅调用了调用处理器的invoke。Object的其他方法没有被重新定义。
对于特定的类加载器和预设的一组接口来说,只能有一个代理类。也就是说,如果使用同一个类加载器和接口数组调用两次newProxyInstance方法的话,那么只能得到同一个类的两个对象,也可以利用getProxyClass方法获得这个类:
Class proxyClass = Proxy.getProxyClass(null, interface) ;
代理类一定是public和final。如果代理类实现的所有接口都是public,代理类就不属于某个特定的包;否则,所有非公有的接口都必须属于同一个包,同时,代理类也属于这个包。
可以通过调用isProxyClass方法检测一个特定的Class对象是否代表一个代理类。