1. 程式人生 > 實用技巧 >CORE JAVA 第五章 繼承

CORE JAVA 第五章 繼承

第5章 繼承

​ 利用繼承,人們可以基於已存在的類構造一個新類。繼承已存在的類就是複用(繼承)這些類的方法和域。在此基礎上,還可以新增一些新的方法和域,以滿足新的需求。

​ 此外,本章還闡述了反射(reflection)的概念。反射是指在程式執行期間發現更多的類及其屬性的能力。

5.1 類、超類和子類

​ “is-a”關係是繼承的一個明顯特徵。

5.1.1 定義子類

​ 關鍵字extends表示繼承。

public class Manager extends Employee
{
    //新增方法和域
}

​ 關鍵字extends表明正在構造的新類派生於一個已存在的類。已存在的類稱為超類(superclass)

基類(base class)父類(parent class);新類稱為子類(subclass)、派生類(derived class)或孩子類(child class)

​ 子類比超類擁有的功能更加豐富

​ Employee類的物件不能使用Manager類中新增的方法,但屬於Manager類的物件可以使用Employee類的方法,這是因為Manager類自動地繼承了超類中的方法,還繼承了超類中的域。

​ 在通過擴充套件超類定義子類的時候,僅需要指出子類與超類的不同之處。因此在設計類的時候,應該將通用的方法放在超類中,而將具有特殊用途的方法放在子類中。

5.1.2 覆蓋方法

​ 超類中有些方法對子類並不一定適用,為此,需要提供一個新的方法來覆蓋超類中的這個方法。

​ 如果在子類中定義一個方法,其名稱、返回型別及引數簽名正好與父類中某個方法的名稱、返回型別及引數簽名相匹配,那麼可以說,子類的方法覆蓋了父類的方法。

public class Manager extends Employee
{
    public double getSalary()
    {
        return salary + bonus;//won't work
    }
}

​ 但是,這個方法並不能執行,因為子類Manager的getSalary方法不能夠直接地訪問超類的私有域。也就是說,儘管每個Manager物件都擁有一個名為salary的域,但在Manager類的getSalary方法中並不能夠直接地訪問salary域。只有Employee類的方法才能夠訪問私有部分。如果Manager類的方法一定要訪問私有域

,就必須藉助於公有的介面,Employee類中的公有方法getSalary正是這樣一個介面。

​ 將對salary域的訪問替換成呼叫getSalary方法:

public double getSalary()
{
    double baseSalary = getSalary(); //still won't work
    return baseSalary + bonus;
}

​ 上面這段程式碼仍然不能執行,因為Manager類也有一個getSalary方法,所以這條語句將會導致無限次地呼叫自己,直到整個程式崩潰為止。

​ 這裡需要指出:我們希望呼叫超類Employee中的getSalary方法,而不是當前類的這個方法。為此,我們可以使用關鍵字super解決這個問題:

super.getSalary()

​ 上述語句呼叫的是Employee類中的getSalary方法。下面是Manager類中getSalary方法的正確書寫格式:

public double getSalary()
{
    double baseSalary = super.getSalary();
    return baseSalary + bonus;
}

​ super不是一個物件的引用,不能將super賦給另一個物件變數,它只是一個指示編譯器呼叫超類方法的特殊關鍵字。

​ 在子類中可以增加域、增加方法或覆蓋超類的方法,然而絕對不能刪除繼承的任何域和方法。

5.1.3 子類構造器

​ 提供一個構造器:

public Manager(String name, double salary, int year, int month, int day)
{
    super(name, salary, year, month, day);
    bonus = 0;
}

​ 這裡的關鍵字super具有不同的含義。語句

super(n, s, year, month, day)

呼叫超類Employee中含有n、s、year、month、和day引數的構造器的簡寫形式。

​ 由於Manager類的構造器不能訪問Employee類的私有域,所以必須利用Employee類的構造器對這部分私有域進行初始化,我們可以通過super實現對超類構造器的呼叫。使用super呼叫構造器的語句必須是子類構造器的第一條語句

如果子類的構造器沒有顯式地呼叫超類的構造器,則將自動地呼叫超類預設(沒有引數)的構造器。如果超類沒有不帶引數的構造器,並且在子類的構造器中又沒有顯式地呼叫超類的其他構造器,則Java編譯器將報告錯誤。

​ 回憶一下:

  • 關鍵字this有兩個用途:
  1. 引用隱式引數
  2. 呼叫該類其他的構造器
  • 關鍵字super也有兩個用途:
  1. 呼叫超類的方法
  2. 呼叫超類的構造器

​ 在呼叫構造器時,這兩個關鍵字的使用方式很相似。呼叫構造器的語句只能作為另一個構造器的第一條語句出現。構造引數既可以傳遞給本類(this)的其他構造器,也可以傳遞給超類(super)的構造器。

​ 一個物件變數可以指示多種實際型別的現象被稱為多型(polymorphism)。在執行時能夠自動地選擇呼叫哪個方法的現象稱為動態繫結(dynamic binding)。

​ 例如:

Employee[] staff = new Employee[3];
Manager boss = new Manager();
staff[0] = boss;
staff[1] = new Employee();
staff[2] = new Employee();
for(Employee e : staff)
    System.out.println(e.getName() + " " + e.getSalary());

​ 以上中e.getSalary()呼叫能夠確定應該執行哪個getSalary方法。儘管這裡將e宣告為Employee型別,但實際上e即可以引用Employee型別的物件,也可以引用Manager型別的物件。虛擬機器知道e實際引用的物件型別,因此能夠正確地呼叫相應的方法。

5.1.4 繼承層次

​ 繼承並不限於一個層次。例如,可以由Manager類派生Executive類。由一個公共超類派生出來的所有類的集合被稱為繼承層次。在繼承層次中,從某個特定的類到其祖先的路徑被稱為該類的繼承鏈。

​ 通常,一個祖先類可以擁有多個子孫繼承鏈。

​ Java不支援多繼承。

5.1.5 多型

​ 多型是同一個行為具有多個不同表現形式或形態的能力。

​ 多型就是同一個介面,使用不同的例項而執行不同操作。多型性是物件多種表現形式的體現。

​ 多型存在的三個必要條件:

  • 繼承

  • 重寫

  • 父類引用指向子類物件

    ​ 多型的實現方式:

    ​ 方式一:重寫

    ​ 方式二:介面

    ​ 方式三:抽象類和抽象方法

父類引用指向子類物件,子類重寫了父類的方法,呼叫父類的方法,實際呼叫的是子類重寫了的父類的該方法。

​ “is-a”規則:

  • 子類的每個物件也是超類的物件。

  • 程式中出現超類物件的任何地方都可以用子類物件置換。

    例如,可以將一個子類的物件賦給超類變數。

    Employee e;
    e = new Employee();
    e = new Manager();
    

    ​ 在Java中,物件變數是多型的。一個Employee變數既可以引用一個Employee類物件,也可以引用一個Employee類的任何一個子類的物件。

    ​ 然而,不能將一個超類的引用賦給子類變數。

    Manager m = staff[i]; //Error
    

    ​ 在Java中,子類陣列的引用可以轉換成超類陣列的引用,而不需要採用強制型別轉換。

    Manager[] managers = new Manager[10];
    Employee[] staff = managers; //OK
    

    ​ 這樣做可能會產生問題,應儘量避免。所有陣列都要牢記建立他們的元素型別,並負責監督僅將型別相容的引用儲存到陣列中。

5.1.6 理解方法呼叫

​ 弄清楚如何在物件上應用方法呼叫非常重要。下面假設要呼叫x.f(args),隱式引數x宣告為類C的一個物件。下面是呼叫過程的詳細描述:

  1. 編譯器檢視物件的宣告型別和方法名。編譯器將會一一列舉所有C類中名為f的方法和其超類中訪問屬性為public且名為f的方法。
  2. 編譯器將檢視呼叫方法時提供的引數型別。如果存在一個與提供的引數型別完全匹配,就選擇這個方法。這個過程被稱為過載解析。
  3. 如果是private方法、static方法、final方法或者構造器,那麼編譯器可以準確地知道應該呼叫哪個方法,我們將這種呼叫方式稱為靜態繫結。與此對應的是,呼叫的方法依賴於隱式引數的實際型別,並且在執行時實現動態繫結。在示例中,編譯器採用動態繫結的方式生成一條呼叫f(String)的指令。
  4. 當程式執行,並且採用動態繫結呼叫方法時,虛擬機器一定呼叫與x所引用物件的實際型別最合適的那個類的方法。
    每次呼叫方法都要進行搜尋,時間開銷相當大。因此,虛擬機器預先為每個類建立了一個方法表,其中列出了所有方法的簽名和實際呼叫的方法。在真正呼叫方法的時候,虛擬機器僅查表就行了。

​ 動態繫結有一個非常重要的特性:無需對現存的程式碼進行修改,就可以對程式進行擴充套件。

​ 在覆蓋一個方法的時候,子類方法不能低於超類方法的可見性。特別是,如果超類方法是public,子類方法一定要宣告為public。

5.1.7 阻止繼承:final類和方法

​ 不允許擴充套件的類被稱為final類,定義時使用final修飾符。

public final class Executive extends Manager
{
    ……
}

​ 類中的特定方法也可以被宣告為final,這樣做子類就不能覆蓋這個方法。final類中的所有方法自動的成為final方法。

public class Employee
{
    public final String getName()
    {
        return name;
    }
}

​ 域也可以被宣告為final,構造物件之後它們的值就不允許被改變。不過,如果將一個類宣告為final,只有其中的方法自動地成為final,而不包括域。

​ 將方法或類宣告為final主要目的是:確保它們不會在子類中改變語義。

​ 如果一個方法沒有被覆蓋並且很短,編譯器就能夠對它進行優化處理,這個過程稱為內聯,例如,內聯呼叫e.getName()將被替換為訪問e.name域。

5.1.8 強制型別轉換

​ 將表示式x的值轉換成整數型別,捨棄了小數部分:

double x = 3.405;
int nx = (int) x;

​ 有時候,也可能需要將某個類的物件引用轉換成另外一個類的物件引用。

​ 物件引用的轉換語法:用一對圓括號將目標類名括起來,並放置在需要轉換的物件引用之前就可以了。

Manager boss = (Manager) staff[0];

​ 進行型別轉換的唯一原因是:在暫時忽視物件的實際型別之後,使用物件的全部功能。

​ 將一個值存入變數時,編譯器將檢查是否允許該操作。將一個子類的引用賦給一個超類變數,編譯器是允許的。但將一個超類的引用賦給一個子類變數,必須進行型別轉換,這樣才能夠通過執行時的檢查。

Employee em = boss;//	OK

Manager ma = staff[1];
//Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
//	Type mismatch: cannot convert from Employee to Manager
//有Error

Manager ma = (Manager) staff[1];
//Exception in thread "main" java.lang.ClassCastException: a.Employee cannot be cast to a.Manager
//沒有Error了,但是仍有異常

只是能通過檢查,但不一定正確。可能丟擲異常,如下;

​ 如果試圖在繼承鏈上進行向下(超類轉換成子類)的型別轉換,並且謊報有關物件包含的內容,例如:

Manager boss = (Manager) staff[1]; //不可以
//Exception in thread "main" java.lang.ClassCastException: a.Employee cannot be cast to a.Manager

​ Java執行時系統將報告這個錯誤,併產生一個ClassCastException異常。如果沒有捕獲這個異常,那麼程式就會終止。

​ instanceof 是 Java 的保留關鍵字。它的作用是測試它左邊的物件是否是它右邊的類的例項,返回 boolean 的資料型別。

Employee[] staff = new Employee[3];
Manager boss = new Manager();
staff[1] = new Employee();

staff[1] instanceof Employee;//true
staff[1] instanceof Manager;//false

boss instanceof Employee;//true
boss instanceof Manager;//true

​ 良好的程式設計習慣:在進行型別轉換之前,先檢視一下是否能夠成功地轉換。使用instanceof操作符就可以實現。 例如:

if (staff[1] instanceof Manager)
{
    boss = (Manager) staff[1];
    ……
}

​ 如果這個型別轉換不可能成功,編譯器就不會進行這個轉換。例如:

String c = (String) staff[1];

​ 將會產生編譯錯誤,因為String不是Employee的子類。

​ 綜上所述:

  • 只能在繼承層次內進行型別轉換。
  • 在將超類轉換成子類之前,應該使用instanceof進行檢查。

​ 實際上,通過型別轉換調整物件的型別並不是一種好的做法。大多數情況並不需要將Employee物件轉換成Manager物件,只有在使用Manager中特有的方法時才需要進行型別轉換,但這時候超類的設計可能就不合理了,重新設計超類才是正確選擇。

