《Core Java》读书笔记——第6章

邹宏峻
2023-12-01

接口与内部类


本章代码

接口

接口不是类,而是对类的一组需求描述。以Comparable接口为例,对象所属的类如果实现了Comparable接口,Arrays类中的sort方法就可以对该类的对象数组进行排序。接口中的方法自动属于public,无需提供修饰符。为了让类实现一个接口,通常需要下面两个步骤

1)将类声明为实现给定的接口
2)对接口中的所有方法进行定义

    class Employee implements Comparable
    {
        public int compareTo(Object otherObject)
        {
            Employee other = (Employee)otherObject;
            return Double.compare(salary,other.salary);
        }
        ...
    }

Java SE 5.0 中,可以做得更好。

    class Employee implements Comparable<Employee>
    {
        public int compareTo(Employee other)
        {
            return Double.compare(salary,other.salary);
        }
        ...
    }

这样就不用对参数Object进行类型转换了。但是这种做法又会在继承中出现问题,Manager类继承了Employee类,但是Employee实现的是Comparable<Employee>接口。解决方法和equals类似。如果子类之间比较的含义不一样,那么应该在compareTo方法开始时进行下列检测。

    if(getClass() != other.getClass()) throw new ClassCastException();

如果存在一个通用算法,它能够对不同的子类对象进行比较,则应该在超类中提供这样的compareTo方法,并将方法声明为final。

6.1.1 接口的特性

接口不是类,尤其是不能使用new实例化一个接口。尽管不能构造接口的对象,却能声明接口的变量。例如

    Comparable x;  //OK
    x = new Employee(...)  //OK, provided Employee implements Comparable

也可以使用instanceof检查一个对象是否实现了某个特定的接口:

    if(anObject instanceof Comparable){...}

接口也可以被扩展,如同继承链一样。

    public interface Moveable
    {
        void move(double x, double y);
    }   

    public interface Powered extends Moveable
    {
        double milesPerGallon();
    }
    //接口中虽然不能包含实例域或静态方法,却可以包含常量
    public interface Powered extends Moveable
    {
        double milesPerGallon();
        double SPEED_LIMIT = 95; //a public static final constant
    }

接口中的域被自动设为public static final。

6.1.2 接口与抽象类

每个类只能继承一个类,不能多继承,但是一个类可以扩展多个接口。

    class Employee extends Person, OtherClass //ERROR
    class Employee implements Cloneable, Comparable //OK
    class Employee extends Person implements Comparable // OK

6.2 对象克隆

当拷贝一个变量时,原始变量与拷贝变量引用同一个对象,改变任意一个都会对另一个产生影响。如果想不影响,就需要利用到clone方法。

clone方法时Object类的一个proteced方法,在用户编写的代码中不能直接调用它。默认的clone操作是浅拷贝。它不能拷贝包含在对象中的内部对象。对每一个类,都要做出如下判断

1) 默认的clone方法是否满足要求
2) 默认的clone方法是否能够通过调用可变子对象的clone得到修补
3) 是否不应该使用clone

选项3 是默认的,如果要选择1或2,类必须

实现Cloneable接口
使用pbulic重新定义clone方法。

如果一个对象需要克隆,而没有实现Cloneable接口,就会产生一个已检验异常。

    class Employee implements Cloneable
    {
        public Employee clone() throws CloneNotSupportedException
        {
            return (Employee) super.clone();
        }
    }

注意,需要声明异常。


6.3 接口与回调

回调是一种常见的程序设计模式,可以指出某个特定事件发生时应该采取的动作。从一个简单的程序看看接口与回调。javax.swing中的Timer类,可以使用它在到达给定的时间间隔时发出通告。定时器需要知道调用哪一个方法,并要求传递的对象所属的类实现了java.event包的ActionListener接口,当到达指定时间间隔,定时器就调用actionPerformed方法。

    public interface ActionListener
    {
        void actionPerformed(ActionEvent event);
    }

例如

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

接下来,构造一个这个类的一个对象,传递个Timer的构造器

    ActionListener listener = new TimerPrinter();
    Timer t = new Timer(10000,listener);
    t.start();          //启动定时器

