5繼承
5.1 類、超類和子類
重用部分代碼,并保留所有域。“is-a”關系,用extends表示。
已存在的類被稱為超類(superclass)、基類(base class)或父類(parent class);新類稱為子類(subclass)、派生類(derived class)或孩子類(child class)。子類比父類封裝了更多的數據,擁有更多的功能。
在設計類時,盡可能將通用的功能放到父類中,具體特殊用途的放到子類中。有時需要提供一個新的方法來覆蓋(override)父類中的方法。例:
class Manager extends Employee{
/**
Manager繼承于Employee類,添加了bonus屬性和setBonus方法。
/
private double bonus;
...
public void setBonus(double b){
bonus = b;
}
}
Manager類中添加方法覆蓋Employee中的getSalary()方法:
public double getSalary(){
return salary+bonus;//won't work
}
該方法無法運行,因為子類中的方法不能直接地訪問父類的私有域*,如果要訪問,必須借助公有的接口。改為:
public double getSalary(){
double baseSalary = getSalary(); //still won't work
return baseSalary + bonus;
}
仍然無法運行,因為Manager類也有一個getSalary方法,所以該語句會無限次調用自己。所以正確的格式為:
public double getSalary(){
//super關鍵字指明調用父類中的getSalary方法,而不是當前類的這個方法
double baseSalary = super.getSalary();
return baseSalary + bonus;
}在子類中可以增加域、增加方法或覆蓋超類的方法,然而絕不能刪除任何繼承的方法和域。
super在構造器中的應用:
public Manager(String n, double s, int year, int month, int day){
super(n,s,year,month,day);
bonus = 0;
}
super指“調用超類Employee中含有n、s、year、month和day參數的構造器”。我們可以通過super實現對超類構造器的調用,使用super調用構造器的語句必須是子類構造器的第一條語句。如果子類的構造器沒有顯式地調用超類的構造器,則將自動地調用超類默認(沒有參數)的構造器。如果超類沒有不帶參數的構造器,并且在子類的構造器中又沒有顯式地調用超類的其他構造器,則編譯報錯。this和super很像。this的兩個功能:1)引用隱式參數;2)調用該類其他結構的構造器。super的兩個用途:1)調用超類的方法;2)調用超類的構造器。
Manager boss = new Manager("Carl", 80000, 1987, 12, 15);
boss.setBonus(5000);
Employee[] staff = new Employee[3];
staff[0] = boss;
staff[1] = new Employee("Harry", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony", 40000, 1990, 3, 15);這里的staff[1]和[2]都是Employee對象,staff[0]是Manager對象。(Li:子類對象可以被認為是父類對象,反之不可。)
boss引用Employee對象時,boss.getSalary()調用的是Employee中的getSalary方法;當boss引用Manager對象時,boss.getSalary()調用的是Manager中的getSalary方法。一個對象變量可以指示多種實際類型的現象被稱為多態(polymorphism)。在運行時能夠自動地選擇調用哪個方法的現象稱為動態綁定(dynamic binding)。
.1繼承層次
- 繼承不僅限于一個層次。由一個公共超類派生出來的所有類的集合被稱為繼承層次(inheritance hierarchy)。在繼承層次中,某個特定的類到其祖先的路徑被稱為該類的繼承鏈(inheritance chain)。
- 通常,一個祖先可以擁有多個子孫繼承鏈。Java不支持多繼承。
.2多態
- 一個判斷是否應該設計為繼承關系的簡單規則,就是“is-a"規則,它表明子類的每個對象也是超類的對象。“is-a"規則的另一種表述法是置換法則,它表明程序中出現超類的對象的任何地方都可以用子類對象置換。例如,可以將一個子類對象賦值給超類變量:
Employee e;
e = new Employee(...);//Employee object expected
e = new Manager(...);//can be used as well - 在Java中,對象變量是多態的,一個Employee變量既可以引用一個Employee類對象,也可以引用一個Employee類的任何一個子類的對象。
Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss; - 變量boss和staff[0]引用同一個對象,但編譯器將staff[0]看成Employee對象。這意味著
boss.setBonus(5000);
成立,而staff[0].setBonus(5000);
無法調用。不能將一個超類的引用賦值給予子類變量。
.3動態綁定
- 調用對象方法的執行過程:
- 1)編譯器查看對象的聲明類型和方法名,一一列舉該類中所有同名方法和其超類中同名的public方法。至此,編譯器已獲得所有可能被調用的候選方法。
- 2)接下來,編譯器將查看調用方法時提供的參數類型。如果在之前獲得的方法中存在一個與提供的參數完全匹配,就選擇該方法。這個過程被稱為重載解析(overloading resolution)。由于允許類型轉換,所以這個過程可能會很復雜。如果編譯器沒有找到與參數類型匹配的方法,或者經過類型轉換之后有多個方法與之匹配,就會報告一個錯誤。至此,編譯器已獲得需要調用的方法名字和參數類型。
- 注:方法的名字和簽名。如果子類中定義了一個與超類方法相同的方法,那么子類中的這個方法就覆蓋了超類中的這個簽名相同的方法。返回類型不是簽名的一部分。因此在覆蓋方法時,一定要保證返回類型的兼容性。允許子類覆蓋方法的返回類型定義為原返回類型的子類型。
- 3)如果是private方法、static方法、final方法或者構造器,那么編譯器將可以準確地知道應該調用哪個方法,我們將這種調用方式稱為靜態綁定(static binding)。與此對應的是,調用的方法依賴于隱式參數的實際類型,并且在運行時實現動態綁定。
- 4)當程序運行,并采用動態綁定調用方法時,虛擬機一定調用與x所引用對象的實際類型最合適的那個類的方法。
- 每次調用都要進行搜索,時間開銷相當大。因此,虛擬機預先為每個類創建了一個方法表(method table),其中列出了所有方法的簽名和實際調用的方法。這樣一類僅需查表即可。
- 動態綁定有一個非常重要的特征:無需對現存的代碼進行修改,就可以對程序進行拓展。
- 注:在覆蓋一個方法時,子類方法不能低于超類方法的可見性。特別是,如果超類方法是public,子類方法一定聲明為public。如果遺漏,編譯器會解釋為更嚴格的訪問權限。
.4阻止繼承:final類和方法
- 不允許擴展的類被稱為final類,定義時用final修飾符表明。
- 類中特定的方法也可以被聲明為final,子類將不能覆蓋這個方法(final類中的所有方法自動地成為final方法)。
- 域也可以被聲明為final。對于final域來說,構造對象之后就不允許改變它們的值了。不過,如果將類聲明為final,只有其中的方法自動地成為final,而不包括域。
- 將方法或類聲明為final主要目的是:確保它們不會在子類中改變語義。
- 動態綁定會帶來更多的系統開銷。如果一個方法沒有被覆蓋并且很短,編譯器就能夠對它進行優化處理,這個過程稱為內聯(inlining)。例如,內聯調用e.getName()將被替換為訪問e.name域。
.5強制類型轉換
double x = 3.405;
int nx = (int)x;
將x的值轉換成整數類型,舍棄了小數部分。
有時需要將某個類的對象引用轉換成為另一個類的對象引用。進行強制類型轉換的唯一原因是:在暫時忽視對象的實際類型之后,使用對象的全部功能。
將一個值存入變量時,編譯器將檢查是否允許該操作。將一個子類的引用賦給一個超類變量,編譯器是允許的。但將一個超類的引用賦給一個子類變量,必須進行強制類型轉換,這樣才能通過檢查。如果試圖在繼承鏈上進行向下的類型轉換,并且謊報有關對象包含的內容,會產生ClassCastException異常。
一個良好的習慣,在進行類型轉換時,先查看一下是否能夠轉換成功:
if(staff[1] instanceof Manager){
//不可轉換便不會執行
boss = (Manager)staff[1];
}Date c = (Date) staff[1];
將會產生編譯錯誤,因為Date不是Employee的子類。綜上:1)只能在繼承層次內進行類型轉換;2)在將超類轉換成子類之前,應該使用instanceof進行檢查。(實際上通過類型轉換調整對象的類型并不是一種好的做法)
.6抽象類
如果自下而上在類的繼承層次中上移,位于上層的類更具有通用性,甚至可能更抽象。從某種角度看,祖先類更加通用。
-
使用abstract關鍵詞,使一個類完全不需要實現某個方法。為了提高清晰度,包含一個或多個抽象方法的類本身必須被聲明為抽象的。除了抽象方法以外,抽象類還可以包含具體數據和具體方法。
abstract class Person{
private String name;
public Person(String n){
name = n;
}
public abstract String getDescription();
public String getName(){
return name;
}
}- Tips:許多程序員認為,在抽象類中布恩那個包含具體方法。建議盡量使用通用的域和方法(不管是否是抽象的)放在超類(不管是否是抽象類)中。
抽象方法充當占位的角色,它們的具體實現在子類中。擴展抽象類可以有兩個選擇:1)在抽象類中定義部分抽象類方法或不定義抽象類方法,這樣就必須將子類也標記成為抽象類;2)定義全部的抽象方法,這樣一來子類就不是抽象的了。
類即使不含抽象方法,也可以將類聲明為抽象類。抽象類不能被實例化。也就是說,如果將一個類聲明為abstract,就不能創建這個類的對象。但是可以創建一個具體子類的對象。需要注意,可以定義一個抽象類的對象變量,但是它只能引用非抽象子類的對象。如:
Person p = new Student("Vince Vu", "Economics");
這里的p是一個抽象類Person的變量,Person引用了一個非抽象子類Student的實例。下面通過抽象類Person擴展一個具體子類Student,在該類中的全部方法都是非抽象方法,所以不再是抽象類:
class Student extends Person{
private String major;
public Student(String n, String m){
super(n);
major = m;
}
public String getDesription(){
return "A student majoring in " + major;
}
}測試程序清單:
Person[] people = new Person[2];
people[0] = new Employee(...);
people[1] = new Student(...);
for(Person p : people){
System.out.println(p.getName()+","+p.getDescription());
}
由于不能構造抽象類Person的對象,所以變量永遠不會引用Person對象,而是引用諸如Employee或Student這樣的具體子類對象,而這些對象中都定義了getDescription方法。如果不聲明抽象方法,則無法使用p調用方法,
.7受保護訪問
- 有些時候,人們希望超類中的某些方法允許被子類訪問,或允許子類的方法訪問超類的某個域。為此,需要將這些方法和域聲明為protected。在實際使用中,要謹慎使用protected屬性,否則會違背OOP的數據封裝原則。聲明為protected的方法,只能被子類調用,其他類無法使用。例,Object類中的clone方法。
- 控制可見性的訪問修飾符:
- 僅對本類可見————private
- 對所有類可見————public
- 對本包和所有子類可見————protected
- 對本包可見————默認,無需修飾符
5.2 Object:所有類的超類
- Object類是Java中的所有類的始祖,在Java中每個類都是由它擴展而來的。可以使用Object類型的變量引用類型的對象:
Object obj = new Employee("Harry", 35000);
當然,Object類型的變量只能用于作為各種值得通用持有者。要想對其中的內容進行具體的操作,還需要清楚對象的原始類型,并進行相應的類型轉換:
Employee e = (Employee) obj;
在Java中,只有基本類型(primitive types)不是對象,例如,數值、字符和布爾類型的值都不是對象。所有的數組類型,不管是對象數組還是基本類型的數組都擴展于Object類。
.1equals方法
-
Object類中的equals方法用于檢測一個對象是否等于另一個對象。這個方法將判斷兩個對象是否具有相同的引用,似乎合情合理,然而對于多數類來說,這種判斷并沒有什么意義。如果兩個對象的狀態相等,就認為這兩個對象是相等的。利用下面這個示例演示equals方法的實現機制:
class Employee{
...
public boolean equals(Object otherObject){
//a quick test to see if the objects are identical
if(this == otherObject) return true;//must return false if the explicit parameter is null if(otherObject == null) return false; //if the classes don't match, they can't be equal if(getClass() != otherObject.getClass()) return false; //now we know otherObject is a non-null Employee Employee other = (Employee)otehrObject; //test whether the fields have identical values return name.equals(other.name) && salary == other.salary && hireDay.equals(other.hireDay); } }
getClass方法返回一個對象所屬的類。
Tips:為了防備name或hireDay可能為null的情況,需要使用Objects.equals方法。如果兩個參數都為null,Objects.equals(a,b)調用將返回ture;如果其中一個參數為null,則返回false;否則,如果兩個參數都不為null,則調用a.equals(b)。利用這個方法,最后一句要改寫為:
return Objects.equals(name, other.name)
&& salary == other.salary
&& Objects.equals(hireDay, other.hireDay);在子類中定義equals方法時,首先調用超類的equals。如果檢測失敗,對象就不可能相等。如果超類中的域都相等,就需要比較子類中的實例域。
class Manager extends Employee{
...
public boolean equals(Object otherObject){
if(!super.equals(otherObject)) return false;
//super.equals checked that this and otherObject belong to the same class
Manager other = (Manager)otherObject;
return bonus == other.bonus;
}
}
.2相等測試與繼承
- 如果隱式和顯式的參數不屬于同一個類,equals方法將如何處理呢?這是一個很有爭議的問題。在前面的例子中,如果發現類型不匹配,equals方法就返回false。但是很多程序員卻喜歡使用instanceof進行檢測:
if(!(otherObject 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
- 這些規則似乎合情合理,但還是有一些特殊情況。就對稱性來說,但當參數不屬于同一個類的時候需要仔細地思考一下。考慮這個調用:
e.equals(m);
。這里的e是一個Employee對象,m是一個Manager對象,并且兩個對象具有相同的姓名、薪水和雇傭日期。如果在Employee.equals中用instanceof檢測,則返回true。然而當調用:m.equals(e);
也需要返回true。對稱性不允許這個方法調用返回false,或者拋出異常。這就使得Manager類受到了束縛,這個類的equals方法必須能夠用自己與任何一個Employee對象進行比較,而不必考慮經理擁有的那部分特有的信息。 - 可以從兩個截然不同的情況看一下這個問題:
如果子類能夠擁有自己的相等概念,則對稱性需求將強制采用getClass進行檢測
如果由超類決定相等的概念,那么就可以使用instanceof進行檢測,這樣可以在不同子類的對象之間進行相等的比較
Tips:在Java庫中包含150多個equals方法的實現,包括使用instanceof檢測、調用getClass檢測、捕獲ClassCastException或者什么也不做。好像陷入了一種困境。
-
下面給出編寫一個完美的equals方法的建議:
- 1)顯式參數命名為otherObject,稍后需要將它轉換成另一個叫做other的變量
- 2)檢測this與otherObject是否引用同一個對象:
if(this == otherObject) return true;
這條語句只是一個優化。實際上,這是一種經常采用的形式。因為計算這個等式要比一個一個地比較類中的域所付出的代價小得多 - 3)檢測otherObject是否為null,如果為null,則返回false。這項檢查時很必要的。
if(otherObject == null) return false;
- 4)比較this與otherObject是否屬于同一個類。如果equals的語義在每個子類中有所改變,就使用getClass檢測:
if(getClass() != otherObject.getClass()) return false;
如果所有的子類都擁有統一的語義,就使用instanceof檢測
`if(!(otherObject instanceof ClassName)) return false; - 5)將otherObject轉換為相應的類類型變量:
ClassName other = (ClassName) otherObject;
- 6)現在開始對所有需要比較的域進行比較了。使用==比較基本類型域,使用equals比較對象域。如果所有的域都匹配,就返回true;否則返回false。
return field1 == other.field1
&& Object.equals(field2, other.field2)
&& ...;
如果在子類中重新定義equals,就要在其中包含調用super.equals(other)。 - Tips:對于數組類型的域,可以使用靜態的Arrays.equals方法檢測相應的數組元素是否相等
.3hashCode方法
散列碼(hash code)是由對象導出的一個整數值。散列碼是沒有規律的。如果x和y是兩個不同的對象,x.hashCode()與y.hashCode()基本不會相同。
String類使用下列算法計算散列碼:
int hash = 0;
for(int i=0; i<length(); i++){
hash = 31*hash + charAt(i);
}-
由于hashCode方法定義在Object類中,因此每個對象都有一個默認的散列碼,其值為對象的存儲地址。例:
String s = "OK";
StringBuilder sb = new StringBuilder(s);
System.out.println(s.hashCode()+" "+sb.hashCode());
String t = new String("OK");
StringBuilder tb = new StringBuilder(t);
System.out.println(t.hasCode()+" "+tb.hashCode());對象 散列碼 s 256 sb 20526976 t 256 tb 20527144
注意,字符串s與t擁有相同的散列碼,這是因為字符串的散列碼是由內容導出的。而字符串緩沖sb與tb卻有著不用的散列碼,這是因為在StringBuffer類中沒有定義hashCode方法,它的散列碼是由Object類的默認hashCode方法導出的對象存儲地址。
- 如果重新定義equals方法,就必須重新定義hashCode方法,以便用戶可以將對象插入散列表中。
- hashCode方法應該返回一個整型數值(也可以是負數),并合理地組合實例域的散列碼,以便能能夠讓各個不同的對象產生的散列碼更加均勻。
- 在Java7中還可以做兩個改進。首先,最好使用null安全的方法Objects.hashCode。如果其參數為null,這個方法會返回0,否則返回對參數調用hashCode的結果。還有更好的做法,需要組合多個散列值時,可以調用Objects.hash并提供多個參數。這個方法會對各個參數調用Objects.hashCode,并組合這些散列值。
- Equals與hashCode的定義必須一致:如果x.equals(y)返回true,那么x.hashCode()就必須與y.hashCode()具有相同的值。
- Tips:如果存在數組類型的域,那么可以使用靜態的Arrays.hashCode方法計算一個散列碼,這個散列碼由數組元素的散列碼組成。
.4toString方法
用于返回表示對象值的字符串。絕大多數(但不是全部)的toString方法都遵守這樣的格式:類的名字,隨后是一對方括號括起來的域值。下面是Employee類中的toString方法的實現:
public String toString(){
return "Employee[name="+name
+",salary="+salary
+",hireDay="+hireDay
+"]";
}
實際上,還可以設計得更好一些。最好通過調用getClass().getName()獲得類名的字符串,而不要類名硬加到toString方法中。
public String toString(){
return getClass().getName()
+"[name="+name
+",salary="+salary
+",hireDay="+hireDay
+"]";
}toString方法也可以供子類調用。如果超類使用了getClass().getName(),那么子類只要調用super.toString()就可以了。例如,下面是Manager類中的toString方法:
Class Manager extends Employee{
...
public String toString(){
return super.toString()
+"[bonus="+bonus
+"]";
}
}-
隨處可見toString方法的主要原因是:只要對象與一個字符串通過操作符“+”連接起來,Java編譯就會自動調用toString方法,以便獲得這個對象的字符串描述。例如:
Point p = new Point(10, 20);
String message = "The current position is "+p;
//automatically invokes p.toString()- Tips:在調用x.toString()的地方可以用""+x替代。這條語句是將一個空字符串與x的字符串表示相連接。這里的x就是x.toString()。與toString不同的是,如果x是基本類型,這條語句照樣能夠執行。如果x是任意一個對象,并調用
System.out.println(x);
,println方法就會直接地調用x.toString(),并打印輸出得到的字符串。
- Tips:在調用x.toString()的地方可以用""+x替代。這條語句是將一個空字符串與x的字符串表示相連接。這里的x就是x.toString()。與toString不同的是,如果x是基本類型,這條語句照樣能夠執行。如果x是任意一個對象,并調用
-
Object類定義了toString方法,用來打印輸出對象所屬的類名和散列碼。例如,調用
System.out.println(System.out);
,將輸出:java.io.PrintStream@2f6684。之所以得到這樣的結果是因為PrintStream類的設計者沒有覆蓋toString方法。- warning:令人煩惱的是,數組繼承了Object類的toString方法,數組類型將按照舊的格式打印。例如:
int[] luckyNumbers = {2,3,5,7,11,13};
String s = ""+luckyNumbers;
生成字符串“[I@1a46e30"(前綴[I表明是一個整型數組)。修正的方式是調用靜態方法Arrays.toString。代碼:
String s = Arrary.toString(luckyNumber);
將生成字符串”[2,3,5,7,11,13]"。想要打印多維數組(即,數組的數組)則需要調用Arrays.deepToString方法。
- warning:令人煩惱的是,數組繼承了Object類的toString方法,數組類型將按照舊的格式打印。例如:
-
toString方法是一種非常有用的調試工具。在標準類庫中,許多類都定義了toString方法,以便用戶能夠獲得一些有關對象狀態的必要信息。(之后會有Log的介紹。)
- Tips:強烈建議為自定義的每一個類增加toString方法。這樣做不僅自己受益,而且所有使用這個類的程序員也會從日志記錄中受益匪淺。
5.3泛型數組列表
在許多語言中,必須在編譯時就確定整個數組的大小。在Java中情況就好多了。它允許在運行時確定數組的大小。
int actualSize = ...;
Employee[] staff = new Employee[actualSize];
當然,這段代碼并沒有完全解決運行時動態更改數組的問題。一旦確定了數組的大小,改變它就太不容易了。在Java中,解決這個問題最簡單的方法是使用Java中另一個被稱為ArrayList的類。它使用起來有點像數組,但在添加或刪除元素時,具有自動調節數組容量的功能,而不需要為此編寫任何代碼。ArrayList是一個采用類型參數(type parameter)的泛型類(generic class)。為了指定數組列表保存的元素對象類型,需要用一對尖括號將類名括起來加在后面。例如:ArrayList<Employee>。
-
聲明和構造一個保存Employee對象的數組列表:
ArrayList<Employee> staff = new ArrayList<Employee>();
兩邊都使用類型參數Employee,有些繁瑣。Java7中,可以省去右邊的類型參數:
ArrayList<Employee> staff = new ArrayList<>();
這被稱為“菱形”語法,因為空尖括號<>就像一個菱形。可以結合new操作符使用菱形語法。編譯器會檢查新值是什么。如果賦值給一個變量,或者傳遞到某個方法,或者從某個方法返回,編譯器會檢查這個變量
、參數或者方法的泛型類型,然后將這個類型放在<>中。- Tips:Java SE5.0以前的版本沒有提供泛型類,而是有一個ArrayList類,其中保存類型為Object元素,它是“自適應大小”的集合。如果一定要使用老版本Java,則需要刪掉所有的后綴<...>。在新版中,沒有后綴的ArrayList會被認為是一個刪除了類型參數的“原始”類型,仍然可以使用。
使用add方法可以將元素添加到數組列表中去。例如,下面展示了如何將雇員對象添加到數組列表中的方法:
staff.add(new Employee("Harry", ...));
staff.add(new Employee("Tony", ...));
數組列表管理著對象引用的一個內部數組。最終,數組的全部空間有可能被用盡。這就顯現出數組列表的操作魅力:如果調用add且內部數組已經滿了,數組列表就將自動地創建一個更大的數組,并且將所有的對象從較小數組中拷貝到較大數組中。如果足夠清楚數組可能存儲的元素數量,就可以在填充數組之前調用ensureCapacity方法:
staff.ensureCapacity(100);
,這個方法將分配一個包含100個對象的內部數組。然后至多調用100次add,而不用重新分配空間。-
另外,還可以把初始容器傳遞給ArrayList構造器:
ArrayList<Employee> staff = new ArrayList<>(100);
- Warning:分配數組列表:
new ArrayList<>(100);//capacity is 100
,它與為新數組分配空間有所不同:new Employee[100];//size is 100
數組列表的容量與數組的大小有一個非常重要的區別。如果為數組分配100個元素的存儲空間,數組就有100個空位置可以使用。而容量為100個元素的數組列表只是擁有保存100個元素的潛力(實際上重新分配空間的話,將會超過100),但是在最初,甚至是完成初始化構造之后,數組列表根本就不含任何元素。
- Warning:分配數組列表:
size方法將返回數組列表中包含的實際元素數目。例如:
staff.size();
,將返回staff數組列表的當前元素數量,它等價于數組a的a.length。一旦能夠確認數組列表的大小不再發生變化,就可以調用trimToSize方法。這個方法將存儲區域的大小調整為當前元素所需要的存儲空間數目。垃圾回收器將回收多余的存儲空間。一旦整理了數組列表的大小,添加新元素就需要花時間再次移動存儲塊,所以應該在確認不會添加任何元素時,再調用trimToSize。
.1訪問數組列表元素
數組列表自動擴展容量的便利增加了訪問元素語法的復雜程度。其原因是ArrayList類并不是Java程序設計語言的一部分,它只是一個由某些人編寫且被放在標準庫中的一個實用類
-
使用get和set方法實現訪問或改變數組元素的操作,而不使用數組中的[]語法格式。例如,要設置第i個元素,可以使用:
staff.set(i, harry);
, 等價于對數組a的元素賦值(下標從0開始),a[i] = harry;
。- Tips:只有i小于或等于數組列表的大小時,才能夠調用list.set(i,x)。例如,下面這段代碼是錯誤的:
ArrayList<Employee> list = new ArrayList<>(100);//capacity is 100,size 0
list.set(0,x);//no element 0 yet
使用add方法為數組添加新元素,而不要使用set方法,它只能替換數組中已經存在的元素內容。
- Tips:只有i小于或等于數組列表的大小時,才能夠調用list.set(i,x)。例如,下面這段代碼是錯誤的:
-
使用:
Employee e = staff.get(i);
,獲得數組列表的元素,等價于:Employee e = a[i];
。- Tips:沒有泛型類時,原始的ArrayList類提供的get方法別無選擇只能返回Object,因此,get方法的調用者必須對返回值進行類型轉換:
Employee e = (Employee)staff.get(i);
。原始的ArrayList存在一定的危險性。它的add和set方法允許接受任意類型的對象。對于這個調用:staff.set(i, new Date());
,編譯不會給出任何警告,只有在檢索對象并試圖對它進行類型轉換時,才會發現有問題。如果使用ArrayList<Employee>,編譯器就會檢測到這個錯誤。
- Tips:沒有泛型類時,原始的ArrayList類提供的get方法別無選擇只能返回Object,因此,get方法的調用者必須對返回值進行類型轉換:
該技巧可以一舉兩得,既可以靈活地擴展數組,又可以方便地訪問數組元素。首先,創建一個數組,并添加所有的元素。
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
}
和for(int i=0; i<staff.size(); i++)
效果相同。
.2類型化與原始數組列表的兼容性
-
假設有遺留代碼:
public class EmployeeDB{
public void update(ArrayList list){...}
public ArrayList find(String query){...}
}
可以將一個類型化的數組列表傳遞給update方法,而不需要進行任何類型轉換。
ArrayList<Employee> staff = ...;
employeeDB.update(staff);
也可以將staff對象傳遞給update方法。- Warning:盡管編譯器沒有給出任何錯誤信息或警告,但是這樣調用并不太安全。在update方法中,添加到數組列表中的元素可能不是Employee類型。在對這些元素進行檢索時就會出現異常。聽起來似乎很嚇人,但思考一下就會發現。這與在Java中增加泛型之前就是一樣的。虛擬級的完整性絕對沒有受到威脅。在這種情形之下,既沒有降低安全性,也沒有受益于編譯時的檢查。
相反的,將一個原始ArrayList賦值給一個類型化ArrayList會得到一個警告。
ArrayList<Employee> result = employeeDB.find(query);//yields warning
使用類型轉換并不能避免出現警告。
ArrayList<Employee> result = (ArrayList<Employee>)employeeDB.find(query);
//yields another warning
這樣將會得到另一個警告信息,被告知類型轉換有誤。這就是Java中不盡如人意的參數類型的限制所帶來的結果。這時候不要做什么,確定不會造成嚴重的后果就可以了。
5.4 對象包裝器與自動裝箱
有時,需要將int這樣的基本類型對象轉換為對象。所有的基本類型都有一個與之對應的類。例如,Integer類對應基本類型int。通常,這些類稱為包裝器(wrapper)。這些對象包裝器類擁有很鮮亮的名字:Integer、Long、Float、Double、Short、Byte、Character、Void和Boolean(前6個派生域公共的超類Number)。對象包裝器類是不可變的,即一旦構造了包裝器,就不允許更改包裝在其中的值。同時,對象包裝器還是final,因此它們不可以定義子類。
假設想定義一個整型數組列表。而尖括號中的類型參數不允許是基本類型。也就是說,不允許寫成ArrayList<int>。這里就用到了Integer對象包裝器類。
ArrayList<Integer> list = new ArrayList<>();Java SE5.0之后的另一個改進之處是更加便于添加或獲得數組元素。這個調用:
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方法。- Tips:自動裝箱規范要求boolean、byte、char<=127,介于-128~127之間的short和int被包裝到固定的對象中。例如,如果前面的例子中將a和b初始化為100,對它們進行比較的結果一定成立。
-
最后強調一下,拆箱和裝箱是編譯器認可的,而不是虛擬機。編譯器在生成類的字節碼時,插入必要的方法調用。虛擬機只是執行這些字節碼。使用數值對象包裝器還有另外一個好處。可以將某些基本方法放置在包裝器中,例如,將一個數字字符串轉換成數值。可以使用:
int x = Integer.parseInt(s);
,這與Integer對象沒有任何關系,parseInt是一個靜態方法。- 包裝器類不可以實現修改參數值的方法,原理同第4章中的值傳遞。
5.5 參數數量可變的方法
Java SE 5.0以前的版本中,每個Java方法都有固定數量的參數。然而,現在的版本提供了可以用個可變的參數數量調用的方法(有時稱為”變參“方法)。
printf方法的定義是這樣的:
public class PrintStream{
public PrintStream print(String fmt, Object...args){return format(fmt,args);}
}
這里的省略號...是Java代碼的一部分,它表明這個方法可以接收任意數量的對象(除fmt參數之外)。實際上,printf方法接收兩個參數,一個是格式字符串,另一個是Object[]數組,其中保存著所以參數(如果調用者提供的是整型數組或者是其他基本類型的值,自動裝箱功能將把它們轉換成對象)。現在將掃描fmt字符串,并將第i個格式說明符域arg[i]的值匹配起來。換句話說,對于printf的實現者來說,Object...參數類型與Object[]完全一樣。編譯器需要對printf的每次調用進行轉換,以便將參數綁定到數組上,并在必要的時候進行自動裝箱:
System.out.printf("%d %s", new Object[]{new Integer(n), "widgets"});
用戶自己也可以定義可變參數的方法,并將參數指定為任意類型,甚至是基本類型。下面是一個簡單的示例:其功能為計算若干個數值的最大值。
public static double max(double...values){
double largest Double.MIN_VALUE;
for(double v : values) if (v > largest) largest = v;
return largest;
}
可以這樣調用這個方法:double m = max(3.1, 40.4, -5);
,編譯器將new double[]{3.1, 40.4, -5}傳遞給max方法。
5.6 枚舉類
例:
public enum Size {SMALL, MEDIUM, LARGE, EXTRA_LARGE};
,實際上,這個聲明定義的類型是一個類,它剛好有4個實例,在此盡量不要構造新對象。因此,在比較兩個枚舉類型的值時,永遠不需要調用equals,直接使用“==”就可以了。如果需要的話,可以在枚舉類型中添加一些構造器、方法和域。當然,構造器只是在構造枚舉常量的時候被調用。下面是一個示例:
public enum Size{
SMALL("S"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL");
private String addreviation;
private Size(String addreviation){this.addreviation = addreviation;}
public String getAddreviation(){return addreviation;}
}所有的枚舉類都是Enum類的子類。它們繼承了這個類的許多方法。其中最實用的是toString,這個方法能夠返回枚舉類常量名。例如,Size.SMALL.toString()將返回字符串“SMALL”。
toString方法的逆方法是靜態方法valueOf。例如,語句:
Size s = Enum.valueOf(Size.class, "SMALL");
,將s設置成Size.SMALL。每個枚舉類型都有一個靜態的values方法,它將返回一個包含全部枚舉類型值的數組。例如:
Size[] values = Size.values();
,將返回包含元素Size.SMALL,Size.MEDIUM,Size.LARGE,Size.EXTRA_LARGE的數組。ordinal方法返回enum聲明中枚舉常量的位置,位置從0開始計數。例如:
Size.MEDIUM.ordinal()
返回1。