​ 在一般情況下,應該儘量少用型別轉換和instancof運算子。

5.1.9 抽象類

​ 從某種角度看,祖先類更加通用,人們只將它作為派生其他類的基類,而不作為想使用的特定的例項類。

​ 使用abstract關鍵字,這樣就完全不需要實現這個方法了。

public abstract String getDescription();
// no implementation required

​ 包含一個或多個抽象方法的類本身必須被宣告為抽象的。

public abstract class Person
{
    ……
    public abstract String getDescription();
}

​ 除了抽象方法之外,抽象類還可以包含具體資料和具體方法。

​ 抽象方法充當著佔位的角色,它們的具體實現在子類中。

​ 擴充套件抽象類有兩種選擇:

  1. 在抽象類中定義部分抽象類方法或不定義抽象類方法,這樣就必須將子類也標記為抽象類。
  2. 定義全部的抽象方法,這樣子類就不是抽象的了。

​ 類即使不含抽象方法,也可以將類宣告為抽象類。

​ 抽象類不能被例項化,不能建立抽象類的物件。但可以建立一個具體子類的物件。

​ 可以定義一個抽象類的物件變數,但是它只能引用非抽象子類的物件。

Person p = new Student();

5.1.10 受保護訪問

​ 在有些時候,人們希望超類中的某些方法允許被子類訪問,或允許子類的方法訪問超類的某個域。為此,需要將這些方法或域宣告為protected。

​ 例如,如果將超類Employee中的hireDay宣告為protected,Manager中的方法就可以直接地訪問它。不過,Manager類中的方法只能夠訪問Manager物件中的hireDay域,而不能訪問其他Employee物件中的這個域。

​ 歸納一下Java用於控制可見性的4個訪問修飾符:

  1. 僅對本類可見——private。
  2. 對所有類可見——public。
  3. 對本包和所有子類可見——protected。
  4. 對本包可見——預設,不需要修飾符。

5.2 Object:所有類的超類

​ Object類是Java中所有類的始祖,在Java中每個類都是由它擴充套件而來。但是並不需要這麼寫:

public class Employee extends Object

​ 如果沒有明確的指出超類,Object就被認為是這個類的超類。

​ 可以使用Object型別的變數引用任何型別的物件:

Object obj = new Employee();

​ Object型別的變數只能用於作為各種值的通用持有者。要想對其中的內容進行具體的操作,還需要清楚物件的原始型別,並進行相應的型別轉換:

Employee e = (Employee) obj;

​ 在Java中,只有基本型別(數值、字元、布林型別的值)不是物件。

​ 所有的陣列型別,不管是物件陣列還是基本型別的陣列都擴充套件了Object類。

Employee[] staff = new Employee[10];
obj = staff;//OK
obj = new int[10];//OK

5.2.1 equals方法

​ Object類中的equals方法用於檢測一個物件是否等於另外一個物件。在Object類中,這個方法將判斷兩個物件是否具有相同的引用(指向同一塊儲存區域)。如果兩個物件具有相同的引用,它們一定是相等的。

​ 然而,對於多數類來說,這種判斷並沒有什麼意義。經常需要檢測兩個物件狀態的相等性,如果兩個物件的狀態相等,就認為這兩個物件是相等的。

​ 例如,如果兩個僱員物件的姓名、薪水和僱用日期都一樣,就認為他們是相等的。

​ 利用下面這個例項演示equals方法的實現機制:

public class Employee
{
    ……
    public boolean equals(Object otherObject)
    {
        if(this == otherObject)  return true;
        
        if(otherObject == null)	return false;
        
        if(getClass() != otherObject.getClass())
            return false;
        
        Employee other = (Employee) otherObject;
        
        return name.equals(other.name)
            && salary == other.salary
            && hireDay.equals(other.hireDay);
    }
}

​ getClass方法將返回一個物件所屬的類。在檢測中,只有在兩個物件屬於同一個類時,才有可能相等。

​ 為了防備name或hireDay可能為null的情況,需要使用Objects.equals方法。

​ Objects.equals(a,b):如果兩個引數都為null,返回true;其中一個為null,返回false。否則,呼叫a.equals(b)。

​ 最後一條語句要改寫為:

return Objects.equals(name, other.name)
    && salary == other.salary
    && Objects.equals(hireDay, other.hireDay);

​ 在子類中定義equals方法時,首先呼叫超類的equals。如果檢測失敗,物件就不可能相等。如果超類中的域都相等,就需要比較子類中的例項域。

public class Manager extends Employee
{
    ……
    public boolean equals(Object otherObject)
    {
        if(!super.equals(otherObject)) return false;
        
        Manager other = (Manager) otherObject;
        return bonus == other.bonus;
    }
}

5.2.2 相等測試與繼承

​ 如果隱式和顯式的引數不屬於同一個類,equals方法將如何處理呢?

​ 在前面的例子中,如果發現類不匹配,equals方法就返回false。

​ 但是,許多程式設計師喜歡使用instanceof進行檢測:

if (!(othenObject instanceof Employee))	return false;

這樣做不但沒有解決otherObject是子類的情況,並且還有可能會招致一些麻煩。建議不要使用這種處理方式。

​ Java語言規範要求equals方法具有下面的特性:

  1. 自反性:對於任何非空引用x,x.equals(x)應該返回true。
  2. 對稱性:對於任何引用x和y,當且僅當y.equals(x)返回true,x.equals(y)也應該返回true。
  3. 傳遞性:對於任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,x.equals(z)也應該返回true。
  4. 一致性:如果x和y引用的物件沒有發生變化,反覆呼叫x.equals(y)應該返回同樣的結果。
  5. 對於任何非空引用x,x.equals(null)應該返回false。

