内部类

内部类(inner class) 是定义在另一个类中的类。为什么需要使用内部类呢? 其主要原因有以下三点:

•内部类方法可以访问该类定义所在的作用域中的数据, 包括私有的数据。

•内部类可以对同一个包中的其他类隐藏起来。

•当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous) 内部类比较 便捷。

使用内部类访问对象状态

语法比较复杂。鉴于此情况, 我们选择一个简单但不太实用的例子说明内部类 的使用方式。下面将进一步分析 TimerTest 示例, 并抽象出一个 TalkingClock 类。构造一个 语音时钟时需要提供两个参数:发布通告的间隔和开关铃声的标志。

public class TalkingClock {
​
private int interval: 
​
private boolean beep; 
​
public TalkingClock(int interval, boolean beep) { . . . } 
​
public void start() {... } 
​
public class TimePrinter implements ActionListener // an inner class {
​
 } 
​
. . .
​
}

 

需要注意, 这里的 TimePrinter 类位于 TalkingClock类内部。这并不意味着每个 TalkingClock 都有一个 TimePrinter 实例域 , 如前所示,TimePrinter对象是由 TalkingClock类的方法构造。

下面是 TimePrinter 类的详细内容。需要注意一点,actionPerformed方法在发出铃声之前 检查了 beep标志

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

 

令人惊讶的事情发生了。

内部类既可以访问自身的数据域,也 可以访问创建它的外围类对象的数据域。

程序清单 6-7给出了一个测试内部类的完整程序。下面我们再看一下访问控制。如果有 一个 TimePrinter 类是一个常规类,它就需要通过 TalkingClock 类的公有方法访问 beep标志, 而使用内部类可以给予改进, 即不必提供仅用于访问其他类的访问器。

[注]  TimePrinter 类声明为私有的。这样一来, 只有 TalkingClock 的方法才能够构造 TimePrinter 对象。只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性。
//程序清单6-7 innerClass/InnerClassTest.java
package innerClass;
​
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.Timer;
​
/**
 * This program demonstrates the use of inner classes.
 * @version 1.10 2004-02-27
 * @author Cay Horstmann
 */
public class InnerClassTest
{
   public static void main(String[] args)
   {
      TalkingClock clock = new TalkingClock(1000, true);
      clock.start();
​
      // keep program running until user selects "Ok"
      JOptionPane.showMessageDialog(null, "Quit program?");
      System.exit(0);
   }
}
​
/**
 * A clock that prints the time in regular intervals.
 */
class TalkingClock
{
   private int interval;
   private boolean beep;
​
   /**
    * Constructs a talking clock
    * @param interval the interval between messages (in milliseconds)
    * @param beep true if the clock should beep
    */
   public TalkingClock(int interval, boolean beep)
   {
      this.interval = interval;
      this.beep = beep;
   }
​
   /**
    * Starts the clock.
    */
   public void start()
   {
      ActionListener listener = new TimePrinter();
      Timer t = new Timer(interval, listener);
      t.start();
   }
​
   public 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();
      }
   }
}
​

 

内部类的特殊语法规则

事实上,使用外围类引用的 正规语法还要复杂一些。表达式

OwterClass.this

表示外围类引用。例如,可以像下面这样编写 TimePrinter 内部类的 actionPerformed方法:

public void actionPerformed(ActionEvent event) { 
​
 . . .
​
if (TalkingClock.this,beep) Toolkit.getDefaultToolkitO.beep(); 
​
} 

 

反过来,可以采用下列语法格式更加明确地编写内部对象的构造器:

outerObject.n&H InnerClass{construction parameters)

例如,

ActionListener listener = this.new TimePrinter();

在这里,最新构造的 TimePrinter 对象的外围类引用被设置为创建内部类对象的方法中的 this 引用。这是一种最常见的情况。通常,this 限定词是多余的。不过,可以通过显式地命名 将外围类引用设置为其他的对象。

需要注意, 在外围类的作用域之外,可以这样引用内部类:

OuterClass.InnerClass

[注]  内部类中声明的所有静态域都必须是 final。原因很简单。我们希望一个静态域只 有一个实例, 不过对于每个外部对象, 会分别有一个单独的内部类实例。如果这个域不 是 final, 它可能就不是唯一的。 内部类不能有 static 方法。Java 语言规范对这个限制没有做任何解释。也可以允许有 静态方法,但只能访问外围类的静态域和方法。显然,Java 设计者认为相对于这种复杂 性来说, 它带来的好处有些得不偿失。

