1. 程式人生 > >Java核心技術--第五章 繼承(更新中)10/9

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 動態繫結

呼叫物件方法的過程:

  1. 編譯器檢視物件的宣告型別和方法名。獲取所有可能被呼叫的候選方法
    可能存在方法f(int),f(String)。編譯器將會一一列舉所有該類中名為f的方法和其超類中訪問屬性為public且名為f的方法。
  2. 編譯器將檢視呼叫方法時提供的引數型別。獲取需呼叫的方法名字和引數型別
    在所有名為f的方法中存在一個與提供的引數型別完全匹配,就選擇該方法—該過程為過載解析。
  3. 若是private方法、static方法、final方法或者構造器,那麼編譯器將可以準確地知道應該呼叫哪個方法,這種呼叫方式稱為靜態繫結。
  4. 當程式執行,並且採用動態繫結呼叫方法時,虛擬機器一定呼叫與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()的解析過程:

  1. 先虛擬機器提取e的實際型別的方法表。可能Employee、Manager、或Employee類的其他子類的方法表
  2. 再虛擬機器搜尋定義getSalary簽名的類。此時,虛擬機器已知道應呼叫哪個方法
  3. 虛擬機器呼叫方法

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();   //抽象方法
	......
}

擴充套件抽象類的兩個方法:

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

就算類不含抽象方法,也可宣告為抽象類。
抽象類不能例項化------若將一個類宣告為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的特性:

  1. 自反性:
    對任意非空引用x,x.equals(x)應返回true
  2. 對稱性
    對任意引用x、y,當且僅當y.equals(x),x.equal(y)都應返回true
  3. 傳遞性
    對任意x、y、z,若x.equals(y)返回true,y.equals(z)、x.equals(z)也返回true
  4. 一致性
    若x、y引用的物件沒有發生變化,則反覆呼叫x.equals(y)返回的結果一致。
  5. 對任意非空引用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域設定為新值

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