​ 關於對稱性,當引數不屬於同一個類時:

  • 如果子類能夠擁有自己的相等概念,則對稱性需求將強制採用getClass進行檢測。(Employee與Manager,如果使用instanceof,則不滿足對稱性
  • 如果由超類決定相等的概念,那麼就可以使用instanceof進行檢測,這樣可以在不同子類的物件之間進行相等的比較。(AbstractSet的equals方法檢測兩個集合是否有相同的元素。AbstractSet類的兩個具體子類:TreeSet和HashSet分別使用不同的演算法實現查詢集合元素)

​ 在僱員和經理的例子中,只要對應的域相等,就認為兩個物件相等。如果兩個Manager物件對應的姓名、薪水和僱用日期均相等,而獎金不相等,就認為他們是不相同的,因此可以使用getClass檢測。

​ 但是假設使用僱員的ID作為相等的檢測標準,並且這個相等的概念適用於所有的子類,就可以使用instanceof進行檢測,並應該將Employee.equals宣告為final。

​ 下面給出編寫一個完美的equals方法的建議:

  1. 顯式引數命名為otherObject,稍後需要將它轉換成另一個叫做other的變數。
  2. 檢測this與otherObject是否引用同一個物件:
if (this == otherObject)	return true;
  1. 檢測otherObject是否為null,如果為null,返回false。

    if (otherObject == null)	return false;
    
  2. 比較this與otherObject是否屬於同一個類。如果equals的語義在每個子類中有所更改,就使用getClass檢測:

    if (getClass() != otherObject.getClass()) return false;
    

    如果所有的子類都擁有統一的語義,就使用instanceof檢測:

    if (!(otherObject instanceof ClassName))	return false;
    
  3. 將otherObject轉換為相應的類型別變數:

    ClassName other = (ClassName) otherObject;
    
  4. 現在開始對所有需要比較的域進行比較了。使用 == 比較基本型別域,使用equals比較物件域。如果所有的域都匹配,就返回true;否則返回false。

    return field1 == other.field1
        && Objects.equals(field2,other.field2)
        && ……;
    

如果在子類中重新定義equals,就要在其中包含呼叫super.equals(other)。

​ 對於陣列型別的域,可以使用靜態的Arrays.equals方法檢測相應的陣列元素是否相等。

​ 可以使用@Override對覆蓋超類的方法進行標記。如果出現了錯誤,並且正在定義一個新方法,編譯器就會給出錯誤報告。

5.2.3 hashCode方法

​ 雜湊碼(hash code)是由物件匯出的一個整型值。雜湊碼是沒有規律的。

​ String類使用下列演算法計算雜湊碼:

int hash = 0;
for (int i = 0; i < length(); i++)
    hash = 31 * hash + chaAr(i);

​ 由於hashCode方法定義在Object類中,因此每個物件都有一個預設的雜湊碼,其值為物件的儲存地址。例如:

String s = "OK";
StringBuilder sb = new StringBuilder(s);
//s.hashCode():2556	
//sb.hashCode():20526976

String t = new String("OK");
StringBuilder tb = new StringBuilder(t);
//t.hashCode():2556
//tb.hashCode():20527144

​ 字串s和t有相同的雜湊碼,這是因為字串的雜湊碼是由內容匯出的。而字串緩衝sb與tb卻有著不同的雜湊碼,這是因為在StringBuffer類中沒有定義hashCode方法,它的雜湊碼是由Object類的預設hashCode方法匯出的物件儲存地址。

​ 如果重新定義equals方法,就必須重新定義hashCode方法,以便使用者可以將物件插入到散列表中。

​ hashCode方法應該返回一個整型數值,併合理地組合例項域的雜湊碼,以便能夠讓各個不同的物件產生的雜湊碼更加均勻。

例如,下面是Employee類的hashCode方法:

public class Employee
{
    public int hashCode()
    {
        return 7 * name.hashCode()
            + 11 * new Double(salary).hashCode()
            + 13 * hireDay.hashCode();
    }
}

​ 改進:

​ 使用null安全的Objects.hashCode方法:如果引數為null,這個方法會返回0,否則返回對引數呼叫hashCode的結果。

另外,使用靜態方法Double.hashCode方法避免建立Double物件:

public int hashCode()
{
    return 7 * Objects.hashCode(name)
        + 11 * Double.hashCode(salary)
        + 13 * Objects.hashCode(hireDay);
}

​ 還有更好的做法,需要組合多個雜湊值時,可以呼叫Objects.hash並提供多個引數。這個方法會對各個引數呼叫Objects.hashCode,並組合這些雜湊值。

public int hashCode()
{
    return Objects.hash(name, salary, hireDay);
}

​ equals與hashCode的定義必須一致:如果x.equals(y)返回true,那麼x.hashCode()就必須與y.hashCode()具有相同的值。例如,如果用定義的Employee.equals比較僱員的ID,那麼hashCode方法就需要雜湊ID,而不是僱員的姓名或儲存地址。

​ 如果存在陣列型別的域,可以使用靜態的Arrays.hashCode方法計算一個雜湊碼,這個雜湊碼由陣列元素的雜湊碼組成。

5.2.4 toString方法

​ toString方法返回表示物件值的字串。絕大多數的toString方法都遵循這樣的格式:類的名字,隨後是一對方括號括起來的值域。

​ 最好通過呼叫getClass().getName()獲得類名的字串,而不要將類名硬加到toString方法中。

​ 設計子類的程式設計師也應該定義自己的toString方法,並將子類域的描述新增進去。如果超類使用了getClass().getName(),那麼子類只要呼叫super.toString()就可以了。

​ 隨處可見toString方法的主要原因是:只要物件與一個字串通過操作符“+”連線起來,Java編譯就會自動地呼叫toString方法,以便獲得這個物件的字串描述。例如:

Point p = new Point(10, 20);
String message = "The current position is " + p;
	// automatically invokes p.toString()

​ 在呼叫x.toString()的地方可以用""+x替代。這條語句將一個空串與x的字串表示相連線。這裡的x就是x.toString()。如果x是基本型別,這條語句照樣能夠執行。

​ 如果x是任意一個物件,並呼叫:

System.out.println(x);

println方法就會直接地呼叫x.toString(),並列印輸出得到的字串。

​ Object類定義了toString方法,用來列印輸出物件所屬的類名和雜湊碼。例如,呼叫

System.out.println(System.out)

將輸出下列內容:

java.io.PrintStream@2f6684

之所以得到這樣的結果是因為PrintStream類的設計者沒有覆蓋toString方法。

​ 陣列繼承了Object類的toString方法,陣列型別將按照舊的格式列印。例如:

int[] luckyNumbers = {2, 3, 5, 7, 11, 13};
String s = "" + luckyNumbers;
// s = [I@7852e922 字首[I表明是一個整型陣列。

​ 修正的方式是呼叫靜態方法Arrays.toString()。程式碼:

String s = Arrays.toString(luckyNumbers);

將生成字串“[2,3,5,7,11,13]”。

​ 列印多維陣列,使用Arrays.deepToString方法。

​ toString方法是一種非常有用的除錯工具。在標準類庫中,許多類都定義了toString方法,以便使用者能夠獲得一些有關物件狀態的必要資訊。像下面這樣顯示除錯資訊非常有益:

Logger.global.info("Current position = " + position);//第七章

5.3 泛型陣列列表

​ 在Java中,允許在執行時確定陣列的大小:

int actualSize = …… ;
Employee[] staff = new Employee[actualSize];

​ 當然 ,這段程式碼沒有完全解決執行時動態更改陣列的問題。使用ArrayList類,在新增或刪除元素時,具有自動調節陣列容量的功能,而不需要為此編寫任何程式碼。

​ ArrayList是一個採用型別引數泛型類。下面宣告和構造一個儲存Employee物件的陣列列表:

ArrayList<Employee> staff = new ArrayList<Employee>();

//在Java SE 7中,可以省去右邊的引數型別:
ArrayList<Employee> staff = new ArrayList<>();

​ 使用add方法可以將元素新增到陣列列表中。

​ 陣列列表管理著物件引用的一個內部陣列。最終,陣列的全部空間有可能被用盡。如果呼叫add且內部陣列已經滿了,陣列列表就將自動地建立一個更大的陣列,並將所有的物件從較小的陣列拷貝到較大的陣列中。

​ 如果已經清楚或能夠估計出陣列可能儲存的元素數量,就可以在填充陣列之前呼叫ensureCapacity方法:

staff.ensureCapacity(100);

這個方法呼叫將分配一個包含100個物件的內部陣列。然後呼叫100次add,而不用重新分配空間。

​ 另外,還可以把初始容量傳遞給ArrayList構造器:

ArrayList<Employee> staff = new ArrayList<>(100);

​ 陣列列表的容量與陣列的大小有一個非常重要的區別。如果為陣列分配100個元素的儲存空間,陣列就有100個空位置可以使用。而容量為100個元素的陣列列表只是擁有儲存100個元素的潛力(實際上,重新分配空間的話,將會超過100),但是在最初,甚至完成初始化構造之後,陣列列表根本就不含有任何元素。

​ size方法返回陣列列表中包含的實際元素數目。

​ 一旦能夠確認陣列列表的大小不再發生變化,就可以呼叫trimToSize方法,將儲存區域的大小調整為當前元素數量所需要的儲存空間數目。垃圾回收器將回收多餘的儲存空間。

5.3.1 訪問陣列列表元素

​ 設定第i個元素:

staff.set(i, harry);
//等價於a[i] = harry;

只有i小於或等於陣列列表的大小時,才能夠呼叫list.set(i,x)。例如,下面這段程式碼是錯誤的:

ArrayList<Employee> list = new ArrayList<>(100);// capacity 100, size 0
list.set(0, x);//no element 0 yet

使用add方法為陣列新增新元素,而不要使用set方法,它只能替換陣列中以及存在的元素內容。

​ 使用下列格式獲得陣列列表的元素:

Employee e = staff.get(i);
//等價於Employee e = a[i];

​ 下面這個技巧一舉兩得,既可以靈活地擴充套件陣列,又可以方便地訪問陣列元素。

​ 首先,建立一個數組列表,並新增所有的元素。

ArrayList<X> list = new ArrayList<>();
while(……)
{
    x = ……;
    list.add(x);
}

執行完上述操作後,使用toArray方法將陣列列表元素拷貝到一個數組中。

X[] a = new X[list.size()];
list.toArray(a);

​ 使用帶索引引數的add方法,可以在陣列列表的中間插入元素。

int n = staff.size() / 2;
staff.add(n, e);

位於n之後的所有元素都要向後移動一個位置。

​ 同樣地,可以從陣列列表中間刪除一個元素。

Employee e = staff.remove(n);

位於這個位置之後的元素都向前移動一個位置,並且陣列的大小減1。

​ 對陣列列表進行插入和刪除元素的操作效率比較低。

​ 可以使用for each迴圈遍歷陣列列表:

for(Employee e : staff)
    	do something with e 

5.3.2 型別化與原始陣列列表的相容性

​ 在自己的程式碼中,可能更願意使用型別引數來增加安全性。這一節介紹如何與沒有使用型別引數的遺留程式碼互動操作。

​ 假設有下面這個遺留下來的類:

public class EmployeeDB
{
    public void update(ArrayList list){……}
    public ArrayList find(String query){……}
}

可以將一個型別化的陣列列表傳遞給update方法,而並不需要進行任何型別轉換。

ArrayList<Employee> staff = ……;
employeeDB.update(staff);

也可以將staff物件傳遞給update方法。

儘管編譯器沒有給出任何錯誤資訊或警告,但是這樣呼叫不太安全。在update方法中,新增到陣列列表中的元素可能不是Employee型別。在對這些元素進行檢索時就會出現異常。

​ 相反地,將一個原始ArrayList賦給一個型別化ArrayList會得到一個警告。

ArrayList<Employee> result = employeeDB.find(query);	// yields warning

使用型別轉換並不能避免出現警告。

ArrayList<Employee> result = (ArrayList<Employee>) employeeDB.find(query);    
// yields another warning
//會指出型別轉換有誤

​ 鑑於相容性的考慮,編譯器在對型別轉換進行檢查之後,如果沒有發現違反規則的現象,就將所有的型別化陣列列表轉換成原始ArrayList物件在程式執行時,所有的陣列列表都是一樣的,即沒有虛擬機器中的型別引數。因此,型別轉換(ArrayList)和(ArrayList)將執行相同的執行時檢查。

​ 在這種情形下,不必做什麼。只要在與遺留的程式碼進行交叉操作時,研究一下編譯器的警告性提示,並確保這些警告不會造成太嚴重的後果就行了。

​ 一旦能確保不會造成嚴重的後果,可以用@SuppressWarnings("unchecked")標註來標記這個變數能夠接受型別轉換,如下:

@SuppressWarnings("unchecked") ArrayList<Employee> result = 
    (ArrayList<Employee>) employeeDB.find(query);  // yields another warning

5.4 物件包裝器與自動裝箱

​ 有時,需要將int這樣的基本型別轉換為物件。所有的基本型別都有一個與之對應的類,稱為包裝器。這些物件包裝器類擁有很明顯的名字:Integer、Long、Float、Double、Short、Byte、Character、Void和Boolean(前6個類派生於公共的超類Number)。

​ 物件包裝器是不可變的,一旦構造了包裝器,就不允許更改包裝在其中的值。同時,物件包裝器還是final,因此不能定義它們的子類。

​ 陣列列表中的引數型別不允許是基本型別,可以使用Integer物件包裝器類。

ArrayList<Integer> list = new ArrayList<>();

​ 有一個特性,便於新增int型別的元素到ArrayList中。下面這個呼叫:

list.add(3);

將自動地變換成

list.add(Integer.valueOf(3));

這種變換被稱為自動裝箱

​ 相反地,將一個Integer物件賦給一個int值時,將會自動拆箱。編譯器將下列語句:

int n = list.get(i);

翻譯成:

int n = list.get(i).intValue();

​ 在算術表示式中也能夠自動地裝箱和拆箱。例如,可以將自增操作符應用於一個包裝器引用:

Integer n = 3;
n++;

編譯器將自動地插入一條物件拆箱的指令,然後進行自增運算,最後再將結果裝箱。

​ ==運算子也可以應用於物件包裝器物件,只不過檢測的是物件是否指向同一個儲存區域,因此,下面的比較通常不會成立:

Integer a = 1000;
Integer b = 1000;
if (a == b) ……

然而,Java實現卻有可能讓它成立。如果將經常出現的值包裝到同一個物件中,這種比較就有可能成立。解決這個問題的辦法是在兩個包裝器物件比較時呼叫equals方法。

​ 自動裝箱規範要求boolean、byte、char<=127,介於-128~127之間的short和int被包裝到固定的物件中。例如,如果在前面的例子中將a和b初始化為100,對它們進行比較的結果一定成立。

​ 由於包裝器類引用可以為null,所以自動裝箱有可能會丟擲一個NullPointerException異常。

​ 如果在一個條件表示式中混合使用Integer和Double型別,Integer值就會拆箱,提升為double,再裝箱為Double。

​ 裝箱和拆箱是編譯器認可的,而不是虛擬機器。編譯器在生成類的位元組碼時,插入必要的方法呼叫。虛擬機器只是執行這些位元組碼。

​ 使用數值物件包裝器還有另外一個好處。可以將某些基本方法放置在包裝器中,例如,將一個數字字串轉換成數值:

int x = Integer.parseInt(s);

這裡與Integer物件沒有任何關係,parseInt是一個靜態方法。

​ 包裝器類也不可以用來實現修改數值引數的方法,因為Integer物件是不可變的,包含在包裝器中的內容不會改變。

​ 如果想編寫一個修改數值引數值的方法,就需要使用在org.omg.CORBA包中定義的holder型別,包括IntHolder、BooleanHolder等。每個holder型別都包含一個公有域值,通過它可以訪問儲存在其中的值。

public static void triple(IntHolder x)
{
    x.value = 3 * x.value;
}

5.5 引數數量可變的方法

​ printf方法就是引數數量可變的方法。

​ printf方法的定義:

public class PrintStream
{
    public PrintStream printf(String fmt, Object... args) { return format(fmt, args); }
}

這裡的省略號...是Java程式碼的一部分,它表明這個方法可以接收任意數量的物件(除fmt引數之外)。

​ 實際上,printf方法接收兩個引數,一個是格式字串,另一個是Object[]陣列,其中儲存著所有的引數(如果呼叫者提供的是整型陣列或者其他基本型別的值,自動裝箱功能將把它們轉換成物件)。現在將掃描fmt字串,並將第i個格式說明符與args[i]的值匹配起來。

​ 換句話說,對於printf的實現者來說,Object...引數型別與Object[]完全一樣。

​ 編譯器需要對printf的每次呼叫進行轉換,以便將引數繫結到陣列上,並在必要的時候進行自動裝箱:

System.out.printf("%d %s",new Object[] { new Integer(n), "widgets"});

​ 使用者自己也可以定義可變引數的方法,並將引數指定為任意型別,甚至是基本型別。

​ 允許將一個數組傳遞給可變引數方法的最後一個引數。因此,可以將已經存在且最後一個引數是陣列的方法重新定義為可變引數的方法,而不會破壞任何已經存在的程式碼。

5.6 列舉類

​ Java 列舉是一個特殊的類,一般表示一組常量。使用 enum 關鍵字來定義,各個常量使用逗號 , 來分割。

​ 定義列舉型別的例子:

public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE };

實際上,這個宣告定義的型別是一個類,它剛好有4個例項,這些列舉成員預設都被 final、public, static 修飾,當使用列舉型別成員時,直接使用列舉名稱呼叫成員即可。在此儘量不要構造新物件。

​ 因此,在比較兩個列舉型別的值時,永遠不需要呼叫equals,而直接使用“==”就可以了。

​ 如果需要的話,可以在列舉型別中新增一些構造器、方法和域。構造器只是在構造列舉常量的時候被呼叫。

public enum Size
{
    SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
    //以上是列舉的成員,必須先定義,而且使用分號結束
    
    private String abbreviation;
    
    //建構函式只能使用 private 訪問修飾符
    private Size(String abbreviation) {this.abbreviation = abbreviation;}
    public String getAbbreviation() {return abbreviation;}
}

​ 所有的列舉型別都是Enum類的子類,它們繼承了這個類的許多方法。其中最有用的一個是toString,這個方法能夠返回列舉常量名。例如,Size.SMALL.toString()將返回字串“SMALL”。

​ toString的逆方法是靜態方法valueOf。例如,語句:

Size s = Enum.valueOf(Size.class, "SMALL");
// 將s設定成Size.SMALL.

​ 每個列舉型別都有一個靜態的values方法,他將返回一個包含全部列舉值的陣列。