来看看整个程序吧,很简单。

import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;

import javax.swing.JOptionPane;
import javax.swing.Timer;

public class TimerTest {
    public static void main(String[] args)
    {
        ActionListener listener = new TimePrinter();
        Timer t = new Timer(10000,listener);
        t.start();

        JOptionPane.showMessageDialog(null, "Quit Program?");
        System.exit(0);
    }



}

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

6.4 内部类

内部类是定义在另一个类中的类。

内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
内部类可以对同一个包中的其他类隐藏起来。
当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷。

6.4.1 使用内部类访问对象状态

用TalkingClock类来介绍一下内部类的使用方式

    public class TalkingClock
    {
        private int interval;
        private boolean beep;
        public TalkingClock(int interval, boolean beep){...}
        public void start(){...}

        //an inner class
        public class TimerPrinter implements ActionListener
        {
            ...
        }
    }

TimePrinter 位于 TalkinClock类的内部,它的对象,由TalkingClock类的方法构造。

    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();
        }
    }

TimPrinter 类并没有任何数据域,但是确能够访问外围类的beep域。内部类的对象总有一个隐式引用,它指向了创建它的外部类对象。为了说明这个概念,将外围类对象的引用称为outer。

    public void actionPerformed(ActionEvent event)
        {
            Date now = new Date();
            System.out.println("At the tone, the time is "+now);
            if(outer.beep) Toolkit.getDefaultToolkit().beep();
        }
    }

那么outer这个引用何时被设置呢?编译器修改了所有内部类的构造器,添加一个外围类引用的参数。由于TimePrinter类没有定义构造器,所以编译器为这个类生成了一个默认的构造器。

    public TimePrinter(TalkingClock clock)  //自动生成的代码
    {
        outer = clock
    }

当在外围类中的方法创建TimePrinter对象后,编译器就会将this引用传递给outer

    ActionListener listener = new TimePrinter(this); //参数是被自动添加的,实际并不需要这样写

可以看到,outer此时和this指向同一个对象,也就是这个外围类。看看这个简单的程序吧。

import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;

import javax.swing.JOptionPane;
import javax.swing.Timer;

public class InnerClassTest {
    public static void main(String[] args)
    {
        TalkingClock clock = new TalkingClock(10000, true);
        clock.start();

        JOptionPane.showMessageDialog(null, "Quit program?");
        System.exit(0);
    }
}

class TalkingClock
{
    private int interval;
    private boolean beep;


    public TalkingClock(int interval, boolean beep)
    {
        this.interval = interval;
        this.beep  = beep;
    }

    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();
        }
    }
}

6.4.2 内部类的特殊语法规则

前面介绍过,内部类有一个外围类的引用outer,正式语法如下
OuterClass.this
可以像下面这样编写actionPerformed方法:

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

可以用下列语法格式更加明确地编写内部对象的构造器:
outerObject.new InnerClass(construction parameters)
例如

    //外围类内部的方法可以这么写
    ActionListener listener = this.new TimePrinter();

如果TimePrinter是一个公有内部类,对于任意的语音时钟都可以构造一个TimePrinter:

    TalkingClock jabberer = new TalkingClock(10000,true);
    TalkingClock.TimePrinter listener = jabber.new TimePrinter();

6.4.3 内部类是否有用、必要和安全

内部类拥有访问特权,比起常规类功能更加强大。(访问数据方面),分析一下内部类

    public class chapter6.section4.innerclass.TalkingClock$TimePrinter{
    public chapter6.section4.innerclass.TalkingClock$TimePrinter(chapter6.section4.innerclass.TalkingClock);

    public void actionPerformed(java.awt.event.ActionEvent);

    final chapter6.section4.innerclass.TalkingClock this$0;
}

可以看到,编译器为了引用外围类,生成了一个附加的实例域this$0。另外还可以看到编译器自动合成的构造器,有一个TalkingClock参数。内部类被翻译成了名字很古怪的常规类,作为常规类,如何访问外围类的私有数据呢?再来分析一下TalkingClock类:

    class chapter6.section4.innerclass.TalkingClock{
    public chapter6.section4.innerclass.TalkingClock(int, boolean);

    public void start();
    static boolean access$0(chapter6.section4.innerclass.TalkingClock);

    private int interval;
    private boolean beep;
}

