天天看點

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 的接口和内部類就容易了解多了。

雖然這些特性本身是相當直覺的,但是就像多态機制一樣,這些特性的使用應該是設計階段考慮的問題。随着時間的推移,讀者将能夠更好地識别什麼情況下應該使用接口,什麼情況使用内部類,或者兩者同時使用。

但此時,至少應該已經完全了解了它們的文法和語義。