Java8编程思想精粹(十一)-内部类(下)

简介: Java8编程思想精粹(十一)-内部类(下)

内部类与控制框架

在将要介绍的控制框架(control framework)中,可以看到更多使用内部类的具体例子。

应用程序框架(application framework)就是被设计用以解决某类特定问题的一个类或一组类。要运用某个应用程序框架,通常是继承一个或多个类,并覆盖某些方法。在覆盖后的方法中,编写代码定制应用程序框架提供的通用解决方案,以解决你的特定问题。这是设计模式中模板方法的一个例子,模板方法包含算法的基本结构,并且会调用一个或多个可覆盖的方法,以完成算法的动作。设计模式总是将变化的事物与保持不变的事物分离开,在这个模式中,模板方法是保持不变的事物,而可覆盖的方法就是变化的事物。


控制框架是一类特殊的应用程序框架,它用来解决响应事件的需求。主要用来响应事件的系统被称作事件驱动系统。应用程序设计中常见的问题之一是图形用户接口(GUI),它几乎完全是事件驱动的系统。


要理解内部类是如何允许简单的创建过程以及如何使用控制框架的,请考虑这样一个控制框架,它的工作就是在事件“就绪”的时候执行事件。虽然“就绪”可以指任何事,但在本例中是指基于时间触发的事件。接下来的问题就是,对于要控制什么,控制框架并不包含任何具体的信息。那些信息是在实现算法的 action() 部分时,通过继承来提供的。


首先,接口描述了要控制的事件。因为其默认的行为是基于时间去执行控制,所以使用抽象类代替实际的接口。下面的例子包含了某些实现:

// innerclasses/controller/Event.java
// The common methods for any control event
package innerclasses.controller;
import java.time.*; // Java 8 time classes
public abstract class Event {
    private Instant eventTime;
    protected final Duration delayTime;
    public Event(long millisecondDelay) {
        delayTime = Duration.ofMillis(millisecondDelay);
        start();
    }
    public void start() { // Allows restarting
        eventTime = Instant.now().plus(delayTime);
    }
    public boolean ready() {
        return Instant.now().isAfter(eventTime);
    }
    public abstract void action();
}

当希望运行 Event 并随后调用 start() 时,那么构造器就会捕获(从对象创建的时刻开始的)时间,此时间是这样得来的:start() 获取当前时间,然后加上一个延迟时间,这样生成触发事件的时间。start() 是一个独立的方法,而没有包含在构造器内,因为这样就可以在事件运行以后重新启动计时器,也就是能够重复使用 Event 对象。例如,如果想要重复一个事件,只需简单地在 action() 中调用 start() 方法。


ready() 告诉你何时可以运行 action() 方法了。当然,可以在派生类中覆盖 ready() 方法,使得 Event 能够基于时间以外的其他因素而触发。


下面的文件包含了一个用来管理并触发事件的实际控制框架。Event 对象被保存在 List<Event> 类型(读作“Event 的列表”)的容器对象中,容器会在 集合 中详细介绍。目前读者只需要知道 add() 方法用来将一个 Event 添加到 List 的尾端,size() 方法用来得到 List 中元素的个数,foreach 语法用来连续获联 List 中的 Event,remove() 方法用来从 List 中移除指定的 Event。

// innerclasses/controller/Controller.java
// The reusable framework for control systems
package innerclasses.controller;
import java.util.*;
public class Controller {
    // A class from java.util to hold Event objects:
    private List<Event> eventList = new ArrayList<>();
    public void addEvent(Event c) { eventList.add(c); }
    public void run() {
        while(eventList.size() > 0)
            // Make a copy so you're not modifying the list
            // while you're selecting the elements in it:
            for(Event e : new ArrayList<>(eventList))
                if(e.ready()) {
                    System.out.println(e);
                    e.action();
                    eventList.remove(e);
                }
    }
}

run() 方法循环遍历 eventList,寻找就绪的(ready())、要运行的 Event 对象。对找到的每一个就绪的(ready())事件,使用对象的 toString() 打印其信息,调用其 action() 方法,然后从列表中移除此 Event。


