第6章 接口与内部类
接口技术主要用来描述类具有什么功能,而不给出每个功能的具体实现。
__6_1 接口
在Java中,接口不是类,而是对类的一组需求描述,如,要使用Arrays类的sort方法,需要使对象所属的类实现Comparable接口。
下面使Comparable接口的代码
public interface Comparable
{
int compareTo(Object other);
}
任何实现Comparable接口的类都需要包含compareTo方法,且其签名与Comparable接口的定义一致。
在Java SE 5.0中Compoarable接口已经改进为泛型类型。
public interface Comparable<T>
{
int compareTo(T other);
}
例如,在实现Comparable < Employee > 接口的类中,必须提供下列方法
int compareTo(Employee other)
接口中的所有方法自动地属于public。因此,在接口中声明方法时,不必提供关键字public。但在实现接口时,必须把方法声明为public。
接口中还可以定义常量。但接口绝不能含有实例域,也不能在接口中实现方法。提供实例域和方法实现的任务应该由实现接口的那个类来完成。
要将类声明为实现某个借口,需要使用关键字implements:
class Employee implements Comparable
- 关于Comparable接口:
实现Comparable接口时,若比较两个整数值大小,常使用相减的方式,但不适用于浮点数,因不相同的两数值相减后可能四舍五入为0,也需要避免减法运算的溢出。
对任意的x和y,需保证sgn(x.compareTo(y)) = -sgn(y.compareTo(x))。和equals方法一样,在继承过程中有可能会出现问题,因为Manager扩展了Employee,而Employee实现的使Comparable< Employee>。如果Manager覆盖了compareTo,就必须要有经理与雇员比较的思想准备,而非仅仅把雇员转换成经理。
与equals方法类似的修改方法,如果子类之间的含义不一样,那就属于不同类的非法比较,应在每个compareTo方法开始时进行检测:
if(getClass()) != other.getClass()) throw new ClassCastException();
如果存在一个通用算法,它能够对两个不同子类的对象进行比较,则应该在超类中提供一个compareTo方法,并将这个方法声明为final。
- 1)接口的特性
接口不是类,尤其不能使用new运算符实例化一个接口,不能构造接口的对象,却能声明借口的变量,接口变量必须引用实现了借口的类对象。可以使用instance检查一个对象是否实现了特定的借口。
x = new Comparable(...); //ERROR
Comparable x; //OK
x = new Employee(...); //OK provided Employee implements Comparable
if( anObject instanceOf Comparable) {...}
接口可以被扩展。虽然接口中不能包含实例域和静态方法,但可以包含常量。接口中的域将自动被设为public static final。
public interface Moveable
{
void move(double x, double y);
}
public interface Powered extends Moveable
{
double milesPerGallon();
double SPEED_LIMIT = 95; // a public static final constant
}
尽管每个类只能够拥有一个超类,却可以实现多个接口。
class Employee implements Cloneable, Comparable
- 2)接口与抽象类
Java语言利用接口机制来实现多继承的大部分功能。
__6_2 对象克隆
Object类的克隆方法是对各个域进行对应的拷贝。如果对象中包含了对子对象的引用,拷贝的结果会使这两个域共同引用同一个子对象。Object类 中,clone方法被声明为protected,因此无法直接调用anObject.clone(),只可以在anObject所属类内部调用clone方法。
Cloneable接口的出现与接口的正常使用没有任何关系,它没有指定clone方法,这个方法使从Object类中继承而来的。接口在这里只是作为一个标记,表明类设计者知道要进行克隆处理。通常使用接口的目的是为了确保类实现某些特定的方法,而标记接口没有方法,使用它的唯一目的是可以用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();
}
}
必须谨慎地实现子类的克隆,因为一旦定义了Employee类的clone方法,任何人都可以利用它克隆Manager对象。
所有的数组类型均包含一个clone方法,这个方法被设为public,而不是protected,可以利用这个方法创建一个包含所有数组元素拷贝的一个新数组。
int[] luckyNumbers = {2, 3, 4, 5, 7, 11, 13};
int[] cloned = (int[]) luckNumbers.clone();
cloned[5] = 12; //doesn't change luckyNumbers[5]
__6_3 接口与回调
java.swing包中有Timer类,可以在到达给定的时间间隔时发出通告。
实例程序(引用书中):
import java.awt.Toolkit;
import java.awt.event.*;
import java.util.*;
import javax.swing.Timer;
public class TimerTest
{
public static void main(String[] args)
{
ActionListener listener = new TimePrinter();//实现接口的类的实例赋值给接口变量
Timer aTimer = new Timer(100, listener);
aTimer.start();
JOptionPane.showMessageDialog(null, "Quit program?");//对话框将位于第一参数的组件中央,第一参数为null时位于屏幕中央
System.exit(0);
}
}
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
Date now = new Date();
System.out.println("current time is " + now);
Toolkit.getDefaultToolkit().beep(); //ubuntu下没有响声,没有查找原因
}
}
__6_4 内部类
内部类可以对同一个包中的其他类隐藏起来。
- 1)使用内部类访问对象状态
内部类既可以访问自身的数据域,也可以访问创建他的外围类对象的数据域。
如
public class TalkingClock
{
public TalkingClock(int interval, boolean beep{...}
public void start(){...}
private int interval;
private boolean beep;
public class TimerPrinter implements ActionListener
{
...
}
}
因为TimePrinter类没有定义构造器,所以编译器为这个类生成了一个默认的构造器,其代码如下所示:
public TimePrinter(TalkingClock clock)
{
outer = clock;
}
创建了TimePrinter对象后,编译器就会将this引用传递给当前语音时钟的构造器:
ActionListener listener = new TimePrinter(this);
- 2)内部类的特殊语法规则
使用OuterClass.this
引用外围类。使用outerObject.new InnerClass()
构造内部类的对象。在外围类的作用域之外,使用OuterClass.InnerClass
引用内部类。
class TimerTest
{
void fun()
{
TimeWorker.TimePrinter aTimePrinter = aTimeWorker.new TimePrinter();
}
}
class TimeWorker
{
public class TimePrinter
{
}
}
3)内部类是否有用,必要和安全
编译器会把内部类翻译成用$分割外部类名和内部类名的常规类文件,而虚拟机对此一无所知。
javap是jdk自带的一个工具,可以反编译,也可以查看java编译器生成的字节码,可以使用javap -private ClassName
查看类的信息。(UINX下要使用\$对$符转义)。
可以看到编译器为了引用外围类,生成了一个附加的实例域this$0,也可以看到TimePrinter构造器的参数。为了让TimeWorker$TimePrinter能够访问beep变量,在外围类添加了静态方法access$000。这意味着任何同一个包内的任何一个类都可以调用这个方法读取到私有域beep。虽然access$000不是合法的Java方法名,但是熟悉类文件结构的黑客可以使用十六进制编辑器轻松地创建一个用虚拟机指令调用那个方法的类文件。4)局部内部类
public void start()
{
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
Date now = new Date();
System.out.println("time is " + new);
if(beep)
Toolkit.getDefaultToolkit().beep();
}
}
ActionListener listener = new TimePrinter();
Timer t = new Timer(interval, listener);
t.start();
}
局部类不能用public 或private访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。局部类可以对外部世界完全隐藏起来,除了声明它的块,没有方法知道它的存在。
- 5)由外部方法访问final变量
局部类可以访问局部变量,但是这些局部变量必须被声明为final。
当创建一个对象时,编译器必须检测对局部变量的访问,为每一个局部变量创建相应的数据域,并将局部变量拷贝到构造器中,以便将这些数据域初始化为局部变量的副本。为了使得局部变量与其拷贝保持一致,局部变量设置为final,但可以使用一个长度为1的数组使得可以在内部类修改这个数据。
final int[] counter = new int[1];
for(int i=0; i<dates.length; i++)
dates[i] = new Date()
{
public int compareTo(Date other)
{
counter[0]++;
return super.compareTo(other);
}
};
在定义final变量时不必进行初始化,定义时没有初始化的final变量被称为空final(blank final)变量。
- 6)匿名内部类
匿名内部类的语法格式为
new SuperType(construction parameters)
{
inner class methods and data
}
其中SuperClass可以接口,则创建一个实现该接口的新对象,此时不能有任何构造器参数;SuperClass为类时,则创建一个扩展该类的新对象,由于构造器名与类名相同,匿名内部类不能有构造器,而将参数传递给超类的构造器。
如果构造参数的闭圆括号跟一个开花括号,则定义的是匿名内部类。
- 7)静态内部类
有时,使用内部类使为了把一个类隐藏在另一个类的内部,并不内部类引用外围类对象。为此可以将内部类声明为static,以取消产生的引用。
只有内部类可以声明为static。当在静态方法中构造内部类对象时,也必须使用静态内部类。
声明在接口中的内部类自动称为static和public。
示例程序来自Java中的static class-陈哈哈-博客园
/* 下面程序演示如何在java中创建静态内部类和非静态内部类 */
class OuterClass{
private static String msg = "GeeksForGeeks";
// 静态内部类
public static class NestedStaticClass{
// 静态内部类只能访问外部类的静态成员
public void printMessage() {
// 试着将msg改成非静态的,这将导致编译错误
System.out.println("Message from nested static class: " + msg);
}
}
// 非静态内部类
public class InnerClass{
// 不管是静态方法还是非静态方法都可以在非静态内部类中访问
public void display(){
System.out.println("Message from non-static nested class: "+ msg);
}
}
}
class Main
{
// 怎么创建静态内部类和非静态内部类的实例
public static void main(String args[]){
// 创建静态内部类的实例
OuterClass.NestedStaticClass printer = new OuterClass.NestedStaticClass();
// 创建静态内部类的非静态方法
printer.printMessage();
// 为了创建非静态内部类,我们需要外部类的实例
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
// 调用非静态内部类的非静态方法
inner.display();
// 我们也可以结合以上步骤,一步创建的内部类实例
OuterClass.InnerClass innerObject = new OuterClass().new InnerClass();
// 同样我们现在可以调用内部类方法
innerObject.display();
}
}
__6_5 代理
利用代理可以在运行时创建一个实现了一组给定接口的新类。带离磊具有下列方法:
1)指定借口所需要的全部方法
2)Object类中的全部方法,例如toString,equals等
不能在运行时定义这些方法的新代码,而是要提供一个调用处理器(invocation handler)。调用处理器是实现了InvocationHandler接口的类对象。在这个接口中只有一个方法:
Object invoke(Object proxy, Method method, Object[]args)
无论何时调用代理对象的方法,调用处理器的invoke方法就会被调用,并向其传递Method对象和原始的调用参数。
要创建一个代理对象,需要使用Proxy类的newProxyInstance方法。这个方法有三个参数:
1)一个类加载器(class loader),用null表示使用默认的类加载器
2)一个Class对象数组,每个元素都是需要实现的接口
3)一个调用处理器
一个代理类只有一个实例域——调用处理器,它定义在Proxy的超类中。为了履行代理对象的职责,所需要的任何附加数据都必须存储在调用处理器中。
所有代理类都覆盖了Object类中的方法toString,equals和hashCode。如同所有的代理方法一样,这些方法仅仅调用了调用处理器的invoke。Object类中的其他方法(如clone和getClass)没有被重新定义。
对于特定的类加载器和预设的一组接口来说,只能有一个代理类。也就是说,如果使用同一个类加载器和接口数组调用两次newProxyInstance方法的话,那么只能够得到同一个类的两个对像,也可以利用getProxyClass方法获得这个类:
Class proxyClass = Proxy.getProxyClass(null, interfaces);
如下为书中示例程序,在二分查找过程中使用代理对象进行跟踪。
import java.lang.reflect.*;
import java.util.*;
/**
* This program demonstrates the use of proxies.
* @version 1.00 2000-04-13
* @author Cay Horstmann
*/
public class ProxyTest
{
public static void main(String[] args)
{
Object[] elements = new Object[1000];
// fill elements with proxies for the integers 1 ... 1000
for (int i = 0; i < elements.length; i++)
{
Integer value = i + 1;
InvocationHandler handler = new TraceHandler(value);
Object proxy = Proxy.newProxyInstance(null, new Class[] { Comparable.class } , handler);
elements[i] = proxy;
}
// construct a random integer
Integer key = new Random().nextInt(elements.length) + 1;
// search for the key
int result = Arrays.binarySearch(elements, key);
// print match if found
if (result >= 0) System.out.println(elements[result]);
}
}
/**
* An invocation handler that prints out the method name and parameters, then
* invokes the original method
*/
class TraceHandler implements InvocationHandler
{
/**
* Constructs a TraceHandler
* @param t the implicit parameter of the method call
*/
public TraceHandler(Object t)
{
target = t;
}
public Object invoke(Object proxy, Method m, Object[] args) throws Throwable
{
// print implicit argument
System.out.print(target);
// print method name
System.out.print("." + m.getName() + "(");
// print explicit arguments
if (args != null)
{
for (int i = 0; i < args.length; i++)
{
System.out.print(args[i]);
if (i < args.length - 1) System.out.print(", ");
}
}
System.out.println(")");
// invoke actual method
return m.invoke(target, args);
}
private Object target;
}
代理类一定是public和final。如果代理类实现的所有接口都是public,代理类就不属于某个特定的包;否则,所有非公有接口必须属于同一个包,同时,代理类也属于这个包。可以通过调用Proxy类中的isProxyClass方法检测一个特定的Class对象是否代表一个代理类。