Java核心技术 第六章 接口和内部类

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方法,以便实现克隆子对象的深拷贝。

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

  1. 默认的克隆方法是否满足要求
  2. 默认的clone方法是否能够通过调用可变子对象的clone得到修补
  3. 是否不应该使用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对象是否代表一个代理类。

时间: 2024-10-21 22:24:59

Java核心技术 第六章 接口和内部类的相关文章

Java核心技术第六章--接口

一.接口 1.1.接口概念 接口(英文:Interface),在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明.一个类通过继承接口的方式,从而来继承接口的抽象方法. 接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念.类描述对象的属性和方法.接口则包含类要实现的方法. 除非实现接口的类是抽象类,否则该类要定义接口中的所有方法. 接口无法被实例化,但是可以被实现.一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类.另外,在

[Java学习笔记] Java核心技术 卷1 第六章 接口与内部类

第6章 接口与内部类 6.1 接口 一个类可以实现一个或多个接口,并在需要接口的地方,随时使用实现了相应接口的对象. 在接口声明中,方法自动public,可以不写修饰符.在实现接口时必须把方法声明为public. 一个接口中可以包含多个方法,还可以定义常量,自动设置public static final 声明在接口中的内部类自动成为static和public类. 接口中不能含有实例域,也不能在接口中实现方法.提供实例域和方法实现的任务应该由实现接口的那个类来完成. 可以将接口看成是没有实例域的抽

Java核心技术(六) —— 接口

后面的博文,我们将开始Java的常用高级技术学习. 接口技术,主要用来描述类具有什么样的功能,而并不给出每个功能的具体实现.一个类可以实现(implement)一个或多个接口,并在需要接口的地方,随时使用实现了相应接口的对象. 本文,我们将从以下几个主要方面对接口进行深入学习 接口 对象克隆 接口与回调 此外还有经常用到的Comparable和Comparator接口. 1.接口 接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义. 下面我们来看下Comparable接口

“全栈2019”Java第九十六章:抽象局部内部类详解

难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第九十六章:抽象局部内部类详解 下一章 "全栈2019"Java第九十七章:在方法中访问局部内部类成员详解 学习小组 加入同步学习小组,共同交流与进步. 方式一:关注头条号Gorhaf,私信"Java学习小组". 方式二:关注公众号Gorhaf,回复"Java学

“全栈2019”Java第九十九章:局部内部类与继承详解

难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第九十九章:局部内部类与继承详解 下一章 "全栈2019"Java第一百章:局部内部类可以实现接口吗? 学习小组 加入同步学习小组,共同交流与进步. 方式一:关注头条号Gorhaf,私信"Java学习小组". 方式二:关注公众号Gorhaf,回复"Java学习小

“全栈2019”Java第十六章:下划线在数字中的意义

难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第十六章:下划线在数字中的意义 下一章 "全栈2019"Java第十七章:赋值运算符和算术运算符 学习小组 加入同步学习小组,共同交流与进步. 方式一:关注头条号Gorhaf,私信"Java学习小组". 方式二:关注公众号Gorhaf,回复"Java学习小组&q

Java核心技术 第五章 继承

类.超类.子类: 子类方法不能直接访问超类的私有域. this和super并非引用,不能将其赋给另一个对象变量. super在构造器中的应用: public Manager(String n, double s, int year, int month, int day ) { super(n, s, year, month, day) ; bonus = 0 ; } 使用super调用构造器的语句必须是子类构造器的第一条语句. 如果子类构造器没有显示的调用超类的构造器,则将自动的调用超类默认(

Java编程思想---第九章 接口(上)

第九章 接口 接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法. 9.1 抽象类和抽象方法 Java提供一个叫做抽象方法的机制,这个机制是不完整的,仅有声明而没有方法体,抽象方法的语法如下: abstract void f(); 包含抽象方法的类叫做抽象类,如果一个类包含一个或者多个抽象方法,该类必须被限定为抽象的,否则编译器就会报错. 如果一个抽象类不完整,那么当我们试图产生该类的对象时,由于抽象类创建对象时不安全的,所以我们会从编译器那里得到一条出错消息,这样编译器会确保抽象类

Java核心技术第三章----Java的基本程序设计结构重难点总结

最近在看Java核心技术这本书,这里对第三章个人认为的重难点做一个总结.方便以后回顾,个人能力有限,本人菜鸟,大神勿喷,请大家多多指教. 一.位运算符 指定 A = 66(0100 0010); B = 22 (0001 0110)(这里为了简化说明支取一个字节即8位来运算) 位运算符比一般的算术运算符速度要快,而且可以实现一些算术运算符不能实现的功能.如果要开发高效率程序,位运算符是必不可少的.位运算符用来对二进制位进行操作,包括:按位与(&).按位或(|).按位异或(^).按位取反(~).按