编译器在外围类添加了静态方法access$0。它将返回作为参数传递给它的对象域beep。也就是在TimePrinter类中的方法actionPerformed中访问beep时

    if(beep)

其实相当于

    if(access$0(outer))

6.4.4 局部内部类

上述例子中,TimePrinter这个类名字只在start方法中出现过一次,这种情况可以在方法中定义局部类

    public void start()
    {
         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();
    }

局部类不能用public或private访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。局部类的优势在于它对外部世界完全隐藏起来了。

6.4.5 由外部方法访问final变量

与其他内部类相比,局部类还有一个优点,它们不仅能够访问包含它们的外部类,还可以访问局部变量,不过,那些局部变量必须被声明为final。

    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();
    }

TimePrinter类在beep释放之前将beep用start方法的局部变量进行备份。但是必须要声明为final。

如果想要在一个封闭的作用域内更改某个局部变量,如下

    int counter = 0;
    Date[] dates = new Date[100];
    for(int i = 0; i < dates.length; i++)
        dates[i] = new Date();
        {
            public int compareTo(Date other)
            {
                counter++;  //ERROR 无法访问
                return super.compareTo(other);
            }
        }

上述对counter的访问是无法成功的,但是如果改成了final,就没办法进行更改了,解决方法时可以使用一个长度为1的数组,并声明为final,这样的话,counter变量不能访问其他的数组引用了,数组内容可以更改。

    final   int[] counter = new int[1];
    Date[] dates = new Date[100];
    for(int i = 0; i < dates.length; i++)
        dates[i] = new Date();
        {
            public int compareTo(Date other)
            {
                counter[0]++;  //OK
                return super.compareTo(other);
            }
        }

6.4.6 匿名内部类

将局部类的使用再更进一步。加入只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类。

    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 SuperTyper(construction parameters)
{
inner class methods and data
}
SuperType可以是一个接口,于是内部类就要实现这个接口。SuperType也可以是一个类,内部类需要扩展它。

匿名类没有类名,所以不能有任何构造器。
取而代之的是,将构造器参数传递给超类。

构造一个类的新对象与构造一个扩展了那个类的匿名内部类的对象的区别如下

    Person queen = new Person("Mary");  //一个Person对象
    Person count = new Person("Alice") {...};
    //一个继承了Person类的匿名内部类的对象。

提示:生成日志调试消息是,通常希望包含当前类的类名:

    System.err.println("Something awful happened in " + getClass());

不过,这对静态方法不奏效,因为静态方法不包含this,所以应该使用以下的表达式:

    new Object(){}.getClass().getEnclosingClass(); //get class of static method

6.4.7 静态内部类

有时候,使用内部类知识为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。为此可以将内部类声明为static。从一个程序中简单看看静态内部类的使用方法。

    class ArrayAlg
    {
        public static class Pair
        {
            ...
        }
        ...
    }

创建Pair类的目的是为了能返回两个值,利用Pair中的方法获取返回的两个值。


public class StaticInnerClassTest {
    public static void main(String[] args)
    {
        double[] d = new double[20];
        for(int i = 0; i < d.length; i++)
            d[i] = Math.random();
        ArrayAlg.Pair p = ArrayAlg.minmax(d);
        for(double v : d)
            System.out.println(v);
        System.out.println("min value: " + p.getMin());
        System.out.println("max value: " + p.getMax());
    }

}

class ArrayAlg
{
    public static class Pair
    {
        private double min;
        private double max;

        public Pair(double min, double max)
        {
            this.min = min;
            this.max = max;
        }

        public double getMin() {
            return min;
        }

        public double getMax() {
            return max;
        }
    }

    public static Pair minmax(double[] d)
    {
        double min = Double.MAX_VALUE;
        double max = Double.MIN_VALUE;

        for(double v : d)
        {
            if(v < min) min = v;
            if(v > max) max = v;
        }

        return new Pair(min,max);
    }
}
 类似资料: