Java核心技術筆記 繼承

Yumiku發表於2019-01-19

《Java核心技術 卷Ⅰ》 第5章 繼承

  • 類、超類、子類
  • Object:所有類的超類
  • 泛型陣列列表
  • 物件包裝器與自動裝箱
  • 引數數量可變的方法
  • 列舉類
  • 繼承的設計技巧

類、超類和子類

定義子類

關鍵字extend表示繼承。

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

extend表明正在構造的新類派生於一個已存在的類。

已存在的類稱為超類(superclass)、基類(base class)或父類(parent class);
新類稱為子類(subclass)、派生類(derived class)或孩子類(child class)。

子類有超類沒有的功能,子類封裝了更多的資料,擁有更多的功能。

所以在擴充套件超類定義子類時,僅需要指出子類與超類的不同之處。

覆蓋方法

有時候,超類的有些方法並不一定適用於子類,為此要提供一個新的方法來覆蓋(override)超類中的這個方法:

public class Manager extends Employee
{
  private double bonus;
  ...
  public double getSalary()
  {
    double baseSalary = super.getSalary();
    return baseSalary + bonus;
  }
  ...
}

這裡由於Manager類的getSalary方法並不能直接訪問超類的私有域

這是因為儘管子類擁有超類的所有域,但是子類沒法直接獲取到超類的私有部分,因為超類的私有部分只有超類自己才能夠訪問。而子類想要獲取到私有域的內容,只能通過超類共有的介面。

而這裡Employee的公有方法getSalary正是這樣的一個介面,並且在呼叫超類方法使,要使用super關鍵字。

那你可能會好奇:

  • 不加super關鍵字不行麼?
  • Employee類的getSalary方法不應該是被Manager類所繼承了麼?

這裡如果不使用super關鍵字,那麼在getSalary方法中呼叫一個getSalary方法,勢必會引起無限次的呼叫自己。

關於superthis需要注意的是:他們並不類似,因為super不是一個物件的引用,不能將super賦給另一個物件變數,它只是一個指示編譯器呼叫超類方法的特殊關鍵字。

子類構造器

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

語句super(...)是呼叫超類中含有對應引數的構造器。

Q1:為什麼要這麼做

A1:由於子類的構造器不能訪問超類的私有域,所以必須利用超類的構造器對這部分私有域進行初始化。但是注意,使用super呼叫構造器的語句必須是子類構造器的第一條語句

Q2:一定要使用麼

A2:如果子類的構造器沒有顯示地呼叫超類構造器,則將自動地呼叫超類預設(沒有引數)的構造器;如果超類並沒有不帶引數構造器,並且子類構造器中也沒有顯示呼叫,則Java編譯器將報告錯誤

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

這裡將e宣告為Emplyee物件,但是實際上e既可以引用Employee物件,也可以引用Manager物件。

  • 當引用Employee物件時,e.getSalary()呼叫的是EmployeegetSalary方法
  • 當引用Manager物件時,e.getSalary()呼叫的是ManagergetSalary方法

虛擬機器知道e實際引用的物件型別,所以能夠正確地呼叫相應的方法。

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

繼承層次

整合並不僅限於一個層次。

由一個公共超類派生出來的所有類的集合被稱為繼承層次(inheritance hierarchy),在繼承層次中,從某個特定的類到其祖先的路徑被稱為該類的繼承鏈(inheritance chain)。

Java不支援多繼承,有關Java中多繼承功能的實現方式,見下一章有關介面的部分。

多型

"is-a"規則的另一種表述法是置換法則,它表明程式中出現超類物件的任何地方都可以用子類物件置換

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

在Java程式設計語言中,物件變數是多型的

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

這個例子中,雖然staff[0]boss引用同一個物件,但是編譯器將staff[0]看成Employee物件,這意味著這樣呼叫是沒有問題的:

boss.setBonus(5000); // OK

但是不能這樣呼叫:

staff[0].setBonus(5000); // Error

這裡因為staff[0]宣告的型別是Employee,而setBonus不是Employee類的方法。

儘管把子類賦值給超類引用變數是沒有問題的,但這並不意味著反過來也可以:

Manager m = staff[2]; // Error

如果這樣賦值成功了,那麼編譯器將m看成是一個Manager類,在呼叫setBonus由於所引用的Employee類並沒有該方法,從而會發生執行時錯誤。

理解方法呼叫

弄清楚如何在物件上應用方法呼叫非常重要。

比如有一個C類物件xC有一個方法f(args)

現在以呼叫x.f(args)為例,說明呼叫過程的詳細描述:

  1. 編譯器檢視物件的宣告型別和方法名。C類中可能有多個同名的方法,編譯器將列舉所有C類中名為f的方法和其超類中屬性為public且名為f的方法(超類私有無法訪問)。
  2. 接下來,編譯器檢視呼叫方法時提供的引數型別。如果存在一個完全匹配的f,就選擇這個方法,這個過程被稱為過載解析(overloading resoluton)。這個過程允許型別轉換(int轉double,Manager轉Employee等等)。如果編譯器沒有找到,或者發現型別轉換後,有多個方法與之匹配,就會報告一個錯誤。
  3. 如果是private方法,static方法、final方法或者構造器,編譯器將可以準確知道應該呼叫哪個方法,這種稱為靜態繫結(static binding)。如果不是這些,那呼叫的方法依賴於隱式引數的實際型別,並且在執行時動態繫結,比如x.f(args)這個例子。
  4. 當程式執行時,並且動態繫結呼叫時,虛擬機器一定呼叫與x所引用物件的實際型別最合適的那個類的方法。比如x實際是C類,它是D類的子類,如果C類定義了方法f(String),就直接呼叫;否則將在C類的超類中尋找,以此類推。簡單說就是順著繼承層次從下到上的尋找方法。

如果每次呼叫方法都要深度/廣度遍歷搜尋繼承鏈,時間開銷非常大。

因此虛擬機器預先為每個類建立一個方法表(method table),其中列出了所有方法的簽名和實際呼叫的方法,這樣一來在真正呼叫時,只需要查表即可。

如果呼叫super.f(args),編譯器將對隱式引數超類的方法表進行搜尋。

之前的Employee類和Manager類的方法表:

Employee:
  getName() -> Employee.getName()
  getSalary() -> Employee.getSalary()
  getHireDay() -> Employee.getHireDay()
  raiseSalary(double) -> Employee.raiseSalary(double)

Manager:
  getName() -> Employee.getName()
  getSalary() -> Manager.getSalary()
  getHireDay() -> Employee.getHireDay()
  raiseSalary(double) -> Employee.raiseSalary(double)
  setBonus(double) -> Manager.setBonus(double)

在執行時,呼叫e.getSalary()的解析過程:

  • 虛擬機器提取實際型別的方法表(之所以叫實際型別,是因為Employee e可以引用所有Employee類的子類,所以要確定實際引用的型別)。
  • 虛擬機器搜尋定義getSalary簽名的類,虛擬機器確定呼叫哪個方法。
  • 最後虛擬機器呼叫方法。

注:在覆蓋一個方法時,子類方法不能低於超類方法的可見性,特別是超類方法是public,子類覆蓋方法時一定宣告為public,因為經常會發生這樣的錯誤:在宣告子類方法時,因為遺漏public而使編譯器把它解釋為更嚴格的訪問許可權。

阻止繼承:final類和方法

有時候,可能希望阻止人們利用某個類定義子類。

不允許擴充套件的類被稱為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,而不包括域。

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

強制型別轉換

有時候就像將浮點數轉換為整數一樣,也可能需要將某個類的物件引用轉換成另一個類的物件引用。

物件引用的轉換語法與數值表示式的型別轉換類似,僅需要一對圓括號將目標類名括起來,並放置在需要轉換的物件引用之前就可以了。

Manager boss = (Manager) staff[0];
// 因為之前把boss這個Manager類物件也存在了Employee陣列中
// 現在通過強制型別轉換回覆成Manager類

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

在Java中,每個物件變數都屬於一個型別,型別描述了這個變數所引用的以及能夠引用的物件型別。

將一個值存入變數時,編譯器將檢查是否允許該操作:

  • 將一個子類的引用賦給一個超類變數,編譯器時允許的
  • 但是將一個超類引用賦給一個子類變數,必須進行型別轉化,這樣才能通過執行時的檢查

如果試圖在繼承鏈上進行向下的型別轉換,並謊報有關物件包含的內容(比如硬要把一個Employee類物件轉換成Manager類物件):

Manager boss = (Manager) staff[1]; // Error

執行時,Java執行時系統將報告這個錯誤(不是在編譯階段),併產生一個ClassCastException異常,如果沒有捕獲異常,程式將會終止。

所以應該養成一個良好習慣:在進行型別強轉之前,先檢視一下是否能成功轉換,使用instanceof操作符即可:

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

注:如果xnull,則它對任何一個類進行instanceof返回值都是false,它因為沒有引用任何物件。

抽象類

位於上層的類通常更具有通用性,甚至可能更加抽象,對於祖先類,我們通常只把它作為派生其他類的基類,而不作為想使用的特定的例項類,比如Person類對於EmployeeStudent類而言。

由於Person對子類一無所知,但是又想規範他們,一種做法是提供一個方法,然後返回空的值,另一種就是使用abstract關鍵字,這樣Person就完全不用實現這個方法了。

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

為了提供程式的清晰度,包含一個或多個抽象方法的類本身必須被宣告為抽象的。

public abstract class Person
{
  private String name;
  ...
  public abstract String getDescription();
  ...
  public String getName()
  {
    return name;
  }
}

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

儘管許多人認為,在抽象類中不能包含具體方法,但是還是建議儘量把通用的域和方法(不管是否抽象)都放在超類(不管是否抽象)中。

雖然你可以宣告一個抽象類的引用變數,但是隻能引用非抽象子類的物件,因為抽象類不能被例項化。

在非抽象子類中定義抽象類的方法:

public class Student extends Person
{
  private String major;
  ...
  public String getDescription()
  {
    return "a student majoring in " + major;
  }
}

儘管Person類中沒有具體定義getDescription的具體內容,但是當一個Person型別引用變數p使用p.getDescription()也是沒有問題的,因為根據前面的方法呼叫過程,在執行時,方法的實際尋找是從實際型別開始尋找的,而實際型別都是定義了這個方法的具體內容。

那你可能會問,我可以只在Student類中定義getDescription不就行了麼?為什麼還要在Person去宣告?因為如果這樣的話,就不能通過p呼叫getDescription方法了,因為編譯器只允許呼叫在類中宣告的方法。

受保護訪問

有些時候,人們希望超類中的某些方法允許被子類訪問,或允許子類的方法訪問超類中的某個域,而不讓其他類訪問到。

為此,需要將這些方法或域宣告為protected

例如,如果Employee中的hireDay宣告為protected,而不是private,則Manager中的方法就可以直接訪問它。

不過,Manager中的方法只能夠訪問Manager物件中的hireDay,而不能訪問其他Employee物件中的這個域,這樣使得子類只能獲得訪問受保護域的權利。

對於受保護的域來說,但是這在一定程度上違背了OOP提倡的資料封裝原則,因為如果當一個超類進行了一些修改,就必須通知所有使用這個類的程式設計師(而不像普通的private域,只能通過開放的方法去訪問)。

相比較,受保護的方法更具有實際意義。如果需要限制一個方法的使用,就可以宣告為protected,這表明子類得到信任,可以正確地使用這個方法,而其他類(非子類)不行。

這種方法的一個最好示例就是Object類中的clone方法。

歸納總結Java控制可見性的4個訪問修飾符:

  1. public:對所有類可見
  2. protected:對本包和所有子類可見
  3. private:僅對本類可見
  4. 預設,無修飾符:僅對本包可見

Object:所有類的超類

Obejct類是Java中所有類的始祖,Java中每個類都是它擴充套件而來。

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

自然地,可以使用Object型別的變數引用任何型別的物件:

Obejct obj = new Employee("Harry Hacker", 35000);

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

Employee e = (Employee) obj;

equals方法

Object類中的equals方法用於檢測一個物件是否等於另外一個物件。

這裡的等於指的是判斷兩個物件是否具有相同的引用

但是在判斷兩個不確定是否為null的物件是否相等時,需要使用Objects.equals方法,如果兩個都是null,將返回true;如果其中一個為null,另一個不是,則返回false;如果兩個都不為null,則呼叫a.equals(b)

當然大多數時候Object.equals並不能滿足,一般來說我們需要比較兩個物件的狀態是否相等,這個時候需要重寫這個方法:

public class Manager extends Employee
{
  ...
  public boolean equals(Object otherObject)
  {
    // 首先要呼叫超類的equals
    if(!super.equals(otherObejct)) return false;
    Manager other = (Manager) otherObject;
    return bonus == other.bonus;
  }
}

相等測試與繼承

在閱讀後面的書籍筆記內容之前,首先補充一下getClassinstanceof到底是什麼:

  • obejct.getClass():返回此object的執行時類Class(Java中有一個類叫Class)。比如一個Person變數p,則p.getClass()返回的就是Person這個類的Class物件,Class類提供了很多方法來獲取這個類的相關資訊
  • obejct instanceof ClassName:用來在執行時指出這個物件是否是這個特定類或者是它的子類的一個例項,比如manager instanceof Employee是返回true

好了,讓我們回到原書吧。

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

有許多程式設計師喜歡使用instanceof來進行檢測:

if(!otherObject instanceof Employee) return false;

這樣做不但沒有解決otherObject是子類的情況,並且還可能招致一些麻煩。

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

  • 自反性:任何非空引用xx.equals(x)應返回true
  • 對稱性:任何引用xyy.equals(x)返回true,則x.equals(y)也應該返回true
  • 傳遞性:任何引用xyz,如果x.equals(y)返回truey.equals(z)返回true,則x.equals(z)也應該返回true
  • 一致性:如果xy引用物件沒有發生變化,反覆呼叫x.equals(y)應該返回同樣結果
  • 任意非空引用xx.equals(null)應該返回false

從兩個不同的情況看一下這個問題:

  • 如果子類能夠擁有自己的相等概念,則對稱性需求將強制採用getClass進行檢測
  • 如果由超類決定相等的概念,那麼就可以使用instanceof進行檢測,這樣可以在不同子類的物件之間進行相等的比較

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

  1. 顯示引數命名為otherObejct,稍後將它轉換成另一個叫做other的變數
  2. 檢測thisotherObject是否因用同一個物件:

    if(this == otherObject) return true;
  3. 檢測otherObject是否為null

    if(otherObject == null) return false;
  4. 比較thisotherObject是否屬於同一個類

    // 如果equals語義在每個子類中有改變,就用getClass
    if(getClass() != otherObject.getClass()) return false;
    // 如果子類擁有統一的語義,就用instanceof檢測
    if(!(otherObejct instanceof ClassName)) return false;
  5. otherObejct轉換為相應的類型別變數:

    ClassName other  = (ClassName) otherObejct;
  6. 開始進行域的比較,使用==比較基本型別域,使用Objects.equals比較物件域

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

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

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

hashCode方法

雜湊碼(hash code)是由物件匯出的一個整數值。

雜湊碼是沒有規律的,如果xy是兩個不同的物件,x.hashCode()y.hashCode()基本上不會相同。

對於String類而言,字串的雜湊碼是由內容匯出的。

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

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

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

例如,Employee類的hashCode方法:

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

不過如果使用null安全的方法Objects.hashCode(...)就更好了,如果引數為null,這個方法返回0。

另外,使用靜態方法Double.hashCode(salary)來避免建立Double物件。

還有更好的做法,需要組合多個雜湊值時,可以呼叫Objects.hash並提供多個引數。

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