注意,在目前的设计中你并不知道 Event 到底做了什么。这正是此设计的关键所在—"使变化的事物与不变的事物相互分离”。用我的话说,“变化向量”就是各种不同的 Event 对象所具有的不同行为,而你通过创建不同的 Event 子类来表现不同的行为。


这正是内部类要做的事情,内部类允许:


控制框架的完整实现是由单个的类创建的,从而使得实现的细节被封装了起来。内部类用来表示解决问题所必需的各种不同的 action()。

内部类能够很容易地访问外围类的任意成员,所以可以避免这种实现变得笨拙。如果没有这种能力,代码将变得令人讨厌,以至于你肯定会选择别的方法。

考虑此控制框架的一个特定实现,如控制温室的运作:控制灯光、水、温度调节器的开关,以及响铃和重新启动系统,每个行为都是完全不同的。控制框架的设计使得分离这些不同的代码变得非常容易。使用内部类,可以在单一的类里面产生对同一个基类 Event 的多种派生版本。对于温室系统的每一种行为,都继承创建一个新的 Event 内部类,并在要实现的 action() 中编写控制代码。


作为典型的应用程序框架,GreenhouseControls 类继承自 Controller:

// innerclasses/GreenhouseControls.java
// This produces a specific application of the
// control system, all in a single class. Inner
// classes allow you to encapsulate different
// functionality for each type of event.
import innerclasses.controller.*;
public class GreenhouseControls extends Controller {
    private boolean light = false;
    public class LightOn extends Event {
        public LightOn(long delayTime) {
            super(delayTime); 
        }
        @Override
        public void action() {
            // Put hardware control code here to
            // physically turn on the light.
            light = true;
        }
        @Override
        public String toString() {
            return "Light is on";
        }
    }
    public class LightOff extends Event {
        public LightOff(long delayTime) {
            super(delayTime);
        }
        @Override
        public void action() {
            // Put hardware control code here to
            // physically turn off the light.
            light = false;
        }
        @Override
        public String toString() {
            return "Light is off";
        }
    }
    private boolean water = false;
    public class WaterOn extends Event {
        public WaterOn(long delayTime) {
            super(delayTime);
        }
        @Override
        public void action() {
            // Put hardware control code here.
            water = true;
        }
        @Override
        public String toString() {
            return "Greenhouse water is on";
        }
    }
    public class WaterOff extends Event {
        public WaterOff(long delayTime) {
            super(delayTime);
        }
        @Override
        public void action() {
            // Put hardware control code here.
            water = false;
        }
        @Override
        public String toString() {
            return "Greenhouse water is off";
        }
    }
    private String thermostat = "Day";
    public class ThermostatNight extends Event {
        public ThermostatNight(long delayTime) {
            super(delayTime);
        }
        @Override
        public void action() {
            // Put hardware control code here.
            thermostat = "Night";
        }
        @Override
        public String toString() {
            return "Thermostat on night setting";
        }
    }
    public class ThermostatDay extends Event {
        public ThermostatDay(long delayTime) {
            super(delayTime);
        }
        @Override
        public void action() {
            // Put hardware control code here.
            thermostat = "Day";
        }
        @Override
        public String toString() {
            return "Thermostat on day setting";
        }
    }
    // An example of an action() that inserts a
    // new one of itself into the event list:
    public class Bell extends Event {
        public Bell(long delayTime) {
            super(delayTime);
        }
        @Override
        public void action() {
            addEvent(new Bell(delayTime.toMillis()));
        }
        @Override
        public String toString() {
            return "Bing!";
        }
    }
    public class Restart extends Event {
        private Event[] eventList;
        public
        Restart(long delayTime, Event[] eventList) {
            super(delayTime);
            this.eventList = eventList;
            for(Event e : eventList)
                addEvent(e);
        }
        @Override
        public void action() {
            for(Event e : eventList) {
                e.start(); // Rerun each event
                addEvent(e);
            }
            start(); // Rerun this Event
            addEvent(this);
        }
        @Override
        public String toString() {
            return "Restarting system";
        }
    }
    public static class Terminate extends Event {
        public Terminate(long delayTime) {
            super(delayTime);
        }
        @Override
        public void action() { System.exit(0); }
        @Override
        public String toString() {
            return "Terminating";
        }
    }
}

