Java核心技術--第五章 繼承(更新中)10/9
類、超類和子類
經理類與普通僱員類有很多相同之處,但還有一些差別。
經理在完成本職任務不僅可以獲得工資,還獲得獎金。而普通僱員只能獲取工資。故而,可以重用Employee類中已編寫的部分部分,還可在其中在增加一些新的功能。
每個經理都是一個僱員,是 is a 的關係。 --------繼承的特徵
繼承Employee類來定義Manager類,關鍵字extends表示繼承
class Manager extends Employee{ //所有的繼承都為公有繼承
新增方法和域
}
關鍵字extends :正在構建的新類派生於一個已存在的類
已存在的類----稱為超類、基類或父類
新類----稱為子類
子類比超類的功能更多
Manager類–子類 Employee類—超類
在Manager類中新增一個儲存獎金資訊的域,和用於設定這個域的方法:
class Manager extends Employee{
......
public void setBonus(double b){ //獲取獎金資訊
bonus = b;
}
private double bonus; //獎金資訊
}
Manager boss = new Manager(......);
boss.setBonus(5000);
setBonus方法在Manager類中定義,故Employee類的物件不可使用該方法。
雖然在Manager類中沒有顯示地定義getName和getHireDay等方法,但屬於Manager類的物件可以使用它們,因為Manager類自動繼承了超類Employee中的這些方法
同樣也繼承了name,salary,hireDay這三個域。故Manager類物件包含四個域:name,salary,hireDay,bonus
通用方法在超類中,特殊方法在子類中。
覆蓋超類中的某方法:只有Employee類的方法才能夠訪問Employee中的私有部分(域)
public double getSalary(){ //覆蓋超類的方法 double baseSalary = super.getSalary(); //運用super關鍵字呼叫超類的方法 return baseSalary+bonus; }
在子類中可增加域、增加方法或覆蓋超類的方法,但是不可以刪除繼承的任何域和方法。
super在構造器的應用:
public Manager(String n,double s,int year,int month,int day){
super(n,s,year,month,day); //使用super呼叫構造器的語句必須是子類構造器的第一條語句
bonus = 0;
}
super(n,s,year,month,day) 呼叫超類Employee中含有n,s,year,month和day引數的構造器
Manager類的物件都不能訪問Employee類的私有域,所以必須通過super實現對超類的域/方法的呼叫。
若子類的構造器沒有顯示地呼叫超類的構造器,則將自動呼叫超類(無引數)的構造器。
若超類沒有帶引數的構造器,並且子類的構造器中沒有顯示地呼叫超類的其他構造器,則Java編譯器將報告錯誤。
例:
//建立一新經理,並設定其獎金
Manager boss = new Manager("Carl Cracker",80000,1987,12,15);
boss.setBonus(5000);
//定義一包含3個僱員的陣列
Employee[] staff = new Employee[3];
//經理和僱員都放到陣列中
staff[0] = boss;
staff[1] = new Employee("Harry Hacker",50000,1989,10,1);
staff[2] = new Employee("Tony Tester",40000.1990,3,15);
//輸出每個人薪水
for[Employee e:staff]
System.out.peirnln(e.getName()+" "+e.getSalary());
執行結果
Carl Cracker B5000.0
Harry Hancker 50000.0
Tony Tester 40000.0
staff[1]和staff[2]對應Employee物件,僅輸出基本薪水;staff[0]對應Manager物件,它的getSalary方法將獎金與基本薪水加在一起。
e.getSalary()能夠確定應執行哪個getSalary方法。這裡e宣告為Employee型別,但實際上e既可以引用Employee型別的物件,也可以引用Manager型別的物件。
當e引用Employee物件時,e.getSalary()呼叫的是Employee類中的getSalary方法;當e引用Manager物件時,e.getSalary()呼叫的是Manager類中的getSalary方法。虛擬機器知道e實際引用的物件型別,因此能夠正確地呼叫相應的類方法。
一個物件變數(例:變數e)可以引用多種實際型別的現象稱為多型。在執行時能夠自動地選擇呼叫哪個方法的現象稱為動態繫結。
5.1.1 繼承層次
Employee類–>派生–>Manager類,Secretary類,Programmer類
Manager類–>派生–>Executive類
繼承層次:由一個公共超累派生出來的所有類的集合
類的繼承鏈:在繼承層次中,從某個特定的類到其祖先的路徑
一個祖先類可擁有多個子孫繼承鏈。
5.1.2 多型
Employee e;
e = new Employee(...);
e = new Manager(...);
//一個Employee變數既可以引用一個Employee類物件,
//也可以引用一個Employee類的任何一個子類的物件(例:Manager、Executive等等)。
置換法則:
Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss;
變數staff[0]與boss引用同一個物件。但編譯器將staff[0]看成Employee物件。
boss.bonus(5000); //可以
//boss宣告的型別是Manager,setBonus是Manager類的方法。
staff[0].Bonus(5000); //出錯
//staff[0]宣告的型別是Employee,而setBonus不是Employee類的方法。
不能將一個超類的引用賦給子類變數
Manager m = staff[i]; //出錯
不是所有的僱員都是經理。如果賦值成功,m有可能引用了一個不是經理的Employee物件,當在後面呼叫m.setBonus(…)時就有可能發生執行時錯誤。
注:
Manager[] managers = new Manager[10];
Employee[] satff = managers;
合法,但是這裡將一普通僱員歸入了經理行列。應避免這種引用。
5.1.3 動態繫結
呼叫物件方法的過程:
- 編譯器檢視物件的宣告型別和方法名。獲取所有可能被呼叫的候選方法
可能存在方法f(int),f(String)。編譯器將會一一列舉所有該類中名為f的方法和其超類中訪問屬性為public且名為f的方法。 - 編譯器將檢視呼叫方法時提供的引數型別。獲取需呼叫的方法名字和引數型別
在所有名為f的方法中存在一個與提供的引數型別完全匹配,就選擇該方法—該過程為過載解析。 - 若是private方法、static方法、final方法或者構造器,那麼編譯器將可以準確地知道應該呼叫哪個方法,這種呼叫方式稱為靜態繫結。
- 當程式執行,並且採用動態繫結呼叫方法時,虛擬機器一定呼叫與x所引用物件的實際型別最合適的那個類的方法。
每次呼叫方法都要進行搜尋,時間開銷相當大。因此,虛擬機器預先為每個類建立了一個方法表,其中列出了所有方法的簽名和實際呼叫的方法。呼叫時虛擬機器查此表即可。
當搜尋方法表時,若多型情況下,即可引用超類,也可引用子類。則會搜尋子類和超類的方法表。
Employee的方法表:
Employee:
getName()---->Employee.getName()
getSalary()---->Employee.getSalary()
getHireDAy()---->Employee.getSalary()
raiseSalary(double)---->Employee.raiseSalary(double)
Manager的方法表:
Manager:
getName()---->Employee.getName()
getSalary()---->Manager.getSalary() //重寫
getHireDay---->Employee.getHireDay()
raiseSalary(double)---->Employee.raiseSalary(double)
setBonus(double)---->Manager.setBonus(double) //新增
在執行時,呼叫e.getSalary()的解析過程:
- 先虛擬機器提取e的實際型別的方法表。可能Employee、Manager、或Employee類的其他子類的方法表
- 再虛擬機器搜尋定義getSalary簽名的類。此時,虛擬機器已知道應呼叫哪個方法
- 虛擬機器呼叫方法
5.1.4 阻止繼承:final類和方法
final類:不允許擴充套件的類
阻止定義Executive類的子類:
final class Executive extends Manager{
......
}
類中的方法被宣告為final,則子類不能覆蓋該方法:
class Employee{
......
public final String getName(){
return name;
}
......
}
將方法或類宣告為final可確保他們不會在子類中改變語義。
設計類層次時,應仔細考慮哪些方法和類宣告為final。
內聯:若一個方法沒有被覆蓋且很短,編譯器就能對它進行優化處理。
例:內聯呼叫e.getName()將被替換為訪問e.name域。
若getName在另一個類中被覆蓋,那麼編譯器就不知道覆蓋的程式碼會做什麼操作了,因此不可對其進行內聯處理。
虛擬機器中的即時編譯器可準確知道類之間的繼承關係,並能檢測出類中是否真正地存在覆蓋給定的方法。若方法簡短、被頻繁呼叫且沒真正被覆蓋,則即時編譯器會將方法進行內聯處理。若被覆蓋了,則優化器將取消對覆蓋方法的內聯。該過程很慢且很少發生。
5.1.5 強制型別轉換
Manager boss = (Manager)staff[0]; //staff陣列為Employee物件的陣列
子類的引用可賦值給超類變數,但超類的引用可賦值給子類變數。
但可以用instanceof運算子查一下是否能轉換成功:(在超類轉換為子類之前)
if(staff[1] instanceof Manager){
boss = (Manager) staff;
......
}
若這個轉換不能成功,編譯器將不會進行這個轉換。
一般情況下,應儘量少使用型別轉換和instanceof運算子。
5.1.6 抽象類
抽象----祖先類更加抽象,用於派生其他新類。將通用的屬性、方法放到超類中,有利於繼承。
abstract class Person{ //抽象類
public Person(String){
}
public abstract String getDescription(); //抽象方法
......
}
擴充套件抽象類的兩個方法:
- 在子類中定義部分抽象方法或抽象方法不定義,則必須將子類也標記為抽象類
- 定義全部的抽象方法,子類就不用定義抽象類了
就算類不含抽象方法,也可宣告為抽象類。
抽象類不能例項化------若將一個類宣告為abstract,就不能建立這個類的物件。但可以建立一個具體子類的物件。
介面中會有更多抽象方法見地6章。
5.1.7 受保護訪問
protected : 超類的某些方法允許被子類訪問,或允許子類訪問的方法訪問超類的某個域。
例:
若是將超類Employee中的hireDay宣告為protected,而不是私有的,Manager中的方法就可以直接訪問它。
而且Manager類中的方法只能夠訪問Manager物件中的hireDay域,而不能訪問其他Employee物件中的這個域。這種限制有助於避免濫用保護機制,使得子類只能獲取訪問受保護域的權利。
謹慎使用protected。因其可以對私有的域進行修改,導致其他人員不知,會造成混亂。修改後要進行通知。
Java用於控制可見性的四個訪問修飾符:
1 private-----僅對本類可見
2 public-----對所有類可見
3 protected------對本包和所有子類可見
4 預設------對本包可見 無任何修飾符 不常用
5.2 Object-所有類的超類
Object類是Java中所有類的最終祖先,每個類都有它擴充套件而來。
但不需要寫成:
class Employee extends Object
可用Object型別的變數引用任何型別的物件:
Object obj = new Employee("Harry Hacker",35000);
Employee e =(Employee)obj;
//在具體操作時,還要使用型別轉換,在進行其他操作。
5.2.1 Equals方法
Object類的Equals方法:檢測以物件是否等於另一個物件。====>引用是否相同
比較兩個物件是否相等沒有太大的意義,經常需要比較的是兩個物件的狀態是否相等,若狀態相等了,兩個物件也就是相等的了
例:
若兩個僱員物件的姓名、薪水和僱用日期都是一樣的,就認為他們是相等的。(實際應用中,ID更有意義)
class Employee{
......
public boolean equals(Object otherObject){
if(this==otherObject) return true;
if(this==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方法返回一個物件所屬的類。檢測時,只有兩個物件屬於同一個類事才可能相等。
在子類中定義equals方法時,首先呼叫超累的equals。若失敗,物件則不可能相等;若成功,則繼續比較子類中的實力域。
5.2.2 相等測試與繼承
進行相等測試時,不建議使用:
if(!(otherObject instanceof Employee) return false;
會出現其他麻煩。不建議使用這種方式(返回False的方式)
equals的特性:
- 自反性:
對任意非空引用x,x.equals(x)應返回true - 對稱性
對任意引用x、y,當且僅當y.equals(x),x.equal(y)都應返回true - 傳遞性
對任意x、y、z,若x.equals(y)返回true,y.equals(z)、x.equals(z)也返回true - 一致性
若x、y引用的物件沒有發生變化,則反覆呼叫x.equals(y)返回的結果一致。 - 對任意非空引用x,x.equals(null)應該返回false
很好編寫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是否屬於同一個類。
這裡可使用getClass檢測:
if(getClass() != otherObject.getClass()) return false;
若所有子類都擁有統一的語義,就使用instanceof檢測:
if(!(otherObject insatnceof ClassName)) return false;
5)將otherObject轉換為相應的類型別變數:
ClassName other = (ClassName) otherObject;
6)對所有需要比較的域進行比較,使用==比較型別基本域,使用equals比較物件域。若都匹配,返回true;否則返回false。
return field1 == other.field1
&&field2 .equals(other.field2)
&&......
若在子類中重新定義equals,需呼叫super.equals(other).
static Boolean equals(type[] a,type[] b)
//若兩個陣列長度相同,並且在對應位置上資料元素也均相同,將返回true。資料的元素型別可以是:Object,int,long,short,char,byte,boolean,float或double。
5.2.3 HashCode方法
Hash Code–雜湊碼 :由物件匯出的一整數值,無規律。
HashCode方法定義在Object類中,每個物件都有一個預設的雜湊碼,其值為物件的儲存地址。
例:s,t,sb,st內容一致,只不過s,t為String型別,sb,st為StringBuffer型別
String s = “OK”;
String t = new String("OK");
String sb = new StringBuffer(s);
String tb = new STringBuffer(t);
System.out.println(s.hashCode()+" "+sb.hashCode());
System.out.println(t.hashCode()+" "+tb.hashCode());
輸出:
2556 20526976
2556 20567144
注意:字串s與t擁有相同的雜湊碼,由於字串的雜湊碼是由內容匯出的。
字串緩衝sb與tb卻有不同的雜湊碼,由於String Buffer類中沒有定義hashCode方法,他的雜湊碼是由Object類的預設hashCode方法匯出的物件儲存地址。
若重新定義equals方法,就必須重新定義hashCode方法,以便使用者可將物件插入到散列表中。詳細內容在第二卷第二章。
HashCode方法應該返回一個整數值(可以為負數),併合理組合例項域的雜湊碼,以便使各個不同物件產生的雜湊碼更均勻)
Equals與hashCode的定義必須一致:若x.equals(y)返回true,那麼x.hashCode()就必須與y.hashCode()具有相同的值。
例:若定義的Employee.equals比較僱員的ID,那麼hashCode方法就需要雜湊ID,而不是僱員的姓名或儲存地址。
5.2.4 ToString方法
toString方法:返回物件值的字串
Point類的toString方法返回的字串:java.awt.Point[x=10,y=20]
絕大多數的toString方法使用格式:類名[域值]
Employee類中toString方法的實現:
public String toString(){
return "Employee[name="+name+
",salary="+salary+
",hireDay="+hireDay+
"]";
}
也可將Employee換成getClass().getName()+“name=”+name+…
getClass().getName()-----獲得類名的字串
toString方法子類也可使用
若超類中使用的是getClass().getName(),則在子類中呼叫就要使用super.toString()就可。
class Manager extends Employee{
......
public String toString(){
return super.toString()+"[bonus="bonus+"]";
}
}
輸出
Manager[name=...,salary=...,hireDay=...][bonus=...]
只要物件與一字串通過“+“連線,Java編譯器就會自動呼叫toString方法,獲取該物件的字串描述。
Object類定義了toString方法:輸出物件的類名和雜湊值
例:
System.out.println(System.out);
輸出
[email protected]
因為PrintStream類中沒有覆蓋toString方法
java.lang.Object
Class getClass():返回包含物件資訊的類物件
Object clone():建立一個物件的副本。Java執行時系統將為新例項分配儲存空間,並將當前的物件複製到這塊儲存區域中。
java.lang.Class
String getName():返回這個類的名字
CLass getSuperclass():以Class物件的形式返回這個類的超類資訊
5.3 泛型陣列列表
為解決過大設定陣列的大小造成浪費,Java中定義了ArrayList類,使用起來很像陣列,但在新增或刪除元素時,具有自動調節陣列容量的功能,而不需要為此編寫任何程式碼。
ArrayList是一個採用型別引數的泛型類。ArrayList 指定陣列列表儲存的元素物件型別。自定義泛型類見第十三章。
宣告和構造一個儲存Employee物件的陣列列表:
ArrayList<Employee> staff = new ArrayList<Employee>();
Vector類實現動態陣列,但ArrayList類更加有效,故不再使用Vector類。
add方法:把資料新增到陣列列表中
staff.add(new Employee("Harry Hacker",)...) ;
staff.add(new Employee("Tony Tester",...));
若呼叫add且內部陣列已滿,陣列列表將自動建立一個更大的陣列,並將所有的物件從較小的陣列中拷貝到較大的陣列中。
若能清楚知道或估算出陣列可能的儲存大小,可在填充陣列前呼叫ensureCapacity方法:
staff.ensureCapacity(100);
陣列的大小是為陣列分配100個元素的位置空間,陣列就有100個空位置可使用。
陣列列表的容量為100個元素,只是擁有儲存100個元素的潛力,實際上分配會超過100,但在開始,完成初始化後,資料列表根本不含有任何元素。
size方法:陣列列表中實際包含的元素數目 ==a.length
staff.size();
確認陣列列表的大小不再變化後,呼叫trimToSize方法:將儲存空間的大小調整為當前元素所需的儲存空間的數目。垃圾回收器回收多餘的儲存空間。
調整後再新增新元素需要花時間再次移動儲存塊,所以應該在確認不會再新增任何元素時,再呼叫trimToSize。
5.3.1 訪問陣列列表元素
陣列列表優劣勢:
優勢:陣列列表可自動擴充套件容量
劣勢:訪問元素語法的複雜程度增加了
get、set方法實現訪問或改變陣列元素的操作,而不是 [ ] 格式
staff.set(i,harry);
等價於
a[i] = harry;
add方法是新增新元素;set方法是對已經存在的元素進行替換。故set中i取值為[0,length-1]
既可以靈活擴充套件陣列,又可方便訪問陣列元素:
//建立一個數組,並新增所有元素
ArrayList<X> List = new ArrayList<X> ();
while(...){
x=...;
List.add(x);
}
//使用toArray方法將陣列元素拷貝到一個數組中
X[] a = new X[List.size()];
List.toArray();
//可在陣列列表的尾部、中間插入元素(使用帶索引引數的add方法):
int n = staff.size()/2;
staff.add(n,e);
//插入一新元素,位置n之後的所有元素都想後移動一個位置
//若插入新元素之後,陣列列表的大小超過了容量,陣列列表就會重新分配儲存空間
//從陣列列表中刪除一個元素
Employee e = staff.remove(n);
//位於n之後的所有元素都向前移動一個位置,且陣列大小減一
對小型陣列來說,插入、刪除元素的操作效率較低,但仍可用;對於元素較多的元素,又經常需在中間位置插入、刪除元素,就應該考慮使用連結串列了。有關連結串列的見第十三章。
for each迴圈對陣列列表遍歷:
for(Employee e:staff)
do something with e
將Employee[ ]陣列改為ArrayList:(ArrayList與陣列的不同)
- 不必指出陣列的大小
- 使用add將任意多的元素新增到陣列中
- 使用size()替代length計算元素的數目
- 使用a.get(i)替代a[i]訪問元素
5.3.2 型別化與原始陣列列表的相容性
相容性:編譯器在對型別轉換進行檢查之後,如果沒有發現違反規則的現象,就將所有的型別化陣列列表轉換成原始的ArrayList物件。在程式執行時,所有的陣列列表都是一樣的----沒有虛擬機器中的型別引數。因此,ArrayList和ArrayList的型別轉換將執行相同的執行時檢查。
ArrayList<> a = b ; // b返回一個ArrayList型別的物件 報錯 出現交叉錯誤
5.4 物件包裝器與自動打包
所有的基本型別都有一個與之對應的類:Integer類對應基本型別int 等。
稱這些類為包裝器。
物件包裝器類名字:Integer、Long、Float、Double、Short、Byte、Character、Void和Boolean(前6個類派生於公共的超類Number)。
物件包裝器類是不可變的,一經構造不可更改包裝中的值。因物件包裝器類是final,因此不能定義它們的子類。
在ArrayLIst<>中的<>的引數型別不允許是基本型別,不可寫成ArrayList;就需要用到Integer物件包裝器類。
宣告一個Integer物件的陣列列表:
ArrayList<Integer> list = new ArrayList<Integer>();
注:ArrayList中每個值分別包裝在物件中,所以其效率遠遠低於int[ ]陣列。故應用它構造小型陣列,原因在於程式設計師操作的方便性比執行效率更重要。
自動打包:
//新增或獲取陣列元素時,自動打包
list.add(3);
//將自動變成
list.add(new Integer(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) ... //這種比較通常不會成立
在兩個物件比較時一般應呼叫equals方法。
打包和拆包時編譯器認可的,不時虛擬機器。編譯器在生成類的位元組碼時,插入必要的方法呼叫。虛擬機器只是執行這些位元組碼。
數值物件包裝器:可將某些基本方法放置在包裝器中
//將一個數字字串轉換成數值
int x = Integer.parseInt(s); //parseInt是一個靜態方法。將此方法放在Integer類中是極好的
5.5 引數數值可變的方法
可以用可變的引數數值呼叫的方法-----"可變參"方法
可變引數方法:諸如printf("%d",n)或printf("%d , %s",n,“weidgets”)等
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的每次呼叫進行轉換,以便引數繫結到陣列上,並在必要時進行自動打包。
使用者可自定義可變引數的方法,並將引數指定為任意型別,甚至是基本型別。
例:
//計算若干個數值的最大值
public static double max(double... values){
double largest = Double.MIN_VALUE;
for(double v:values)
if(v>largest)
largest = v;
return largest;
}
//呼叫max方法:
double m = max(3.1,49,3,-5);
編譯器將new double[ ]{3.1,49,3,-5}傳遞給max方法
5.6 列舉類
例:
public enum Size{SMALL,MEDIUM,LARGE,EXTRA_LARGE};
這個宣告定義的型別是一個類,有四個例項。
在比較兩個列舉型別的值時,直接用 == 就可以,不要使用equals
可在列舉型別中新增一些構造器、方法和域。其中構造器只在構造列舉常量時被呼叫。
例:
enum Size{
SMALL("s"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL");
private Size(String abbreviation){ this.abbreviation = abbreviation;}
public String getAbbreviation(){ return abbreviation;}
private String abbreviation;
}
所有的列舉型別都是Enum類的子類。它們繼承了Enum類的許多方法。最有用的一個是toString----獲得列舉常量名
例:
Size.SMALL.toString(); //返回字串“SMALL“
toString的逆方法----靜態方法valueOf
Size s = (SIze) 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
5.7 反射
反射庫----便於編寫可動態操縱Java程式碼的程式。
該功能廣泛應用於JavaBeans中,它是Java元件的體系結構—JavaBeans詳細內容見卷II。 反射可支援Visual Basic使用者習慣使用的工具
特別在設計或執行中新增新類時,能快速應用開發工具動態的查詢新新增類。
反射----能夠分析類能力的程式。
用反射機制可以:
- 在執行中分析類的能力
- 在執行中檢視物件,例:編寫一個toString方法供所有類使用
- 實現陣列的操作程式碼
- 利用Method物件,這個物件很像C++中的函式指標
使用反射的主要物件是工具製造者,而不是應用程式猿。若僅對設計應用程式有興趣,對構造工具不感興趣,可跳過本章剩餘部分,稍後再回來學習。。。
5.7.1 Class類
程式執行期間,Java執行時系統始終為所有的物件維護一個被稱為執行時的型別標識。這個資訊儲存每個物件所屬的類足跡。虛擬機器利用執行時資訊選擇相應的方法執行。
可通過專門的Java類訪問這些資訊,儲存資訊的類被稱為Class。
Class類易於讓人混淆,舉個例子:
Employee e;
...
Class cl = e.getClass();
getClass()方法返回一個Class型別的例項。----獲得Class類物件的第一種方法
和Employee類一樣,Class類中也包含了某些屬性。
最常用的Class方法是getName----返回類的名字
例:
System.out.println(e.getClass().getName()+" "+e.getName());
若e是一個僱員,則輸出
Employee Harry Hacker
若e是經理,則輸出
Manager Harry Hacker
若類在一個包裡,包的名字也作為類名的一部分:
Date d = new Date();
Class c1 = d.getClass();
String name = c1.getName(); //name="java.util.Date"
獲得Class類的第二種方法
//還可以呼叫靜態方法forName獲取類名對應的Class物件
String className = “java.util.Date”;
Class c1 = Class.forName(className);
類名儲存在字串中且在執行中可改變,就可使用該方法。
注:此方法只有在className是類名或介面名時才能夠執行。否則,forName方法將丟擲一個checked exception(已檢查異常)。使用該方法時,應提供一個異常處理器(exception handler),見本節中“捕獲異常”。
獲取Class類的第三種方法
若T是任意的Java型別,T.class將代表匹配的類物件。
例:
Class cl1 = Date.class; //匯入了java.util.*;
Class cl2 = int.class;
Class cl3 = Double[].class;
注:一個Class物件實際上表示的是一個型別,而這個型別未必一定是一種類。諸如int不是類,但int.class是一個Class型別的物件。
警告:getName()在應用於陣列型別時會返回一個奇怪的名字:
Double [ ].class.getName( ) 返回“[Ljava.lang.Double; ”
int[ ].class.getName( ) 返回“[I"
虛擬機器為每個型別管理一個Class物件。可用==運算子對兩個類物件進行比較。例:
if(e.getClass() == Employee.class) ...
newInstance():快速建立一個類的例項—呼叫預設的構造器初始化新建立的物件。若該類沒有預設的構造器,就會丟擲一個異常。
例:
e.getClass().getInstance(); //建立了一個與e有相同類型別的例項
forName+newInstance配合使用:根據儲存在字串中的類名建立一個物件
String s = “java.util.Date”;
Object n = Class.forName(s).newInstance();
5.7.2 捕獲異常
異常處理機制見第十一章。
當程式執行過程中發生錯誤時,就會“丟擲異常”。丟擲異常比終止程式更加靈活,因為有一個“捕獲”異常的處理器對異常情況進行處理。
若沒有提供處理器,程式會終止,並在控制檯上打印出一條資訊,其中給出了異常的型別。例:偶然使用了null引用或者陣列越界等。
異常:未檢查異常 + 已檢查異常
已檢查異常—編譯器將會檢查是否提供了處理器
未檢查異常—例如:訪問null引用。編譯器不會檢視是否為這些錯誤提供了處理器,應編寫程式碼來避免這些錯誤發生。但是不是所有錯誤都可以避免的,若竭盡全力還是發生了異常,編譯器就要求提供一個處理器。Class.forName方法就是一個丟擲已檢查異常的例子。異常處理的策略見第十一章。
最簡單的處理器
//將可能丟擲已檢查異常的一個或多個方法呼叫程式碼放在try塊中,
//然後在catch子句中提供處理器程式碼。
try{
statements that might throw exceptions
}
catch(Exception){
handler action;
}
例:
try{
String name = ...;
Class cl = Class.forName(name);
...
}
catch(Exception e){
e.printStackTrace();
}
若類名不存在,則將跳過try塊中的剩餘程式碼,程式直接進入catch子句(這裡,Throwable類的printStackTrace方法打印出棧的軌跡。Throwable是Exception類的超類)。若try塊中為丟擲任何異常,則跳過catch自居的處理器程式碼。
對於已檢查異常,只需要提供一個異常處理器。
若呼叫了一個丟擲已檢查異常的方法,而又沒有提供處理器,編譯器就會給出錯誤報告。
5.7.3 利用反射分析類的能力
反射機制最重要的內容-------檢查類的結構
java.lang.reflect包中有Field、Method和Constructor分別描述類的域、方法和構造器。
這三個類中都有一個getName方法----返回專案的名稱
都有一個getModifiers方法----返回一個整型數值,用不同的位開關描述public和static這樣的修飾符使用狀況。在Modifier類中。Modifier類中的isPublic、isPrivate或isFind判斷方法或構造器是否是public、private或final。
Field類有一個getType方法----返回描述域所屬型別的Class物件
Method和Constructor類有能夠報告引數型別的方法
Method類還有一報告返回型別的方法
Class類中的getFields、getMethods和getConstructor方法分別返回類提供的public域、方法和構造器陣列,其中包括超類的公有成員。
Class類的getDeclareFields、getDeclareMethods和getDeclareConstructors方法分別返回類中宣告的全部域、方法和構造器,其中包括私有和受保護成員,但不包括超類的成員。
要應用到程式上!!!實踐。。。
5.7.4 在執行時使用反射分析物件
Field類中的get方法:檢視物件域。若f是一個Field型別的物件(例:通過getDeclareFields得到的物件),obj是某個包含f域的類的物件,f.get(obj)將返回一個物件,其值為obj域的當前值
Employee harry = new Employee("Harry Hacker",35000,10,1,1989);
Class cl = harry.getClass(); //Employee
Field f = cl.getDeclareField("name"); //返回cl物件的name域
Object v = f.get(harry); //返回cl物件的name域的值 “Harry Hacker”
該段程式碼中存在一個問題。由於name是一個私有域,所以只有用get方法才能得到所訪問的域的值;否則會丟擲IllegalAccessException。
除非有訪問許可權,否則Java安全機制只允許檢視任意物件有哪些域,而不允許讀取它們的值。
反射機制的預設行為受限於Java的訪問控制。在一個Java程式沒有安全管理器的控制,就可以覆蓋訪問控制。為達到該目的,需呼叫Field、Method或Constructor物件的setAccessible方法。
例:
f.setAccessible(true);
setAccessible方法是AccessibleObject類中的一個方法—是Field、Method和Constructor類的公共超類。為除錯、持久儲存和相似機制提供。
get方法:在檢視String型別的name域時,沒有任何問題;但是當想檢視double型別的salary域時,因為Java中數值型別不是物件,可用Field類中的getDouble方法,也可呼叫get方法。反射機制會自動將這個域值打包到相應的物件包裝器中,此處打包為Double。
f.set(obj,value)-----將obj物件的f域設定為新值