equalshashCode的定義必須一致:如果x.equals(y)返回true,那麼x.hashCode()就必須與y.hashCode()具有相同的值。

toString方法

Object中還有一個重要的方法,就是toString方法,它用於返回表示物件值的字串。

絕大多數(但不是全部)的toString方法都遵循這樣的格式:類的名字,隨後是一對方括號括起來的域值。

public String toString()
{
  return getClass().getName()
    + "[name=" + name
    + ",salary=" + salary
    + ",hireDay=" + hireDay
    + "]";
}

toString方法也可以供子類呼叫。

當然,設計子類的程式設計師也應該定義自己的toString方法,並將子類域的描述新增進去。

如果超類使用了getClass().getName(),子類只需要呼叫super.toString()即可。

public class Manager extends Employee
{
  ...
  public String toString()
  {
    return super.toString()
      + "[bonus=" + bonus
      + "]";
  }
}

現在,Manager物件將列印輸出如下所示內容:

Manager[name=...,salary=...,hireDay=...][bonus=...]

注意這裡在子類中呼叫的super.toString(),不是在超類Employee中呼叫的麼?為什麼列印出來的是Manager

因為getClass正如前面所說,獲取的是這個物件執行時的類,與在哪個類中呼叫無關。

如果任何一個物件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@1a46e30