​ ordinal方法返回enum宣告中列舉常量的位置,從0開始計數。

5.7 反射

​ 反射庫提供了一個非常豐富且精心設計的工具集,以便編寫能夠動態操縱Java程式碼的程式。使用反射,在設計或執行中新增新類時,能夠快速地應用開發工具動態地查詢新新增類的能力。

​ 能夠分析類能力的程式稱為反射。反射機制可以用來:

  • 在執行時分析類的能力。
  • 在執行時檢視物件,例如,編寫一個toString方法供所有類使用。
  • 實現通用的陣列操作程式碼。
  • 利用Method物件。

5.7.1 Class類

​ 在程式執行期間,Java執行時系統始終為所有的物件維護一個被稱為執行時的型別標識。這個資訊跟蹤著每個物件所屬的類。虛擬機器利用執行時型別資訊選擇相應的方法執行。

​ 可以通過專門的Java類訪問這些資訊。儲存這些資訊的類被稱為Class,Object類中的getClass()方法會返回一個Class型別的例項。

Employee e;
……
Class c1 = e.getClass();

​ 如同用一個Employee物件表示一個特定的僱員屬性一樣,一個Class物件將表示一個特定類的屬性。最常用的Class方法是getName。這個方法將返回類的名字。如果類在一個包裡,包的名字也作為類名的一部分。

​ 靜態方法forName獲得類名對應的Class物件。

String className = "java.util.Random";
Class c1 = Class.forName(className);

如果類名儲存在字串中,並可在執行中改變,就可以使用這個方法。當然,這個方法只有在className是類名或介面名時才能夠執行。無論何時使用這個方法,都應該提供一個異常處理器。

​ 獲得Class類物件的第三種方法:如果T是任意的Java型別(或void關鍵字),T.class將代表匹配的類物件。例如:

Class cl1 = java.util.Random.class;
Class cl2 = int.class;
Class cl3 = Double[].class;

注意:一個Class物件實際上表示的是一個型別,而這個型別未必一定是一種類。例如,int不是類,但Int.class是一個Class型別的物件。

​ Class類實際上是一個泛型類。

​ 虛擬機器為每個型別管理一個Class物件。因此,可以利用==運算子實現兩個類物件比較的操作。

​ newInstance()方法,可以用來動態地建立一個類的例項。

e.getClass().newInstance();

建立了一個與e具有相同類型別的例項。newInstance方法呼叫預設的構造器初始化新建立的物件。

​ 將forName與newInstance配合起來使用,可以根據儲存在字串中的類名建立一個物件。

String s = "java.util.Random";
Object m = Class.forName(s).newInstance();

註釋:如果需要以這種方式向希望按名稱建立的類的構造器提供引數,就不要使用上面那條語句,而必須使用Construtor類中的newInstance方法。

5.7.2 捕獲異常

​ 當程式執行過程中發生錯誤時,就會“丟擲異常”。丟擲異常比終止程式靈活得多,因為可以提供一個“捕獲”異常的處理器對異常情況進行處理。

​ 如果沒有提供處理器,程式就會終止,並在控制檯上列印一條資訊,其中給出了異常的型別。

​ 異常有兩種型別:未檢查異常和已檢查異常。對於已檢查異常,編譯器將會檢查是否提供了處理器。

​ 最簡單的處理器:

將可能丟擲已檢查異常的一個或多個方法呼叫程式碼放在try塊中,然後在catch子句中提供處理器程式碼。

try
{
    //statements that might throw exceptions
}
catch (Exception e)
{
   // handler action
}

如果try塊中沒有丟擲任何異常,那麼會跳過catch子句的處理器程式碼。

​ 對於已檢查異常,只需要提供一個異常處理器。可以很容易地發現會丟擲已檢查異常的方法。如果呼叫了一個丟擲已檢查異常的方法,而又沒有提供處理器,編譯器就會給出錯誤的報告。

5.7.3 利用反射分析類的能力

​ 反射機制最重要的內容:檢查類的結構。

​ 在java.lang.reflect包中有三個類Field、Method和Constructor分別用於描述類的域、方法和構造器。

  • 這三個類都有一個叫做getName的方法,用來返回專案的名稱。
  • Field類有一個getType方法,用來返回描述域所屬型別的Class物件。
  • Method和Constructor類有能夠報告引數型別的方法,Method類還有一個可以報告返回型別的方法。
  • 這三個類都有一個叫做getModifiers的方法,它將返回一個整型數值,用不同的位開關描述public和static這樣的修飾符使用情況。另外,還可以利用java.lang.reflcet包中的Modifier類的靜態方法分析getModifiers返回的整型數值。例如,可以使用Modifier類中的isPublic、isPrivate或isFinal判斷方法或構造器是否是public、private或final。

​ Class類中的:

  • getFields、getMethods和getConstructors方法將分別返回類提供的public域、方法和構造器陣列,其中包括超類的公有成員。

  • getDeclaredFields、getDeclareMethods和getDeclaredConstructors方法將分別返回類中宣告的全部域、方法和構造器,其中包括私有和受保護成員,但不包括超類的成員。

​ 程式清單5-13可以分析Java直譯器能夠載入的任何類,能夠列印一個類的全部資訊。輸入類名,輸出類中所有的方法和構造器的簽名,以及全部域名。

5.7.4 在執行時使用反射分析物件

​ 前一節講述瞭如何檢視任意物件的資料域名稱和型別:

  • 獲得對應的Class物件。
  • 通過Class物件呼叫getDeclaredFields。

本節將進一步檢視資料域的實際內容。利用反射機制,可以檢視在編譯時還不清楚的物件域。

​ 檢視物件域的關鍵方法是Field類中的get方法。如果f是一個Field型別的物件(例如,通過getDeclaredFields得到的物件),obj是某個包含f域的類的物件,f.get(obj)將返回一個物件,其值為obj域的當前值。

//get方法簽名
Object get(Object obj)
Employee harry = new Employee("Harry Hacker", 35000, 10, 1, 1989);
Class cl = harry.getClass();
	// the class object representing Employee
Field f = cl.getDeclaredFields("name");
	// the name field of the Employee class
Object v = f.get(harry);
	// the value of the name field of the harry object,i.e.,the String object "Harry Hacker"

但是,由於name是一個私有域,所以get方法將會丟擲一個IllegalAccessException。

​ 只有利用get方法才能得到可訪問域的值。除非擁有訪問許可權,否則Java安全機制只允許檢視任意物件有哪些域,而不允許讀取它們的值。

​ 反射機制的預設行為受限於Java的訪問控制。然而,如果一個Java程式沒有受到安全管理器的控制,就可以覆蓋訪問控制。為了達到這個目的,需要呼叫Field、Method或Constructor物件的setAccessible方法。例如:

f.setAccessible(true);	// now OK to call f.get(harry)

這個特性是為除錯、持久儲存和相似機制提供的。

