《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
方法,勢必會引起無限次的呼叫自己。
關於super
和this
需要注意的是:他們並不類似,因為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()
呼叫的是Employee
的getSalary
方法 - 當引用
Manager
物件時,e.getSalary()
呼叫的是Manager
的getSalary
方法
虛擬機器知道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
類物件x
,C
有一個方法f(args)
。
現在以呼叫x.f(args)
為例,說明呼叫過程的詳細描述:
- 編譯器檢視物件的宣告型別和方法名。
C
類中可能有多個同名的方法,編譯器將列舉所有C
類中名為f
的方法和其超類中屬性為public
且名為f
的方法(超類私有無法訪問)。 - 接下來,編譯器檢視呼叫方法時提供的引數型別。如果存在一個完全匹配的
f
,就選擇這個方法,這個過程被稱為過載解析(overloading resoluton)。這個過程允許型別轉換(int轉double,Manager轉Employee等等)。如果編譯器沒有找到,或者發現型別轉換後,有多個方法與之匹配,就會報告一個錯誤。 - 如果是
private
方法,static
方法、final
方法或者構造器,編譯器將可以準確知道應該呼叫哪個方法,這種稱為靜態繫結(static binding)。如果不是這些,那呼叫的方法依賴於隱式引數的實際型別,並且在執行時動態繫結,比如x.f(args)
這個例子。 - 當程式執行時,並且動態繫結呼叫時,虛擬機器一定呼叫與
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];
...
}
注:如果x
為null
,則它對任何一個類進行instanceof
返回值都是false
,它因為沒有引用任何物件。
抽象類
位於上層的類通常更具有通用性,甚至可能更加抽象,對於祖先類,我們通常只把它作為派生其他類的基類,而不作為想使用的特定的例項類,比如Person
類對於Employee
和Student
類而言。
由於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個訪問修飾符:
-
public
:對所有類可見 -
protected
:對本包和所有子類可見 -
private
:僅對本類可見 - 預設,無修飾符:僅對本包可見
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;
}
}
相等測試與繼承
在閱讀後面的書籍筆記內容之前,首先補充一下getClass
和instanceof
到底是什麼:
- 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
方法具有下面的特性:
- 自反性:任何非空引用
x
,x.equals(x)
應返回true
- 對稱性:任何引用
x
和y
,y.equals(x)
返回true
,則x.equals(y)
也應該返回true
- 傳遞性:任何引用
x
,y
和z
,如果x.equals(y)
返回true
,y.equals(z)
返回true
,則x.equals(z)
也應該返回true
- 一致性:如果
x
和y
引用物件沒有發生變化,反覆呼叫x.equals(y)
應該返回同樣結果 - 任意非空引用
x
,x.equals(null)
應該返回false
從兩個不同的情況看一下這個問題:
- 如果子類能夠擁有自己的相等概念,則對稱性需求將強制採用
getClass
進行檢測 - 如果由超類決定相等的概念,那麼就可以使用
instanceof
進行檢測,這樣可以在不同子類的物件之間進行相等的比較
給出一個編寫完美equals
方法的建議:
- 顯示引數命名為
otherObejct
,稍後將它轉換成另一個叫做other
的變數 -
檢測
this
與otherObject
是否因用同一個物件:if(this == otherObject) return true;
-
檢測
otherObject
是否為null
:if(otherObject == null) return false;
-
比較
this
和otherObject
是否屬於同一個類// 如果equals語義在每個子類中有改變,就用getClass if(getClass() != otherObject.getClass()) return false; // 如果子類擁有統一的語義,就用instanceof檢測 if(!(otherObejct instanceof ClassName)) return false;
-
將
otherObejct
轉換為相應的類型別變數:ClassName other = (ClassName) otherObejct;
-
開始進行域的比較,使用
==
比較基本型別域,使用Objects.equals
比較物件域return field1 == other.field1 && Objects.equals(field2, other.field2) && ...;
如果在子類中重新定義equals
,還要在其中包含呼叫super.equals(other)
。
另外,對於陣列型別的域,可以使用靜態的Array.equals
方法檢測相應的陣列元素是否相等。
hashCode方法
雜湊碼(hash code)是由物件匯出的一個整數值。
雜湊碼是沒有規律的,如果x
和y
是兩個不同的物件,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);
}
equals
與hashCode
的定義必須一致:如果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
方法。這個方法將儲存區域的大小調整為當前元素數量所需要的儲存空間數目,垃圾回收器將回收多餘的儲存空間。
訪問陣列列表元素
陣列列表自動擴充套件容量的便利增加了訪問元素語法的複雜程度。
需要使用get
和set
方法實現或改變陣列元素的操作,而不是[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)。
這些物件包裝器有很明顯的名字:Integer
、Long
、Float
、Double
、Short
、Byte
、Character
、Void
和Boolean
(前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
異常 - 如果條件表示式中混用
Integer
和Double
型別,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開始技術。
反射
反射是一種功能強大且複雜的機制,使用它的主要人員是工具構造者,而不是應用程式設計師。
所以這部分先跳過,將會在以後一個專題單獨來說明。
繼承的設計技巧
- 將公共操作和域放在超類
- 不要使用受保護的域
- 使用繼承實現
"is-a"
關係 - 除非所有繼承的方法都有意義,否則不要使用繼承
- 在覆蓋方法時,不要改變預期的行為,不要偏離最初的設計想法
- 使用多型,而非型別資訊
- 不要過多地使用反射
Java繼承總結
- 子類(定義、構造器、方法覆蓋)
- 繼承層次
- 多型
- 方法呼叫的過程細節
- final類和方法
- 強制型別轉換
- 抽象類
-
protected
受保護訪問 -
Object
所有類的超類 -
equals
方法 - 相等測試與繼承
-
hashCode
方法 -
toString
方法 - 泛型陣列列表
- 物件包裝器與自動裝箱
- 引數數量可變的方法
- 列舉類
- 繼承設計技巧
個人靜態部落格:
- 氣泡的前端日記: https://rheabubbles.github.io