字首[I表明是一個整形陣列,如果想要得到裡面內容的字串,應該使用Arrays.toString

String s = Arrays.toString(luckyNumbers);
// s [2,3,5,7,11,13]

如果想要列印多維陣列,應該使用Arrays.deepToString方法。

強烈建議為自定義的每一個類增加toString方法。

泛型陣列列表

在許多程式設計語言中,必須在編譯時就確定整個陣列大小。

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

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

當然,這段程式碼並沒有完全解決執行時動態更改陣列的問題。一旦確定了大小,想要改變就不容易了。

在Java中,最簡單的解決方法是使用Java中另一個被稱為ArrayList的類,它使用起來有點像陣列,但在新增或刪除元素時,具有自動調節陣列容量的功能,而不需要為此編寫任何程式碼。

ArrayList是一個採用型別引數(type paraneter)的泛型類(generic class)。為了指定陣列列表儲存的元素物件型別,需要用一對尖括號將類名括起來加在後面,例如ArrayList<Employee>

ArrayList<Employee> staff = new ArrayList<Employee>();
// 兩邊都是用引數有些繁瑣,在Java SE 7中,可以省去右邊的型別引數
ArrayList<Employee> staff = new ArrayList<>();

這一般叫做“菱形語法”(<>),可以結合new操作符使用。

如果賦值給一個變數,或傳遞到某個方法,或者從某個方法返回,編譯器會檢查這個變數、引數或方法的泛型型別,然後將這個型別放在<>中。

在這個例子中,new ArrayList<>()將賦值給一個型別為ArrayList<Employee>的變數,所以泛型型別為Employee

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

staff.add(new Employee(...));

陣列列表管理著物件引用的一個內部陣列,最終陣列空間有可能被用盡,這時陣列列表將會自動建立一個更大的陣列,並將所有的物件從較小陣列中拷貝到較大陣列中。

也可以確定儲存的元素數量,在填充陣列前呼叫ensureCapacity方法:

// 分配一個包含100個物件的內部陣列
// 在100次呼叫add時不用再每次都重新分配空間
staff.ensureCapacity(100);
// 當然也可以通過把初始容量傳遞給構造器實現
ArrayList<Employee> staff = new ArrayList<>(100);

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

staff.size()

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

訪問陣列列表元素

陣列列表自動擴充套件容量的便利增加了訪問元素語法的複雜程度。

需要使用getset方法實現或改變陣列元素的操作,而不是[index]語法格式。

staff.set(i, harry);
Employee e = staff.get(i);

當沒有泛型類時,原始的ArrayList類提供的get方法別無選擇只能返回Object,因此,get方法的呼叫者必須對返回值進行型別轉換:

Employee e = (Employee) staff.get(i);

當然還是有一個比較方便的方法來靈活擴充套件又方便訪問:

ArrayList<X> list = new ArrayList<>();
while(...)
{
  x = ...;
  list.add(x);
}
X[] a = new X[list.size()];
// 使用toArray方法把陣列元素拷貝到一個陣列中
list.toArray(a);

還可以在陣列列表的中間插入元素:

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

當然也可以刪除一個元素:

Employee e = staff.remove(n);

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

for(Employee e : staff)
  do sth with e

物件包裝器與自動裝箱

有時需要將int這樣的基本型別轉換為物件,所有基本型別都有一個與之對應的類。

例如,Integer類對應基本型別int,通常這些類稱為包裝器(wrapper)。

這些物件包裝器有很明顯的名字:IntegerLongFloatDoubleShortByteCharacterVoidBoolean(前6個類派生於公共超類Number)。

物件包裝器類是不可變的,即一旦構造了包裝器,就不允許更改包裝在其中的值。

同時,物件包裝器類還是final,因此不能定義它們的子類。

有一個很有用的特性,便於新增int型別的元素到ArrayList<Integer>中。

ArrayList<Integer> list = new ArrayList<>();
list.add(3);
// 這裡將自動地變為
list.add(Integer.valueOf(3));

這種變換被稱為自動裝箱(autoboxing)。

相反地,將一個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中上面的判斷是有可能(may)成立的(這也太玄學了),所以解決辦法一般是使用equals方法。

還有一些需要強調的:

  • 包裝器引用可以為null,所以自動裝箱可能會丟擲NullPointerException異常
  • 如果條件表示式中混用IntegerDouble型別,Integer值就會拆箱,提升為double,再裝箱為Double
  • 裝箱和拆箱是編譯器認可的,而不是虛擬機器,編譯器在生成類位元組碼時,插入必要的方法呼叫,虛擬機器只是執行這些位元組碼(就相當於一個語法糖吧)。

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

int x = Integer.parseInt(s);

引數數量可變的方法

Java SE 5以前的版本中,每個Java方法都有固定數量的引數,然而現在的版本提供了可變的引數數量呼叫的方法。

比如printf方法的定義:

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

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

實際上,printf方法接收兩個引數,一個是格式字串,另一個是Object[]陣列,其中儲存著所有的引數。

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

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

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

// 找出最大值
public static double max(double... values)
{
  double largest = Double.NEGATIVE_INFINITY;
  for(double v : values) if(v > largest) largest = v;
  return largest;
}

列舉類

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

實際上這個宣告定義的型別是一個類,它剛好有4個例項。

因此比較兩個列舉型別值時,不需要呼叫equals,直接使用==就可以了。

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

public enum Size
{
  SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");

  private String abbreviation;

  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方法,返回一個包含全部列舉值的陣列。

Sizep[] values = Size.values();

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

反射

反射是一種功能強大且複雜的機制,使用它的主要人員是工具構造者,而不是應用程式設計師。

所以這部分先跳過,將會在以後一個專題單獨來說明。

繼承的設計技巧

  1. 將公共操作和域放在超類
  2. 不要使用受保護的域
  3. 使用繼承實現"is-a"關係
  4. 除非所有繼承的方法都有意義,否則不要使用繼承
  5. 在覆蓋方法時,不要改變預期的行為,不要偏離最初的設計想法
  6. 使用多型,而非型別資訊
  7. 不要過多地使用反射

Java繼承總結

  • 子類(定義、構造器、方法覆蓋)
  • 繼承層次
  • 多型
  • 方法呼叫的過程細節
  • final類和方法
  • 強制型別轉換
  • 抽象類
  • protected受保護訪問
  • Object所有類的超類
  • equals方法
  • 相等測試與繼承
  • hashCode方法
  • toString方法
  • 泛型陣列列表
  • 物件包裝器與自動裝箱
  • 引數數量可變的方法
  • 列舉類
  • 繼承設計技巧

個人靜態部落格:

相關文章