​ 此外,反射機制可以自動地將基本型別域值打包到相應的物件包裝器中,如double->Double。

​ 呼叫f.set(obj, value)可以將obj物件的f域設定成新值。

​ 程式清單5-14objectAnalyzer顯示瞭如何編寫一個可供任意類使用的通用toString方法。先使用getDeclaredField、setAccessible和get方法獲得每個域的名字和值,遞迴呼叫toString方法,將每個值轉換成字串。

​ 還可以使用通用的toString方法實現自己類中的toString方法,如下:

public String toString()
{
    return new ObjectAnalyzer().toString(this);
}

5.7.5 使用反射編寫泛型陣列程式碼

​ java.lang.reflect包中的Array類允許動態地建立陣列。例如,將這個特性應用到Arrays類中的copyOf方法實現中,這個方法可以用於擴充套件已經填滿的陣列。

Employee[] a = new Employee[100];

a = Arrays.copyOf(a, 2 * a.length);

​ 如何編寫這樣一個通用的方法,能夠將Employee[]陣列轉換為Object[]陣列?

​ 第一次嘗試:

public static Object[] badCopyOf(Object[] a, int newLength)	// not useful
{
    Object[] newArray = new Object[newLength];
    System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));
    return newArray;
}

然而,在實際使用結果陣列時會遇到一個問題。這段程式碼返回的陣列型別是物件陣列(Object[])型別,這是因為使用下面這行程式碼建立的陣列:

new Object[newLength];

一個物件陣列不能轉換成僱員陣列。如果這樣做,則在執行時Java將會產生ClassCastException異常。

Java陣列會記住每個元素的型別,即建立陣列時new表示式中使用的元素型別。將一個Employee[]臨時的轉換成Object[]陣列,然後再把它轉換回來是可以的,但一個從開始就是Object[]的陣列卻永遠不能轉換成Employee[]陣列。

​ 為了編寫這類通用的陣列程式碼,需要能夠建立與原陣列型別相同的新陣列。

Object newArray = Array.newInstance(componentType, newLength);
//param:	componentType:陣列元素型別

為了能夠實際的執行,需要獲得新陣列的長度和元素型別。

  • 可以通過呼叫Array.getLength(a)獲得陣列的長度,也可以通過Array類的靜態getLength方法的返回值得到任意陣列的長度。

  • 獲得新陣列元素型別:

  1. 首先獲得a陣列的類物件
  2. 確認它是一個數組
  3. 使用Class類(只能定義表示陣列的類物件)的getComponentType方法確定陣列對應的型別。

程式碼:

public static Object goodCopyOf(Object a, int newLength)
{
    Class cl = a.getClass();
    if (!cl.isArray())	return null;
    Class componentType = cl.getComponentType();
    
    int length = Array.getLength(a);
    
    Object newArray = Array.newInstance(componentType, newLength);
    System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
    return newArray;
}

這個CopyOf方法可以用來擴充套件任意型別的陣列,而不僅是物件陣列。

int[] a = {1, 2, 3, 4, 5};
a = (int[]) goodCopyOf(a, 10);

為了能夠實現上述操作,應該將goodCopyOf的引數宣告為Object型別,而不要宣告為物件型陣列(Object[])。整型陣列型別int[]可以被轉換成Object,但不能轉換成物件陣列。

5.7.6 呼叫任意方法

​ 反射機制允許呼叫任意方法。可以使用Method物件實現C語言中函式指標的所有操作。

​ 與Field類的get方法檢視物件域的過程類似,在Method類中有一個invoke方法,允許呼叫包裝在當前Method物件中的方法:

Object invoke(Object obj, Object... args)

第一個引數是隱式引數,其餘的物件提供了顯示引數。

對於靜態方法,第一個引數可以被忽略,即可以將它設定為null。

​ 例如,假設用ml代表Employee類的getName方法,下面這條語句顯示瞭如何呼叫這個方法:

String n = (String) ml.invoke(harry);

​ 如果返回型別是基本型別,invoke方法會返回其包裝器型別。

​ 如何得到Method物件呢?

  • 可以通過呼叫getDeclareMethods方法,然後對返回的Method物件陣列進行查詢,知道發現想要的方法為止。
  • 也可以呼叫Class類中的getMethod方法得到想要的方法。
//getMethod方法簽名:
Method getMethod(String name, Class... parameterTypes)

下面說明了如何獲得Employee類的getName方法和raiseSalary方法的方法指標:

Method m1 = Employee.class.getMethod("getName");
Method m2 = Employee.class.getMethod("raiseSalary", double.class);

​ 注意:invoke的引數和返回值必須是Object型別的,這就意味著必須進行多次的型別轉換。這樣做將會使編譯器錯過檢查程式碼的機會。因此,等到測試階段才能發現這些錯誤,找到並改正它們將會更加困難。不僅如此,使用反射獲得方法指標的程式碼要比僅僅直接呼叫方法明顯慢一些。

​ 有鑑於此,建議僅在必要的時候才使用Method物件,而最好使用介面或lambda表示式。

5.8 繼承的設計技巧

  1. 將公共操作和域放在超類
  2. 不要使用受保護的域

protected機制並不能夠帶來更好的保護:

  • 子類集合是無限制的,任何一個人都能夠由某個類派生一個子類,並編寫程式碼以直接訪問protected的例項域,從而破壞了封裝性。
  • 在同一個包中的所有類都可以訪問protected域,而不管它是否為這個類的子類。

不過,protected方法對於指示那些不提供一般用途而應在子類中重新定義的方法很有用。

  1. 使用繼承實現“is-a"關係
  2. 除非所有繼承的方法都有意義,否則不要使用繼承
  3. 在覆蓋方法時,不要改變預期的行為
  4. 使用多型,而非型別資訊

對於下面這種形式的程式碼:

if (x is of type 1)
    action1(x);
else if (x is of type 2)
    action2(x);

都應該考慮使用多型性。

如果action1和action2表示的是相同的概念,就應該為這個概念定義一個方法,並將其放置在兩個類的超類或介面中,然後,就可以呼叫

x.action();

以便使用多型性提供的動態分配機制執行相應的操作。

​ 使用多型方法或介面編寫的程式碼比使用對多種型別進行檢測的程式碼更容易維護和擴充套件。

  1. 不要過多的使用反射

編譯器很難幫助人們發現程式中的錯誤,因此只有在執行時才發現錯誤並導致異常。