注意,light,water 和 thermostat 都属于外围类 GreenhouseControls,而这些内部类能够自由地访问那些字段,无需限定条件或特殊许可。而且,action() 方法通常都涉及对某种硬件的控制。


大多数 Event 类看起来都很相似,但是 Bell 和 Restart 则比较特别。Bell 控制响铃,然后在事件列表中增加一个 Bell 对象,于是过一会儿它可以再次响铃。读者可能注意到了内部类是多么像多重继承:Bell 和 Restart 有 Event 的所有方法,并且似乎也拥有外围类 GreenhouseContrlos 的所有方法。


一个由 Event 对象组成的数组被递交给 Restart,该数组要加到控制器上。由于 Restart() 也是一个 Event 对象,所以同样可以将 Restart 对象添加到 Restart.action() 中,以使系统能够有规律地重新启动自己。


下面的类通过创建一个 GreenhouseControls 对象,并添加各种不同的 Event 对象来配置该系统,这是命令设计模式的一个例子—eventList 中的每个对象都被封装成对象的请求:

// innerclasses/GreenhouseController.java
// Configure and execute the greenhouse system
import innerclasses.controller.*;
public class GreenhouseController {
    public static void main(String[] args) {
        GreenhouseControls gc = new GreenhouseControls();
        // Instead of using code, you could parse
        // configuration information from a text file:
        gc.addEvent(gc.new Bell(900));
        Event[] eventList = {
                gc.new ThermostatNight(0),
                gc.new LightOn(200),
                gc.new LightOff(400),
                gc.new WaterOn(600),
                gc.new WaterOff(800),
                gc.new ThermostatDay(1400)
        };
        gc.addEvent(gc.new Restart(2000, eventList));
        gc.addEvent(
                new GreenhouseControls.Terminate(5000));
        gc.run();
    }
}

输出为:

Thermostat on night setting
Light is on
Light is off
Greenhouse water is on
Greenhouse water is off
Bing!
Thermostat on day setting
Bing!
Restarting system
Thermostat on night setting
Light is on
Light is off
Greenhouse water is on
Bing!
Greenhouse water is off
Thermostat on day setting
Bing!
Restarting system
Thermostat on night setting
Light is on
Light is off
Bing!
Greenhouse water is on
Greenhouse water is off
Terminating

这个类的作用是初始化系统,所以它添加了所有相应的事件。Restart 事件反复运行,而且它每次都会将 eventList 加载到 GreenhouseControls 对象中。如果提供了命令行参数,系统会以它作为毫秒数,决定什么时候终止程序(这是测试程序时使用的)。


当然,更灵活的方法是避免对事件进行硬编码。


这个例子应该使读者更了解内部类的价值了,特别是在控制框架中使用内部类的时候。

继承内部类

因为内部类的构造器必须连接到指向其外围类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。问题在于,那个指向外围类对象的“秘密的”引用必须被初始化,而在派生类中不再存在可连接的默认对象。要解决这个问题,必须使用特殊的语法来明确说清它们之间的关联:

// innerclasses/InheritInner.java
// Inheriting an inner class
class WithInner {
    class Inner {}
}
public class InheritInner extends WithInner.Inner {
    //- InheritInner() {} // Won't compile
    InheritInner(WithInner wi) {
        wi.super();
    }
    public static void main(String[] args) {
        WithInner wi = new WithInner();
        InheritInner ii = new InheritInner(wi);
    }
}

可以看到,InheritInner 只继承自内部类,而不是外围类。但是当要生成一个构造器时,默认的构造器并不算好,而且不能只是传递一个指向外围类对象的引用。此外,必须在构造器内使用如下语法:

enclosingClassReference.super();

这样才提供了必要的引用,然后程序才能编译通过。

内部类可以被覆盖么?

如果创建了一个内部类,然后继承其外围类并重新定义此内部类时,会发生什么呢?也就是说,内部类可以被覆盖吗?这看起来似乎是个很有用的思想,但是“覆盖”内部类就好像它是外围类的一个方法,其实并不起什么作用:

// innerclasses/BigEgg.java
// An inner class cannot be overridden like a method
class Egg {
    private Yolk y;
    protected class Yolk {
        public Yolk() {
            System.out.println("Egg.Yolk()");
        }
    }
    Egg() {
        System.out.println("New Egg()");
        y = new Yolk();
    }
}
public class BigEgg extends Egg {
    public class Yolk {
        public Yolk() {
            System.out.println("BigEgg.Yolk()");
        }
    }
    public static void main(String[] args) {
        new BigEgg();
    }
}

输出为:

New Egg()
Egg.Yolk()

默认的无参构造器是编译器自动生成的,这里是调用基类的默认构造器。你可能认为既然创建了 BigEgg 的对象,那么所使用的应该是“覆盖后”的 Yolk 版本,但从输出中可以看到实际情况并不是这样的。


这个例子说明,当继承了某个外围类的时候,内部类并没有发生什么特别神奇的变化。这两个内部类是完全独立的两个实体,各自在自己的命名空间内。当然,明确地继承某个内部类也是可以的:

// innerclasses/BigEgg2.java
// Proper inheritance of an inner class
class Egg2 {
    protected class Yolk {
        public Yolk() {
            System.out.println("Egg2.Yolk()");
        }
        public void f() {
            System.out.println("Egg2.Yolk.f()");
        }
    }
    private Yolk y = new Yolk();
    Egg2() { System.out.println("New Egg2()"); }
    public void insertYolk(Yolk yy) { y = yy; }
    public void g() { y.f(); }
}
public class BigEgg2 extends Egg2 {
    public class Yolk extends Egg2.Yolk {
        public Yolk() {
            System.out.println("BigEgg2.Yolk()");
        }
        @Override
        public void f() {
            System.out.println("BigEgg2.Yolk.f()");
        }
    }
    public BigEgg2() { insertYolk(new Yolk()); }
    public static void main(String[] args) {
        Egg2 e2 = new BigEgg2();
        e2.g();
    }
}

输出为:

Egg2.Yolk()
New Egg2()
Egg2.Yolk()
BigEgg2.Yolk()
BigEgg2.Yolk.f()

现在 BigEgg2.Yolk 通过 extends Egg2.Yolk 明确地继承了此内部类,并且覆盖了其中的方法。insertYolk() 方法允许 BigEgg2 将它自己的 Yolk 对象向上转型为 Egg2 中的引用 y。所以当 g() 调用 y.f() 时,覆盖后的新版的 f() 被执行。第二次调用 Egg2.Yolk(),结果是 BigEgg2.Yolk 的构造器调用了其基类的构造器。可以看到在调用 g() 的时候,新版的 f() 被调用了。

局部内部类

前面提到过,可以在代码块里创建内部类,典型的方式是在一个方法体的里面创建。局部内部类不能有访问说明符,因为它不是外围类的一部分;但是它可以访问当前代码块内的常量,以及此外围类的所有成员。下面的例子对局部内部类与匿名内部类的创建进行了比较。

// innerclasses/LocalInnerClass.java
// Holds a sequence of Objects
interface Counter {
    int next();
}
public class LocalInnerClass {
    private int count = 0;
    Counter getCounter(final String name) {
        // A local inner class:
        class LocalCounter implements Counter {
            LocalCounter() {
                // Local inner class can have a constructor
                System.out.println("LocalCounter()");
            }
            @Override
            public int next() {
                System.out.print(name); // Access local final
                return count++;
            }
        }
        return new LocalCounter();
    }
    // Repeat, but with an anonymous inner class:
    Counter getCounter2(final String name) {
        return new Counter() {
            // Anonymous inner class cannot have a named
            // constructor, only an instance initializer:
            {
                System.out.println("Counter()");
            }
            @Override
            public int next() {
                System.out.print(name); // Access local final
                return count++;
            }
        };
    }
    public static void main(String[] args) {
        LocalInnerClass lic = new LocalInnerClass();
        Counter
                c1 = lic.getCounter("Local inner "),
                c2 = lic.getCounter2("Anonymous inner ");
        for(int i = 0; i < 5; i++)
            System.out.println(c1.next());
        for(int i = 0; i < 5; i++)
            System.out.println(c2.next());
    }
}

输出为:

LocalCounter()
Counter()
Local inner 0
Local inner 1
Local inner 2
Local inner 3
Local inner 4
Anonymous inner 5
Anonymous inner 6
Anonymous inner 7
Anonymous inner 8
Anonymous inner 9

Counter 返回的是序列中的下一个值。我们分别使用局部内部类和匿名内部类实现了这个功能,它们具有相同的行为和能力,既然局部内部类的名字在方法外是不可见的,那为什么我们仍然使用局部内部类而不是匿名内部类呢?唯一的理由是,我们需要一个已命名的构造器,或者需要重载构造器,而匿名内部类只能用于实例初始化。


所以使用局部内部类而不使用匿名内部类的另一个理由就是,需要不止一个该内部类的对象。

内部类标识符

由于编译后每个类都会产生一个**.class** 文件,其中包含了如何创建该类型的对象的全部信息(此信息产生一个"meta-class",叫做 Class 对象)。

你可能猜到了,内部类也必须生成一个**.class** 文件以包含它们的 Class 对象信息。这些类文件的命名有严格的规则:外围类的名字,加上“$",再加上内部类的名字。例如,LocalInnerClass.java 生成的 .class 文件包括:

Counter.class
LocalInnerClass$1.class
LocalInnerClass$LocalCounter.class
LocalInnerClass.class

如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符。如果内部类是嵌套在别的内部类之中,只需直接将它们的名字加在其外围类标识符与“$”的后面。


虽然这种命名格式简单而直接,但它还是很健壮的,足以应对绝大多数情况。因为这是 java 的标准命名方式,所以产生的文件自动都是平台无关的。(注意,为了保证你的内部类能起作用,Java 编译器会尽可能地转换它们。)

本章小结

比起面向对象编程中其他的概念来,接口和内部类更深奥复杂,比如 C++ 就没有这些。将两者结合起来,同样能够解决 C++ 中的用多重继承所能解决的问题。

然而,多重继承在 C++ 中被证明是相当难以使用的,相比较而言,Java 的接口和内部类就容易理解多了。


虽然这些特性本身是相当直观的,但是就像多态机制一样,这些特性的使用应该是设计阶段考虑的问题。随着时间的推移,读者将能够更好地识别什么情况下应该使用接口,什么情况使用内部类,或者两者同时使用。

但此时,至少应该已经完全理解了它们的语法和语义。


目录
相关文章
|
5天前
|
Java
java中,剩下的这两个内部类不太好理解!
java中,剩下的这两个内部类不太好理解!
14 0
|
5天前
|
Java 编译器
java中常见的几种内部类,你会几个?(未完)
java中常见的几种内部类,你会几个?(未完)
15 1
|
5天前
|
Java
Java一分钟之-Java内部类与匿名类
【5月更文挑战第12天】本文介绍了Java的内部类和匿名类,包括成员内部类和局部内部类,以及匿名类的一次性子类实现。通过代码示例展示了它们的使用方法,同时提到了常见问题和易错点,如混淆内部类与嵌套类、匿名类的生命周期管理及内部类的访问权限,并给出了相应的避免策略。理解这些概念有助于提升代码质量。
17 3
|
5天前
|
Java
Java内部类
Java内部类
10 2
|
5天前
|
Java
什么是Java内部类,为什么使用它?
【4月更文挑战第13天】
20 1
|
5天前
|
安全 Java 编译器
接口之美,内部之妙:深入解析Java的接口与内部类
接口之美,内部之妙:深入解析Java的接口与内部类
38 0
接口之美,内部之妙:深入解析Java的接口与内部类
|
5天前
|
Java API
Java基础—笔记—内部类、枚举、泛型篇
本文介绍了Java编程中的内部类、枚举和泛型概念。匿名内部类用于简化类的创建,常作为方法参数,其原理是生成一个隐含的子类。枚举用于表示有限的固定数量的值,常用于系统配置或switch语句中。泛型则用来在编译时增强类型安全性,接收特定数据类型,包括泛型类、泛型接口和泛型方法。
14 0
|
5天前
|
存储 Java
java接口和内部类
java接口和内部类
|
5天前
|
Java 编译器
详解java各种内部类
详解java各种内部类
|
5天前
|
设计模式 Java
JAVA内部类
JAVA内部类
11 1