局部内部类

如果仔细地阅读一下 TalkingClock 示例的代码就会发现, TimePrinter 这个类名字只在 start 方法中创建这个类型的对象时使用了一次。

当遇到这类情况时, 可以在一个方法中定义局部类。

public void start(){ 
​
class TiiePrinter inpleients ActionListener { 
​
    public void actionPerforaed(ActionEvent event) { 
      System.out.println("At the tone, the tine is " + new DateO); 
    if (beep)
      Toolkit.getDefaultToolkit.beep(); ​ } ​ } ActionListener listener
= new TimePrinter(); ​ Timer t = new Timer(interva1, listener); ​ t.start(); }

 

局部类不能用 public 或 private访问说明符进行声明。它的作用域被限定在声明这个局部 类的块中。

局部类有一个优势, 即对外部世界可以完全地隐藏起来。 即使 TalkingClock 类中的其他 代码也不能访问它。除 start 方法之外, 没有任何方法知道 TimePrinter 类的存在。

由外部方法访问变量

与其他内部类相比较, 局部类还有一个优点。它们不仅能够访问包含它们的外部类, 还 可以访问局部变量。不过,那些局部变量必须事实上为 final。这说明,它们一旦赋值就绝不 会改变。

下面是一个典型的示例。这里, 将 TalkingClock 构造器的参数 interva丨和 beep 移至 start 方法中。

public void start(int interval, boolean beep) { 
​
    class TimePrinter implements ActionListener { 
​
        public void actionPerformed(ActionEvent event) { 
​
            Systea.out.println("At the tone, the tiie is " + new DateO);
​
            if (beep) Toolkit.getDefaultToolki10•beep(); 
​
        } 
​
    } 
​
    ActionListener listener = new TimePrinter(); 
​
    Timer t = new Timer(interval,listener); 
​
    t.start(); 
​
} 

 

请注意,TalkingClock 类不再需要存储实例变量 beep 了,它只是引用 start 方法中的 beep 参数变量。

为了能够清楚地看到内部的问题,让我们仔细地考査一下控制流程。

1 ) 调用 start 方法。

2) 调用内部类 TimePrinter 的构造器, 以便初始化对象变量 listener。

3 ) 将 listener 引用传递给 Timer 构造器,定时器开始计时, start 方法结束。此时,start 方法的 beep 参数变量不复存在。

4 ) 然后,actionPerformed方法执行 if(beep)...。

匿名内部类

假如只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类(anonymous inner class)。

public void start(int interval, boolean beep) { 
​
    ActionListener listener = new ActionListenerO { 
​
        public void actionPerformed(ActionEvent event) { 
​
            System.out.println("At the tone, the time is " + new Date()); 
​
            if (beep) Toolkit.getDefaultToolkit().beep(); 
        } 
    } ;
​
    Timer t = new Timer(interval, listener); 
​
    t.start();
​
} 

 

这种语法确实有些难以理解。它的含义是:创建一个实现 ActionListener 接口的类的新 对象,需要实现的方法 actionPerformed定义在括号内。

通常的语法格式为:

new SuperType(constructionparameters) { 
​
inner class methodsanddata 
​
} 

 

其中, SuperType 可以是 ActionListener 这样的接口, 于是内部类就要实现这个接口。 SuperType 也可以是一个类,于是内部类就要扩展它。

由于构造器的名字必须与类名相同, 而匿名类没有类名,所以,匿名类不能有构造器。 取而代之的是,将构造器参数传递给超类(superclass) 构造器。尤其是在内部类实现接口的 时候, 不能有任何构造参数。不仅如此,还要像下面这样提供一组括号:

new InterfaceType(){ 
​
methods and data 
​
} 

 

如果构造参数的闭小括号后面跟一个开大括号, 正在定义的就是匿名内部类。

程序清单 6-8包含了用匿名内部类实现语音时钟程序的全部源代码。将这个程序与程序 清单 6-7相比较就会发现使用匿名内部类的解决方案比较简短、更切实际、 更易于理解。

//程序清单 6-8 anonymousInnerClass/AnonymousInnerClassTest.java
package anonymousInnerClass;
​
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.Timer;
​
/**
 * This program demonstrates anonymous inner classes.
 * @version 1.10 2004-02-27
 * @author Cay Horstmann
 */
public class AnonymousInnerClassTest
{
   public static void main(String[] args)
   {
      TalkingClock clock = new TalkingClock();
      clock.start(1000, true);
​
      // keep program running until user selects "Ok"
      JOptionPane.showMessageDialog(null, "Quit program?");
      System.exit(0);
   }
}
​
/**
 * A clock that prints the time in regular intervals.
 */
class TalkingClock
{
   /**
    * Starts the clock.
    * @param interval the interval between messages (in milliseconds)
    * @param beep true if the clock should beep
    */
   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();
   }
}

 

静态内部类

有时候, 使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用 外围类对象。为此,可以将内部类声明为 static, 以便取消产生的引用。

在内部类不需要访问外围类对象的时候, 应该使用静态内部类。 有些程序员用嵌 套类 (nested class) 表示静态内部类。

与常规内部类不同, 静态内部类可以有静态域和方法。

声明在接口中的内部类自动成为 static 和 public 类。

程序清单 6-9 包含 ArrayAIg 类和嵌套的 Pair 类的全部源代码。

//程序清单 6-9 staticInnerClass/StaticInnerClassTest.java
package staticInnerClass;
​
/**
 * This program demonstrates the use of static inner classes.
 * @version 1.01 2004-02-27
 * @author Cay Horstmann
 */
public class StaticInnerClassTest
{
   public static void main(String[] args)
   {
      double[] d = new double[20];
      for (int i = 0; i < d.length; i++)
         d[i] = 100 * Math.random();
      ArrayAlg.Pair p = ArrayAlg.minmax(d);
      System.out.println("min = " + p.getFirst());
      System.out.println("max = " + p.getSecond());
   }
}
​
class ArrayAlg
{
   /**
    * A pair of floating-point numbers
    */
   public static class Pair
   {
      private double first;
      private double second;
​
      /**
       * Constructs a pair from two floating-point numbers
       * @param f the first number
       * @param s the second number
       */
      public Pair(double f, double s)
      {
         first = f;
         second = s;
      }
​
      /**
       * Returns the first number of the pair
       * @return the first number
       */
      public double getFirst()
      {
         return first;
      }
​
      /**
       * Returns the second number of the pair
       * @return the second number
       */
      public double getSecond()
      {
         return second;
      }
   }
​
   /**
    * Computes both the minimum and the maximum of an array
    * @param values an array of floating-point numbers
    * @return a pair whose first element is the minimum and whose second element
    * is the maximum
    */
   public static Pair minmax(double[] values)
   {
      double min = Double.MAX_VALUE;
      double max = Double.MIN_VALUE;
      for (double v : values)
      {
         if (min > v) min = v;
         if (max < v) max = v;
      }
      return new Pair(min, max);
   }
}
​

 

 

第七章 异常、断言和曰志

 

处理错误

也有可能是因为使用无效的数组下标, 或者试图使用 一个没有被赋值的对象引用而造成的。用户期望在出现错误时, 程序能够采用一些理智的行 为。如果由于出现错误而使得某些操作没有完成, 程序应该:

•返回到一种安全状态,并能够让用户执行一些其他的命令;

•允许用户保存所有操作的结果,并以妥善的方式终止程序。

为了能够在程序中处理异常情况, 必须研究程序中可能会出现的错误和问题, 以及哪类 问题需要关注。

1.用户输入错误

除了那些不可避免的键盘输人错误外, 有些用户喜欢各行其是,不遵守程序的要求。例 如, 假设有一个用户请求连接一个 URL,而语法却不正确。在程序代码中应该对此进行检 查, 如果没有检査,网络层就会给出警告。

2.设备错误

硬件并不总是让它做什么,它就做什么。打印机可能被关掉了。网页可能临时性地不能浏 览。在一个任务的处理过程中,硬件经常出现问题。例如,打印机在打印过程中可能没有纸了。

3.物理限制

磁盘满了,可用存储空间已被用完。

4.代码错误

程序方法有可能无法正确执行。例如,方法可能返回了一个错误的答案,或者错误地调 用了其他的方法。计算的数组索引不合法,试图在散列表中查找一个不存在的记录, 或者试 图让一个空找执行弹出操作,这些都属于代码错误。

 

异常分类

在 Java 程序设计语言中, 异常对象都是派生于 Throwable 类的一个实例。

需要注意的是,所有的异常都是由 Throwable 继承而来,但在下一层立即分解为两个分 支:Error 和 Exception。

Error类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误。应用程序不应该 抛出这种类型的对象。 如果出现了这样的内部错误, 除了通告给用户,并尽力使程序安全地 终止之外, 再也无能为力了。这种情况很少出现。

在设计 Java 程序时, 需要关注 Exception 层次结构。这个层次结构又分解为两个分支: 一个分支派生于 RuntimeException ; 另一个分支包含其他异常。划分两个分支的规则是: 由 程序错误导致的异常属于 RuntimeException ; 而程序本身没有问题, 但由于像 I/O 错误这类 问题导致的异常属于其他异常。

派生于 RuntimeException 的异常包含下面几种情况:

•错误的类型转换。

•数组访问越界 。

•访问 null 指针。

不是派生于 RuntimeException 的异常包括:

•试图在文件尾部后面读取数据。

•试图打开一个不存在的文件。

•试图根据给定的字符串查找 Class 对象, 而这个字符串表示的类并不存在。

Java语 言 规 范 将 派 生 于 Error 类 或 RuntimeException类的所有异常称为非受查 ( unchecked) 异常,所有其他的异常称为受查(checked) 异常。这是两个很有用的术语,在 后面还会用到。编译器将核查是否为所有的受査异常提供了异常处理器。

声明受查异常

方法应该在其首部声明所有可能抛出的异常。这样可以从首部反映出这个方法可能抛出 哪类受査异常。

在自己编写方法时, 不必将所有可能抛出的异常都进行声明。至于什么时候需要在方法 中用 throws 子句声明异常, 什么异常必须使用 throws 子句声明, 需要记住在遇到下面 4 种 情况时应该抛出异常:

1 ) 调用一个抛出受査异常的方法, 例如, FilelnputStream 构造器。

2 ) 程序运行过程中发现错误,并且利用 throw语句抛出一个受查异常(下一节将详细地 介绍 throw 语句)。

3 ) 程序出现错误, 例如,a[-l]=0 会抛出一个 ArraylndexOutOffloundsException 这样的 非受查异常。

4 ) Java 虚拟机和运行时库出现的内部错误。

 

如何抛出异常

假设在程序代码中发生了一些很糟糕的事情。一个名为 readData 的方法正在读取一个首 部具有下列信息的文件:

Content-length: 1024

然而,读到 733 个字符之后文件就结束了。我们认为这是一种不正常的情况,希望抛出一个 异常。

首先要决定应该抛出什么类型的异常。将上述异常归结为 IOException 是一种很好的选 择。仔细地阅读 Java API 文档之后会发现:EOFException异常描述的是“ 在输人过程中, 遇 到了一个未预期的 EOF 后的信号”。这正是我们要抛出的异常。下面是抛出这个异常的语句:

throw new EOFExceptionQ;

或者

EOFException e = new EOFExceptionO;

throw e;

下面将这些代码放在一起:

String readData(Scanner in) throws EOFException{
​
    . . . 
​
    while (…) { 
​
        if (Mn.hasNextQ) // EOF encountered
​
        {
​
            if (n < len) 
​
                throw new EOFExceptionQ;
​
        }
​
        ...
​
    }
​
    return s;
​
}

 

EOFException类还有一个含有一个字符串型参数的构造器。这个构造器可以更加细致的 描述异常出现的情况。

String gripe = "Content-length: " + len + ", Received: " + n; 
​
throw new EOFException(gripe); 

 

在前面已经看到, 对于一个已经存在的异常类, 将其抛出非常容易 D 在这种情况下:

1 ) 找到一个合适的异常类。

2 ) 创建这个类的一个对象。

3 ) 将对象抛出。 一旦方法抛出了异常, 这个方法就不可能返回到调用者。也就是说, 不必为返回的默认 值或错误代码担忧。

创建异常类

定义一个派生于 IOException 的类。 习惯上, 定义的类应该包含两个构造器, 一个是默认的构造器;另一个是带有详细描述信息 的构造器(超类 Throwable 的 toString 方法将会打印出这些详细信息, 这在调试中非常有用)。

class FileFormatException extends IOException { 
​
    public FileFormatException() {} 
​
    public FileFormatException(String gripe) { 
        super(gripe); 
​
    } 
​
} 

 

现在,就可以抛出自己定义的异常类型了。

String readData(BufferedReader in) throws FileFormatException { 
​
...
​
    while (. ..) { 
​
        if (ch == -1 ) // EOF encountered
        {
            if (n < len) 
                throw new FileFornatExceptionQ;
​
        }
        ...
    }
    return s;
}

 

 

posted on 2020-08-11 19:51  ♌南墙  阅读(178)  评论(0)    收藏  举报