當編寫一個類時,經常會爲該類定義一些方法,這些方法用以描述該類的行爲方式,那麼這些方法都有具體的方法體。但在某些狀況下,某個父類並不須要實現,由於它只須要當作一個模板,而具體的實現,能夠由它的子類來實現。好比說一個長方體的表面積和一個立方體的表面積計算方式是有區別的,長方體表面積須要有三個參數,而立方體須要一個參數。
抽象方法能夠只有方法簽名,而沒有方法實現。java
抽象方法和抽象類必須使用abstract修飾符來定義,有抽象方法的類只能被定義成抽象類,抽象類裏能夠沒有抽象方法。
抽象方法和抽象類的規則以下:
1.抽象類必須使用abstract修飾符來修飾,抽象方法也必須使用abstract修飾符來修飾,抽象方法不能有方法體。連大括號都不能用
2.抽象類不能被實例化(實例化是爲了調用屬性和方法,抽象類自己沒有方法實現的),沒法使用new關鍵字來調用抽象類的構造器來初始化抽象類的實例。即便抽象類裏不包含抽象方法,這個抽象類也不能建立實例。
3.抽象類能夠包含屬性、方法(普通方法和抽象方法均可以)、構造器、初始化塊、內部類、枚舉類六種成分。抽象類的構造器不能用於建立實例,主要用於被其子類調用
4.含義抽象方法的類(包括直接定義了一個抽象方法:繼承了一個抽象父類,但沒有徹底實現父類包含的抽象方法;以及實現了一個接口,但沒有徹底實現接口包含的抽象方法三種狀況)只能被定義成抽象類。
根據定義規則:普通類不能包含抽象方法,而抽象類不只能夠包含普通方法,也能夠包含抽象方法。程序員
abstract class 類名稱{ 屬性; 權限類型 返回值類型 方法名稱(參數類別 參數列表){ } 權限類型 abstract 返回值類型 抽象方法名稱(參數類型 參數列表) }
注意些抽象方法時候:只能是聲明,後面連大括號{}都不能跟。大括號裏面哪怕什麼都沒有,也表示是空語句,不是抽象方法。web
一個抽象類不能用final來聲明,由於抽象類是須要繼承來覆寫抽象類的方法,可是final關鍵字意味着抽象類不能有子類,顯然矛盾。數據庫
抽象類的抽象方法也不能用private聲明,由於抽象類的抽象方法須要子類覆寫,使用private修飾的話,子類沒法覆寫抽象方法。編程
抽象方法和空方法體的方法不是同一個概念。例如public abstract void test();是一個抽象方法,它根本沒有方法體,即方法定義後面沒有一對花括號;但public void test(){}方法是一個普通方法,它已經定義了一個方法體,只是方法體爲空,方法體上面也不作,這個方法不能用abstract來修飾。
abstract不能用於修飾屬性,不能用於修飾局部變量,即沒有抽象屬性、抽象變量的說法,abstract也不能用於修飾構造器,抽象類裏定義的構造器只能是普通構造器。
除此以外,當使用static來修飾一個方法時,表面這個方法屬於當前類,即該方法能夠經過類來調用,若是該方法被定義成抽象方法,則將致使經過該類來調用該方法時出現錯誤(調用了一個沒有方法體的方法確定會引發錯誤),所以static和abstract不能同時修飾某個方法,即沒有所謂的類抽象方法。
abstract關鍵字修飾的方法必須被其子類重寫纔有意義,不然這個方法將永遠不會有方法體,所以abstract方法不能定義爲private訪問權限,即private和abstract不能同時使用。windows
package chapter6; import java.util.Scanner; public class Circle extends Shape{ private double radius; public Circle(String color,double radius){ super(color); if(radius > 0){ this.radius = radius; } } public void setRadius(double radius){ if(radius > 0){ this.radius = radius; } } //重寫計算園周長的方法 public double calPerimeter(){ return 2 * Math.PI * radius; } public String getType(){ return getColor() + "圓"; } public static void main(String[] args){ Triangle s1 = new Triangle("紅色",3,4,5); Circle s2 = new Circle("紫色",5); System.out.println(s1.getType() + " 周長是" + s1.calPerimeter()); System.out.println(s2.getType() + " 周長是" + s2.calPerimeter()); } }
從前面的實例能夠看出,抽象類不能建立實例,它只能當成父類被繼承。從語義角度來看,抽象類是從多個具體類抽象出來的父類,它具備更高層次的抽象。從多個具備相同特徵的類中抽象出一個抽象類,以這個抽象類做爲其子類的模板,從而避免了子類設計的隨意性。
抽象類提醒的就是一種模板模式的設計,抽象類做爲子類的模板,子類在抽象類的繼承上進行擴展和改造。
若是編寫一個抽象父類,父類提供了多個子類的通用方法,並把一個或多個方法留給子類實現,這就是一種模板模式,模板模式也是最多見、最簡單的設計模式之一。如前面的Shape、Circle和Triangle就是使用這種模式。設計模式
抽象類是從多個類抽象出來的模板,若是將這種抽象進行得更完全,則能夠提煉出一種更加特殊的"抽象類"——接口(interface),接口裏不能包含普通方法,接口裏全部方法都是抽象方法。數組
常常據說接口,好比PCI接口、AGP接口,所以不少人認爲接口等同於主機板上的插槽,這是一種錯誤的認識。當咱們說PCI接口時,指的是主機板上那條插槽遵照的了PCI規範,而具體的PCI插槽只是PCI接口的實例。
對於不一樣型號的主機板而言,它們各自的PCI插槽都須要遵照一個規範,遵照這個規範就能夠保證插入該插槽裏的板卡能與主機板正常通訊。對於同一個型號的主機板而言,它們的PCI插槽都須要相同的數據交換方式、相同的實現細節,它們都是同一個類的不一樣實例。
下圖展現了三種層次的關係:接口層次、類層次和對象層次網絡
從上圖能夠看出,同一個類的內部狀態數據,各類方法的實現細節徹底相同,類是一種具體實現體。而接口定義了一種規範,接口定義某一批類所須要遵照的規範,接口不關心這些類的內部狀態數據,也不須要關係這些類裏方法的實現細節。它只規定這批類裏必須提供某些方法,提供這些方法的類就可知足實際須要。
可見,接口是從多個類似類中抽象出來的規範,接口不提供任何實現。接口體現的是規範和實現分離的設計這些。閉包
讓規範和實現分離是接口的好處,讓軟件系統的各組件之間面向接口耦合,是一種鬆耦合的設計。例如主機板上提供了PCI插槽,只要一塊顯卡遵照PCI接口規範,就能夠插入PCI插槽內,與該主板正常通訊。至於這塊顯卡是哪一個廠家製造的,內部若是實現,主機板無需關心。
相似的,軟件系統的各模塊之間也應該採用面向接口的耦合,儘可能下降各模塊之間的耦合,爲系統提供更好的可擴展性和可維護性。
所以接口定義的是多個類共同的公共行爲規範,這些行爲是與外部交流的通道,這就意味着接口裏一般是定義一組共用方法。
和類定義不一樣,定義接口再也不使用class關鍵字,而是使用interface關鍵字,接口定義的基本語法是:
修飾符 interface 接口名 extends 父接口1,父接口2...{ 零到多個常量定義... 領個到多個抽象方法定義... }
語法的詳細說明以下:
1.修飾符能夠是public或者省略,若是省略了public訪問控制符,則默認採用包權限訪問控制符,即只有在相同包結構下才能夠訪問該接口。
2.接口名應與類名採用相同的命名規則,即若是僅從語法角度來看,接口名只要合法便可,但實際是須要是有意義。
3.一個接口能夠有多個直接父接口,但接口只能繼承接口,不能繼承類。
接口是一種規範,所以接口裏不能包含構造器和初始化塊定義,接口裏只能包含常量屬性、抽象實例方法(不能有static)、內部類(包括內部接口)和枚舉類定義。
因爲接口是一種公共規範,所以接口裏面的全部成員包括常量、抽象方法、內部類和枚舉類都是public訪問權限。定義接口成員時候,能夠省略控制修飾符,但若是指定,則只能是public。對於接口裏定義的常量屬性而言,它們是接口相關的,它們只能是常量,所以系統會自動爲這些屬性增長static和final兩個修飾符。也就是說,在接口定義屬性時,無論是否使用publicstatic final修飾符,接口裏的屬性總將使用這三個修飾符來修飾。並且因爲接口裏沒有構造器和初始化塊,所以接口裏定義的屬性只能定義時指定默認值。
接口裏定義屬性採用以下兩行代碼的結果徹底同樣:
int MAX_SIZE = 50; public static final int MAX_SIZE = 50;
接口定義的抽象方法修飾符是public abstract,不能有static修飾,內部類和枚舉類都默認採用public static兩個修飾符,無論定義時是否只能這兩個修飾符,系統自動使用public static 對它們進行修飾。
package chapter6; public abstract interface Output { //接口定義的屬性只能是常量 int MAX_CACHE_LINE = 50; //接口裏定義只能是抽象方法 void out(); void getData(String name); } package chapter6; public class TestOutputProperty { public static void main(String[] args){ //訪問另外一個包中的屬性 System.out.println(chapter6.Output.MAX_CACHE_LINE); //不能從新賦值,由於接口的屬性都是final的 //Output.MAX_CACHE_LINE = 3; } }
接口的繼承和類繼承不一樣,接口徹底支持多繼承,即一個接口能夠有多個直接父接口。和類繼承類似,子接口擴展某個父接口,將得到父接口的全部抽象方法、常量屬性、內部類和枚舉類定義。
一個接口繼承多個父接口時,多個父接口排在extends關鍵字以後,多個父接口之間以英文逗號隔開。
package chapter6; interface interfaceA{ int PROP_A = 5; void test_A(); } interface interfaceB{ int PROP_B = 6; void test_B(); } interface interfaceC extends interfaceA,interfaceB{ int PROP_C = 6; } public class TestInterfaceExtends { public static void main(String[] args){ System.out.println(interfaceA.PROP_A); System.out.println(interfaceB.PROP_B); System.out.println(interfaceC.PROP_C); } }
接口不能用於建立實例,但接口能夠用於聲明引用類型的變量。當使用接口來聲明引用類型的變量時,這個引用類型的變量必須引用到其實現類的對象。除此以外,接口主要用途是被實現類實現。
一個類能夠實現一個或多個接口,繼承使用extends關鍵字,實現則使用implements關鍵字。由於一個類能夠實現多個接口,這也是Java爲單繼承靈活性不足所作的補充。類實現接口的語法格式:
修飾符 class 類名 extends 父類 implements 接口1,接口2...{ 類體部分 }
實現接口與繼承父類類似,同樣能夠得到所實現接口裏定義常量屬性、抽象方法、內部類和枚舉類定義。
讓類實現接口須要類定義後增長implements部分,當須要實現多個接口時,多個接口之間以英文逗號(,)隔開。一個類能夠繼承一個父類,並同時實現多個接口,implements部分必須放在extends部分以後。
一個類實現了一個或多個接口以後,這個類必須徹底實現這些接口裏所定義的所有抽象方法(也就是重寫這些抽象方法);不然,該類將保留從父接口那裏繼承到的抽象方法,該類也必須定義抽象類。下面看一個實現接口的類。
import lee.Output; //定義一個Product接口 interface Product { int getProduceTime(); } //讓Printer類實現Output和Product接口 public class Printer implements Output , Product { private String[] printData = new String[MAX_CACHE_LINE]; //用以記錄當前需打印的做業數 private int dataNum = 0; public void out() { //只要還有做業,繼續打印 while(dataNum > 0) { System.out.println("打印機打印:" + printData[0]); //把做業隊列總體前移一位,並將剩下的做業數減1 System.arraycopy(printData , 1 , printData, 0, --dataNum); } } public void getData(String msg) { if (dataNum >= MAX_CACHE_LINE) { System.out.println("輸出隊列已滿,添加失敗"); } else { //把打印數據添加到隊列裏,已保存數據的數量加1。 printData[dataNum++] = msg; } } public int getProduceTime() { return 45; } public static void main(String[] args) { //建立一個Printer對象,當成Output使用 Output o = new Printer(); o.getData("輕量級Java EE企業應用實戰"); o.getData("瘋狂Java講義"); o.out(); o.getData("瘋狂Android講義"); o.getData("瘋狂Ajax講義"); o.out(); //建立一個Printer對象,當成Product使用 Product p = new Printer(); System.out.println(p.getProduceTime()); //全部接口類型的引用變量均可直接賦給Object類型的變量 Object obj = p; } }
實現接口方法時,必須使用public訪問控制修飾符,由於接口裏的方法都是public的,而子類(至關於實現類)重寫父類方法時訪問權限只能更大或相等,因此實現類實現接口裏的方法時只能使用public訪問權限。
接口不能顯式繼承任何類,但全部接口類型的引用變量均可以直接賦給Object類型的引用變量。因此上面的程序能夠把Product類型變量直接賦給Object類型的變量,這是利用向上轉型實現,由於編譯器知道任何Java對象都必須是Object或其子類的實例。
接口和抽象都很像,它們都具備以下特徵:
1. 接口和抽象類都不能被實例化,它們都位於繼承樹的頂端,用於被其餘類實現和繼承。
2. 接口和抽象類能夠包含抽象方法,實現接口或繼承抽象類的普通子類都必須實現這些抽象方法。
但接口和抽象類之間的差異很大,這種差異主要體如今兩者設計目的上,下面具體分析兩者的差異。
接口做爲系統與外界交互的窗口,接口體現的是一種規範。對於接口的實現者而言,接口規定了實現者必須向外提供哪些服務(以方法的形式來提供);對於接口的調用者而言,接口規定了調用者能夠調用哪些服務,當在一個程序中使用接口時,接口是多個模塊間的耦合標準;當在多個應用程序之間使用接口時,接口是多個程序之間的通訊標準。
但接口和抽象類之間的差異很是大,這種差異主要體如今兩者的設計目的上,下面具體分析二者的差異。
接口做爲系統與外界交互的窗口,接口體現的是一種規範。對於接口的實現者而言,接口規定了實現者必須向外提供哪些服務(以方法的形式來提供);對於接口的調用者而言,接口規定了調用者能夠調用哪些服務,以及如何調用這些服務。當在一個程序中使用接口時,接口是多個模塊間的耦合標準;當在多個應用程序之間使用接口時,接口是多個程序之間的通訊標準。
從某種程度上來看,接口相似於整個系統的總綱,制定了系統各模塊應該遵循的標準,所以一個系統中的接口不該該常常改變。一旦接口被改變,對整個系統甚至其餘系統的影響將是輻射式的,致使系統中大部分類都須要改寫。
抽象類則不同,抽象類做爲系統中多個子類的共同父類,它所體現的是一種模板式設計。抽象父類做爲多個子類的抽象父類,能夠被當成系統實現過程當中的中間產品,這個中間產品已經實現了系統的部分功能(那些已經提供實現的方法),但這個產品依然不能當成最終產品,必須有跟進一步的完善,這種完善可能有幾種不一樣方式。
除此以外,接口和抽象類在用法上也存在以下區別。
1. 接口裏只能包含抽象方法,不包含已經提供實現的方法;抽象類則徹底能夠包含普通方法。
2. 接口裏不能定義靜態方法;抽象類裏能夠定義靜態方法。
3. 接口裏只能定義靜態常量屬性,不能定義普通屬性;抽象類裏則既能夠定義普通屬性,也能夠定義靜態常量屬性。
4. 接口不包含構造器;抽象類裏能夠包含構造器,抽象類裏的構造器並非用於建立對象,而讓其子類調用這些構造器來完成屬於抽象類的初始化操做。
5. 接口裏不能包含初始化塊,但抽象類則徹底能夠包含初始化塊。
6. 一個類最多隻能有一個直接父類,包括抽象類;但一個類能夠直接實現多個接口,經過實現多個接口能夠彌補Java單繼承的不足。
接口體現的是一種規範和設計分離的設計哲學,充分利用接口能夠極好地下降程序各模塊之間的耦合,從而提升系統的可擴展性和可維護性。
基於這種原則,不少軟件架構設計理論都倡導面向接口編程,而不是面向實現類編程,但願經過面向接口編程來下降程序的耦合。下面將介紹兩種經常使用場景來示範面向接口編程的優點。
有這樣的場景,假設程序中有個Computer類須要組合一個輸出設備,如今又兩個選擇:直接讓Computer該類組合衣櫃Printer屬性,或者讓Computer組合一個Output屬性,採用哪一種方式更好呢?
假設讓Computer組合一個Printer屬性,若是有一天系統須要重構,須要使用BetterPrinter來代替Printer,因而須要打開Computer類源代碼進行修改。若是系統中只有一個Computer類組合了Printer屬性,那麼咱們只須要修改這一個Computer類就能夠了,可是若是有1000個、10000個類甚至更多的類,那麼就須要一個個打開這麼多文件進行修改(並非這麼多類都放在一個word文檔裏面進行批量替換這麼簡單的)。這種工做量很是之大。爲了不這個問題,咱們讓Computer組合一個Output屬性,將Computer類與Printer類徹底分離。Computer對象實際組合的是Printer對象,仍是BetterPrinter對象,對Computer而言是屏蔽的,由Output來進行耦合Computer和Output屬性
理清一下思路:
假設如今一個應用程序要打印一份文件,那麼這個應用程序能夠調用windows平臺開放的打印程序,而一臺計算機上能夠鏈接多個不一樣類型的打印機,這樣打印程序就能夠接口的形式來調用具體實現打印功能的類
示例:
下面這個程序能夠當作windows平臺的打印程序,用於開放給應用程序
package chapter6; public class Computer { private Output out; public Computer(Output out){ this.out = out; } //定義一個模擬字符串輸入的方法 public void keyIn(String msg){ out.getData(msg); } public void print(){ out.out(); } }
上面的程序已經將Computer類和Printer類相分離,用Output接口來耦合,也就是使用Output來生產(建立)Printer類對象來供Computer類來使用。再也不用Computer來生產。
package chapter6; public class OutputFactory { //製造Printer對象 public Output getOutput(){ return new Printer(); } public static void main(String[] args){ OutputFactory of = new OutputFactory(); Computer c = new Computer(of.getOutput()); c.keyIn("瘋狂Java講義"); c.keyIn("簡明德語語法"); c.print(); } } public class Printer implements Output{ private String[] printData = new String[MAX_CACHE_LINE]; //記錄當前須要打印的頁數 private int dataNum = 0; public void out(){ //只要頁數大於0繼續打印 while(dataNum > 0){ System.out.println("打印機打印:" + printData[0]); //做業隊列總體前移一位,並將剩下的做業減1 System.arraycopy(printData, 1, printData, 0, --dataNum); } } public void getData(String msg){ if(dataNum >= MAX_CACHE_LINE){ System.out.println("輸出隊列已滿,添加失敗"); }else{ printData[dataNum++] = msg; } } }
咱們能夠把上面的Printer代碼修改爲BetterPrinter,而且在OutputFactory程序中修改製造對象。
有這樣的場景:某個方法須要完成某個行爲,但這個行爲的具體實現沒法肯定,必須等到執行該方法時才能夠肯定。具體一點:假設有個方法須要遍歷某個數組的數組元素,但沒法肯定在遍歷數組時候還須要作什麼操做,須要在調用該方法時指定具體的處理行爲。這個看起來很奇怪,難道調用函數時候,可以把行爲做爲一個參數?在某些編程語言,如Ruby,這種特性是支持的,在Java裏面不支持,可是能夠間接作到。
Java是不容許代碼塊單獨存在的,所以可使用一個Command接口來定義一個方法來封裝處理行爲。
package chapter6; public interface Command { //定義封裝抽象的行爲 void process(int[] target); } package chapter6; public class ProcessArray { public void process(int[] target,Command cmd){ cmd.process(target); } }
經過一個Command類,就實現了讓ProcessArray類和具體處理行爲相分離,程序使用Command接口處理表明處理數組的處理行爲。Command接口也沒有提供真正的處理,只有等到須要調用ProcessArray對象的process方法時,才真正傳入一個Comman對象,才肯定對數組的處理行爲。
package chapter6; public class PrintCommand implements Command{ public void process(int[] target){ for(int temp:target){ System.out.println("迭代輸出的數組是:" + temp); } } } package chapter6; public class AddCommand implements Command{ public void process(int[] target){ int sum = 0; for(int temp:target){ sum += temp; } System.out.println("數組元素總和是:" + sum); } } package chapter6; public class TestCommand { public static void main(String[] args){ ProcessArray pa = new ProcessArray(); int[] target = {3,4,9}; //第一次處理數組 pa.process(target, new PrintCommand()); System.out.println("---------------"); //第二次處理數組 pa.process(target, new AddCommand()); } }
大部分時候,咱們把類定義成一個獨立的程序單元。在某些狀況下,咱們把一個類放在另外一個類的內部定義,這個定義在其餘類內部的類被稱爲內部類(也被稱爲嵌套類),包含內部類的類也被稱爲外部類(有的地方也叫宿主類)。內部類有以下做用。
1.內部類提供了更好的封裝,能夠把內部類隱藏在外部類以內,不容許同一個包中的其餘類訪問該類。假設須要建立一個Cow類,Cow類須要組合一個CowLeg屬性,CowLeg類只有在Cow類裏纔有效,離開Cow類以後沒有任何意義。這種狀況下就能夠把CowLeg類定義成Cow的內部類,不容許其餘類訪問CowLeg
2.內部類成員能夠直接訪問外部類的私有數據,由於內部類被當成其外部類成員,同一個類的成員之間能夠互相訪問,但外部類不能訪問內部類的實現細節,例如內部類的屬性。
3.匿名內部類適合用於建立那些僅須要一次使用的類。對於前面介紹的命令模式,當須要傳入一個Command對象時,從新專門定義PrintCommand和AddCommand兩個實現類可能沒有太大的意義,由於這兩個實現類可能僅須要使用一次。這種狀況下,匿名內部類使用更方便。
1 非靜態內部類 2 public class OuterClass{ 3 //此處定義內部類 4 }
大部分時候,內部類被做爲成員內部類定義,而不是做爲局部內部類。成員內部類是一種與屬性、方法、構造器和初始化塊類似的類成員;局部內部類和匿名內部類則不是類成員。
成員內部類分爲兩種:靜態內部類和非靜態內部類,使用static修飾的成員內部類是靜態內部類,沒有使用static修飾的成員內部類是非靜態內部類。
1 package chapter6; 2 3 public class Cow { 4 private double weight; 5 //外部類的兩個重載構造器 6 public Cow(){ 7 8 } 9 public Cow(double weight){ 10 this.weight = weight; 11 } 12 //定義一個非靜態內部類 13 private class CowLeg{ 14 //非靜態內部類的兩個屬性 15 private double length; 16 private String color; 17 //非靜態內部類的兩個構造器 18 public CowLeg(){ 19 20 } 21 public CowLeg(double length,String color){ 22 this.length = length; 23 this.color = color; 24 } 25 public void setLength(double length){ 26 this.length = length; 27 } 28 public double getLength(){ 29 return length; 30 } 31 public void setColor(String color){ 32 33 } 34 public String getColor(){ 35 return color; 36 } 37 //非靜態內部類的實例方法 38 public void info(){ 39 System.out.println("當前牛腿的顏色是: " + color + " 高:" + length); 40 //直接訪問外部類屬性 41 System.out.println("本牛腿所在的奶牛體重:" + weight); 42 } 43 } 44 //用內部類建立對象 45 public void test(){ 46 CowLeg cl = new CowLeg(1.1,"黑白相間"); 47 cl.info(); 48 } 49 public static void main(String[] args){ 50 Cow c = new Cow(380); 51 c.test(); 52 } 53 } 54 輸出結果: 55 當前牛腿的顏色是: 黑白相間 高:1.1 56 本牛腿所在的奶牛體重:380.0
從上面的結果能夠看出:
1.內部類和類的屬性同樣,有多種做用域,當定義成private時,訪問級別是類內部,內部類能夠訪問外部類的屬性、方法,外部類裏的方法也能夠訪問內部類的屬性、方法
2.內部類的使用和普通類使用沒有區別。
3.編譯程序能夠看出生成兩個class文件,一個是Cow.class一個是Cow$CowLeg.class,前者是外部類的Cow的class文件,後者是內部類的class文件。
當在非靜態內部類的方法內訪問某個變量時,系統優先在該方法內查找是否存在該名字的局部變量,若是存在該名字的局部變量,就使用該變量;若是不存在,則到該方法所在的內部類中查找是否存在該名字的屬性,若是存在則使用該屬性;若是不存在,則到該內部類所在的外部類中查找是否存在該名字的屬性,若是存在則使用該屬性。若是依然不存在,系統將出現編譯錯誤:提示找不到該變量。
所以,若是外部類屬性、內部類屬性與內部類裏方法的局部變量同名,則可經過使用this、外部類類名.this做爲限定來區分。
1 package chapter6; 2 3 public class DiscernVariable { 4 private String prop = "外部類屬性"; 5 private class InnerClass{ 6 private String prop = "內部類屬性"; 7 public void info(){ 8 String prop = "局部變量屬性"; 9 //經過外部類.this.外部類屬性名訪問外部類屬性 10 System.out.println("外部類屬性值:" + DiscernVariable.this.prop); 11 //經過this.內部屬性名訪問內部屬性 12 System.out.println("內部類屬性值:" + this.prop); 13 //訪問方法內的局部變量值 14 System.out.println("局部屬性值:" + prop); 15 } 16 } 17 public void test(){ 18 InnerClass in = new InnerClass(); 19 in.info(); 20 } 21 public static void main(String[] args){ 22 new DiscernVariable().test(); 23 } 24 }
非靜態內部類的成員能夠訪問外部類的private成員,但反過來就不成立了。非靜態內部類的成員只在非靜態內部類範圍內是可知的,並不能被外部類直接使用。若是外部類須要訪問非靜態內部類成員,則必須顯式建立非靜態內部類對象來調用訪問其實例成員。
1 public class Outer 2 { 3 private int outProp = 9; 4 class Inner 5 { 6 private int inProp = 5; 7 public void acessOuterProp() 8 { 9 //非靜態內部類能夠直接訪問外部類的成員 10 System.out.println("外部類的outProp值:" 11 + outProp); 12 } 13 } 14 public void accessInnerProp() 15 { 16 //外部類不能直接訪問非靜態內部類的實例Field, 17 //下面代碼出現編譯錯誤 18 //System.out.println("內部類的inProp值:" + inProp); 19 //如需訪問內部類的實例Field,必須顯式建立內部類對象 20 System.out.println("內部類的inProp值:" 21 + new Inner().inProp); 22 } 23 public static void main(String[] args) 24 { 25 //執行下面代碼,只建立了外部類對象,還未建立內部類對象 26 Outer out = new Outer(); //① 27 out.accessInnerProp(); 28 } 29 }
非靜態內部類對象必須寄存在外部類對象裏,而外部類對象則沒必要必定有非靜態內部類對象寄存其中。簡單的說,若是存在一個非靜態內部類對象,則必定存在一個被它寄存的外部類對象。但外部類對象存在時,外部類對象裏不必定寄存了非靜態內部類對象。所以外部類對象訪問非靜態內部類成員時,可能非靜態普通內部類對象根本不存在。而非靜態內部類對象訪問外部類成員時,外部類對象必定是存在的。
根據靜態成員不能訪問非靜態成員的規則,外部類的靜態方法、靜態代碼塊不能訪問非靜態內部類,包括不能使用非靜態內部類定義變量,建立實例等。總之,不能在外部類的靜態成員中直接使用非靜態內部類。
1 public class InnerNoStatic 2 { 3 private class InnerClass 4 { 5 /* 6 下面三個靜態聲明都將引起以下編譯錯誤: 7 非靜態內部類不能有靜態聲明 8 */ 9 static 10 { 11 System.out.println("=========="); 12 } 13 private static int inProp; 14 private static void test(){} 15 } 16 }
若是使用static來修飾一個內部類,則這個內部類變成是外部類相關的,屬於整個外部類,而不是單獨屬於外部類的某個對象。所以使用static修飾的內部類被稱爲類內部類,有的地方稱爲靜態內部類。
static關鍵字的做用是把類的成員變成類相關,而不是實例相關,即static修飾成員是屬於整個類,而不是屬於單個對象。外部類的上一級程序單元是包,因此不可以使用static修飾;而內部類的上一級程序單元是外部類,使用static修飾能夠講內部類變成外部類相關,而不是外部類實例相關,所以static關鍵字不可修飾外部類,能夠修飾內部類。
靜態內部類能夠包含靜態成員,也能夠包含非靜態成員。靜態內部類不能訪問外部類實例成員,只能訪問外部類成員。即便靜態內部類的實例方法也不能訪問外部類的實例成員,只能訪問外部類的靜態成員。
外部類不能直接訪問靜態內部類成員,但可使用靜態內部類的類名做爲調用者來訪問靜態內部類的類成員,也可使用靜態內部類的對象做爲調用者來訪問靜態內部類的實例成員。
1 package chapter6; 2 3 public class AccessStaticInnerClass { 4 static class InnerClass{ 5 private static String prop1 = "Hello"; 6 private int prop2 = 3; 7 } 8 public void AccessStaticInnerClass(){ 9 //直接訪問報錯誤 10 //System.out.println(prop1); 11 //經過內部靜態類名訪問 12 System.out.println(InnerClass.prop1); 13 //經過建立實例來訪問內部類屬性 14 System.out.println(new InnerClass().prop2); 15 } 16 17 }
除此以外,Java還容許在接口裏定義內部類,接口裏定義的內部類默認使用public static修飾,也就是說接口內部類只能是靜態內部類。
若是爲接口內部類指定訪問控制符,則只能指定public訪問控制符。若是定義接口內部類時省略訪問控制符,則該內部類默認是public訪問控制權限。
接口裏也能夠定義內部接口,但這種作法用處不大,接口裏的內部接口是接口的成員,接口的做用是定義一個公共規範,若是定義成一個內部接口,那麼意義不大。
定義類的主要做用就是定義變量、建立實例和做爲父類被繼承。定義內部類的主要做用也如此。但使用內部類定義變量和建立實例則與外部類存在一些小小的差別。下面分三種狀況討論內部類的用法。
在前面程序中能夠看出,在外部類的內部使用類時,與日常使用普通類沒有太大區別。同樣能夠直接經過內部類類名來定義變量,經過new 調用內部類構造器來建立實例。
惟一存在的一個區別是:不要在外部類的靜態成員(靜態方法和靜態初始化塊)中使用非靜態內部類,由於靜態成員不能訪問非靜態成員。
在外部類內部定義內部類的子類與日常定義子類也沒有太大的區別。
若是但願在外部類之外的地方訪問內部類(包括靜態和非靜態兩種),則內部類不能使用private訪問控制權限,private修飾的內部類只能在外部類內部使用。對於使用其餘訪問控制符修飾的內部類,則能在訪問控制符對應訪問權限內使用。
1.省略訪問控制符的內部類,只能被與外部類處於同一個包中其餘類所訪問。
2.使用protected修飾的內部類:可被與外部類處於同一個包中其餘類和外部類的子類訪問
3.使用public修飾的內部類:可在任何地方被訪問。
在外部類之外地方來使用內部類的語法是:
OuterClass.InnerClass varName
也就是要用相對完整的類名,若是在別的包外,還須要包名。
由於非靜態內部類的對象必須寄存在外部類的對象裏,所以建立非靜態內部類對象以前,必須先建立其外部類對象。在外部類之外的地方建立非靜態內部類實例的語法以下:
OuterInstance.new InnerConstructor()
也就是說,在外部類之外的地方建立非靜態內部類實例必須使用外部類實例和new來調用非靜態內部類構造器。
1 class Out 2 { 3 //定義一個內部類,不使用訪問控制符, 4 //即只有同一個包中其餘類可訪問該內部類 5 class In 6 { 7 public In(String msg) 8 { 9 System.out.println(msg); 10 } 11 } 12 } 13 public class CreateInnerInstance 14 { 15 public static void main(String[] args) 16 { 17 Out.In in = new Out().new In("測試信息"); 18 /* 19 上面代碼可改成以下三行代碼: 20 使用OutterClass.InnerClass的形式定義內部類變量 21 Out.In in; 22 建立外部類實例,非靜態內部類實例將寄存在該實例中 23 Out out = new Out(); 24 經過外部類實例和new來調用內部類構造器建立非靜態內部類實例 25 in = out.new In("測試信息"); 26 */ 27 } 28 }
若是須要在外部類之外的地方建立非靜態內部類的子類,尤爲須要注意上面的規則:非靜態內部類的構造器必須經過其外部類對象來調用。
咱們知道:當建立一個子類時,子類構造器總會調用父類的構造器,所以在建立非靜態內部類的子類時,必須保證讓子類的構造器能夠調用非靜態內部類的構造器,調用非靜態內部類的構造器時,必須存在一個外部類的對象。下面程序定義了一個子類繼承了Out類的非靜態內部類In
1 public class SubClass extends Out.In{ 2 //顯式定義SubClass的構造器 3 public SubClass(Out out){ 4 //經過傳入Out對象顯式調用In的構造器 5 out.super("hello"); 6 } 7 }
上面的代碼能夠看出若是要建立一個SubClass對象,必須先建立一個Out對象。由於SubClass是非靜態內部類In的子類,非靜態內部類In對象裏必須有一個對Out對象的引用。其子類SubClass對象裏也應該有一個Out對象的引用。
非靜態內部類In對象和SubClass對象都必須保留有指向Outer對象的引用,區別是建立兩種對象時傳入Out對象方式不一樣:當建立非靜態內部類In類的對象時,必須經過Outer對象來調用new 關鍵字;當建立SubClass類的對象時,必須將Outer對象做爲參數傳給SubClass的構造器。
非靜態內部類的子類不必定是內部類,也能夠是頂層類。但非靜態內部類的子類實例同樣須要保留一個引用,該引用就是指向子類的父類所在的外部類的對象,也就是說,若是有一個內部類的子類對象存在,必定存在與之對應外部類的對象。
靜態內部類是外部類相關的,所以建立內部類對象時無需建立外部類的對象。
語法是:
1 new OuterClass.InnerConstructor() 2 class StaticOut{ 3 //定義一個靜態內部類,不使用訪問控制符, 4 //即同一個包中其餘類可訪問該內部類 5 static class StaticIn{ 6 public StaticIn(){ 7 System.out.println("靜態內部類的構造器"); 8 } 9 } 10 } 11 public class CreateStaticInnerInstance{ 12 public static void main(String[] args){ 13 StaticOut.StaticIn in = new StaticOut.StaticIn(); 14 /* 15 上面代碼可改成以下兩行代碼: 16 使用OutterClass.InnerClass的形式定義內部類變量 17 StaticOut.StaticIn in; 18 經過new來調用內部類構造器建立靜態內部類實例 19 in = new StaticOut.StaticIn(); 20 */ 21 } 22 }
由於調用靜態內部類的構造器無需使用外部類對象,因此建立靜態內部類的子類比較簡單,下面定義靜態內部類StaticIn定義了一個空的子類
public class StaticSubClass extends StaticOut.StaticIn{}
若是把一個內部類放在方法裏定義,則這個內部類就是一個局部內部類,局部內部類僅在該方法裏有效。所以,局部內部類不能在外部類之外的地方使用,那麼局部內部類也無需使用訪問控制符和static修飾符修飾。
對局部成員而言,無論是局部變量仍是局部內部類,它們的上一級程序單元是方法,而不是類,使用static修飾它們沒有任何意義。所以,全部局部成員都不能使用static修飾。不只如此,由於局部成員的做用域是所在方法,其餘程序單元永遠也不可能訪問另外一個方法中的局部成員,因此局部成員都不能使用訪問控制符修飾。
1 package chapter6; 2 3 public class LocalInnerClass { 4 public static void main(String[] args){ 5 //定義局部內部類 6 class InnerBase{ 7 int a; 8 } 9 //定義局部內部類的子類 10 class SubInnerBaseClass extends InnerBase{ 11 int b; 12 } 13 //局部類對象 14 SubInnerBaseClass sb = new SubInnerBaseClass(); 15 sb.a = 3; 16 sb.b = 4; 17 System.out.println(sb.a); 18 System.out.println(sb.b); 19 } 20 }
編譯上面程序,看到生成三個class文件LocalInnerClass$1SubInnerBaseClass.class,LocalInnerClass$1InnerBase.class,LocalInnerClass.class,局部內部類的class文件綜述遵循以下命名格式:OuterClass$NInnerClass.class,注意到局部內部類的class文件的文件名比成員內部類的class文件的文件名多了一個數字,這是由於同一個類裏不可能有兩個同名的成員內部類,而同一個類裏面可能有兩個以上同名的局部內部類。因此Java爲局部內部類的class文件名增長一個數字用於區分。
匿名內部類適合一次性建立使用的類,例如命令模式的對象,建立匿名內部類時會當即建立該類的一個實例,這個類的定義當即消失,匿名內部類不能重複使用。
語法格式:
1 new 父類名稱|接口{ 2 3 4 }
從上面的定義能夠看出匿名內部類必須繼承一個父類,或實現一個接口,但最多隻能繼承一個父類或實現一個接口
關於匿名內部類還有以下兩條規則:
1.匿名內部類不能是抽象類,由於系統在建立匿名內部類的時候,會當即建立匿名內部類的對象。所以不容許將匿名內部類定義成抽象類。
2.匿名內部類不能定義構造器,由於匿名內部類沒有類名,因此沒法定義構造器,但匿名內部類能夠定義實例初始化塊,經過實例初始化塊來完成構造器須要完成的事情。
最經常使用的建立匿名內部類的方式是須要建立某個接口類型的對象,以下所示:
package chapter6; interface Product{ public double getPrice(); public String getName(); } public class TestAnonymous { public void test(Product p){ System.out.println("購買了一個名爲" + p.getName() + " 價格爲:" + p.getPrice()); } public static void main(String[] args){ TestAnonymous ta = new TestAnonymous(); //調用test方法時候,須要傳遞一個Product類對象做爲參數 ta.test(new Product(){ public double getPrice(){ return 6.3; } public String getName(){ return "南瓜"; } }); } }
上面程序中的TestAnonymous類定義了一個test方法,該方法須要一個Product對象做爲參數,但Product只是一個接口,沒法直接建立對象,所以此處考慮建立一個Product接口實現類的對象傳入該方法——若是這個Product接口實現類須要重複使用,則應該講該實現類定義成一個獨立類;若是這個Product接口實現類只需一次使用,則能夠採用上面程序中的方式,定義一個匿名內部類。
匿名內部類無須class關鍵字,而是在定義匿名內部類時直接生成該匿名內部類的對象。
因爲匿名內部類不能是抽象類,因此匿名內部類必須實現它的抽象父類或者接口裏麪包含的全部抽象方法。
上面建立Product實現類對象的代碼,能夠拆分紅以下代碼:
1 class AnonymousProduct implements Product{ 2 public double getPrice(){ 3 return 6.3; 4 } 5 public String getName(){ 6 return "南瓜"; 7 } 8 ta.test(new AnonymousProduct());
顯然使用內部類是更加簡潔一點。
當經過實現接口來建立匿名內部類時,匿名內部類也不能顯式建立構造器,所以匿名內部類只有一個隱式的無參數構造器,故new接口名後的括號裏不能傳入參數值。
但若是經過繼承父類來建立匿名內部類時,匿名內部類將擁有和父類類似的構造器,此處的類似指的是擁有相同的形參列表。
1 abstract class Device{ 2 private String name; 3 public abstract double getPrice(); 4 public Device(){} 5 public Device(String name){ 6 this.name = name; 7 } 8 public void setName(String name){ 9 this.name = name; 10 } 11 public String getName(){ 12 return this.name; 13 } 14 } 15 public class AnonymousInner{ 16 public void test(Device d){ 17 System.out.println("購買了一個" + d.getName() + ",花掉了" + d.getPrice()); 18 } 19 public static void main(String[] args){ 20 AnonymousInner ai = new AnonymousInner(); 21 //調用有參數的構造器建立Device匿名實現類的對象 22 ai.test(new Device("電子示波器"){ 23 public double getPrice(){ 24 return 67.8; 25 } 26 }); 27 //調用無參數的構造器建立Device匿名實現類的對象 28 Device d = new Device(){ 29 //初始化塊{ 30 System.out.println("匿名內部類的初始化塊..."); 31 } 32 //實現抽象方法 33 public double getPrice(){ 34 return 56.2; 35 } 36 //重寫父類的實例方法 37 public String getName(){ 38 return "鍵盤"; 39 } 40 }; 41 ai.test(d); 42 } 43 }
上面程序建立了一個抽象父類Device,這個抽象父類裏包含兩個構造器:一個無參數一個有參數。當建立以Device爲父類的匿名內部類時,既能夠傳入參數,也能夠不傳入參數。
當建立匿名內部類時,必須實現接口或抽象方法裏的全部抽象方法。若是有須要,也能夠重寫父類中的普通方法。
若是匿名內部類須要訪問外部類的局部變量,則必須使用final修飾符來修飾外部類的局部變量,不然系統將報錯。
1 interface A{ 2 void test(); 3 } 4 public class TestA{ 5 public static void main(String[] args){ 6 int age = 0; 7 A a = new A(){ 8 public void test(){ 9 //下面語句將提示錯誤:匿名內部類內訪問局部變量必須使用final修飾 10 System.out.println(age); 11 } 12 }; 13 } 14 }
注意:若是沒有A a = new A()後面的大括號進行初始化,只有A a = new A();是不對的,由於接口沒有構造器,沒法進行實例化
閉包就是一種內部類,用內部類來實現外部接口,而且能夠直接調用外部類的private成員(也就是回調)。
Java並不能顯式支持閉包,但對於非晶態內部類而言,它不只記錄了其外部類的詳細信息,還保留了一個建立非靜態內部類對象的引用,而且能夠直接調用外部類的private成員,所以能夠把非靜態內部類當成面向對象領域的閉包。
經過這種仿閉包的非靜態內部類,能夠很方便地實現回調功能,回調就是某個方法一旦得到了內部類對象引用後,就能夠在合適時候反過來調用外部類實例的方法。
下面的Teachable和Programmer基類都提供了work方法,這兩個方法的簽名同樣,可是方法功能不一樣。
1 package chapter6; 2 3 interface Teachable { 4 public void work(); 5 } 6 package chapter6; 7 8 public class Programmer { 9 protected String name; 10 //無參構造器 11 public Programmer(){ 12 13 } 14 //有參數構造器 15 public Programmer(String name){ 16 this.name = name; 17 } 18 //省略了getter和setter方法 19 public void work(){ 20 System.out.println(name + "在燈下認真敲鍵盤"); 21 } 22 }
假設如今有一我的,既是程序員,也是一個教師,也就是須要定義一個特殊的類,既須要實現Teachable接口,也須要繼承Programmer父類,表面上看起來沒有任何問題,問題是Teachable接口和Programmer父類裏包含了相同的work方法,若是按照下面代碼來定義一個特殊的TeachableProgrammer類,是有問題的
1 package chapter6; 2 3 public class TeachableProgrammer extends Programmer implements Teachable{ 4 public void work(){ 5 System.out.println(super.name + "教師在課堂上講解"); 6 } 7 }
顯然上面的TeachableProgrammer類只有一個work方法,這個work方法只能進行教學,再也不能夠進行編程,但實際須要二者技能都要具有。
能夠用一個內部類來實現這個功能
1 package chapter6; 2 3 public class TeachableProgrammer extends Programmer{ 4 public TeachableProgrammer(){ 5 6 } 7 public TeachableProgrammer(String name){ 8 super.name = name; 9 } 10 public void teach(){ 11 System.out.println(getName() + "教師在課堂上講解"); 12 } 13 private class Closure implements Teachable{ 14 //非靜態內部類回調外部類的work方法 15 public void work(){ 16 teach(); 17 } 18 } 19 //返回一個非靜態內部類引用,使得外部類容許非靜態內部類引用回調外部類的方法 20 public Teachable getCallBackReference(){ 21 return new Closure(); 22 } 23 }
上面的TeachableProgrammer至少Programmer類的子類,它能夠直接調用Programmer基類的work方法,該類也包含教學teach方法,單子合格方法與Teachable接口沒有任何關係,TeachableProgrammer也不能當場Teachable使用,此時建立了一個內部類,實現了Teachable接口,並實現了教學的work方法。但這種實現是經過回調TeachableProgrammer類的teach方法實現的。若是須要讓TeachableProgrammer對象進行教學,只須要調用Closure內部類(它是Teachable接口的實現類)對象的work方法便可。
TeachableProgrammer類提供了一個獲取內部類對象的方法:該方法無需返回Closure類型,只須要返回所實現接口Teachable類型便可,由於它只須要當初一個Teachable對象使用便可。
1 public class TestTeachableProgrammer 2 { 3 public static void main(String[] args) 4 { 5 TeachableProgrammer tp = new TeachableProgrammer("李剛"); 6 //直接調用TeachableProgrammer類從Programmer類繼承到的work方法 7 tp.work(); 8 //表面上調用的是Closure的work方法,其實是回調TeachableProgrammer的teach方法 9 tp.getCallbackReference().work(); 10 } 11 }
在某些狀況下,一個類的對象是有限並且固定的,例如季節類,它只有四個對象。這種實例有限並且固定的類,在Java裏被稱爲枚舉類。
手動實現枚舉類
若是須要手動實現枚舉類,能夠採用以下設計方式:
1.經過private將構造器隱藏起來
2.把這個類的全部可能實例都使用public static final屬性來保存
3.若是有必要,能夠提供一些靜態方法,容許其餘程序根據特定參數來獲取與之匹配的實例。
1 public class Season{ 2 //把Season類定義成不可變的,將其屬性也定義成final 3 private final String name; 4 private final String desc; 5 public static final Season SPRING = new Season("春天" , "趁春踏青"); 6 public static final Season SUMMER = new Season("夏天" , "夏日炎炎"); 7 public static final Season FALL = new Season("秋天" , "秋高氣爽"); 8 public static final Season WINTER = new Season("冬天" , "圍爐賞雪"); 9 10 public static Season getSeaon(int seasonNum){ 11 switch(seasonNum){ 12 case 1 : 13 return SPRING; 14 case 2 : 15 return SUMMER; 16 case 3 : 17 return FALL; 18 case 4 : 19 return WINTER; 20 default : 21 return null; 22 } 23 } 24 25 //將構造器定義成private訪問權限 26 private Season(String name , String desc){ 27 this.name = name; 28 this.desc = desc; 29 } 30 //只爲name和desc屬性提供getter方法 31 public String getName(){ 32 return this.name; 33 } 34 public String getDesc(){ 35 return this.desc; 36 } 37 }
上面的方式有些麻煩,J2SE1.5新增了一個enum關鍵字,用以定義枚舉類。枚舉類是特俗的類。它能夠有本身的方法和屬性,能夠實現一個或者多個接口,也能夠定義本身的構造器。一個Java源文件最多隻能定義一個public訪問權限的枚舉類,且該Java源文件必須和該枚舉類的類名相同。
但枚舉類終究不是普通類,有本身的以下特色:
1.枚舉類能夠實現一個或多個接口,使用enum定義的枚舉類默認繼承了java.lang.Enum類,而不是繼承Object類,其中java.lang.Enum類實現了java.lang.Serializable和java.lang.Comparable兩個接口
2.枚舉類的構造器只能使用private訪問控制符,若是省略了其構造器的訪問控制符,則默認使用private修飾,若是強制指定,則只能使用private修飾符
3.枚舉類的全部實例都必須在枚舉類中顯式列出,不然這個枚舉類將永遠都不能產生實例。列出這些實例時,系統會自動添加public static final修飾,無須程序員顯式添加。
4.全部枚舉類都提供了一個values方法,該方法能夠很方便的遍歷全部的枚舉值。
1 package chapter6; 2 3 public enum SeasonEnum { 4 SPRING,SUMMER,FALL,WINTER; 5 public static void main(String[] args){ 6 System.out.println(SeasonEnum.SPRING); 7 8 } 9 }
編譯上面的程序能夠生成一個class文件,enum關鍵字和class、interface關鍵字的做用大體相似。
定義枚舉類時,須要顯式列出全部枚舉值,如上面的 SPRING等,枚舉值之間用逗號,隔開,枚舉值列舉結束後以英文分號做爲結束。這些枚舉值是枚舉類的實例。
1 package chapter6; 2 3 public class TestEnum { 4 public void judge(SeasonEnum s){ 5 //swich分支語句 6 switch(s){ 7 case SPRING: 8 System.out.println("面朝大海,春暖花開~"); 9 break; 10 case SUMMER: 11 System.out.println("像夏花同樣絢爛"); 12 break; 13 case FALL: 14 System.out.println("自古逢秋悲寂寥,我言秋日勝春朝"); 15 break; 16 case WINTER: 17 System.out.println("你就像那冬天裏的一把火"); 18 break; 19 } 20 } 21 public static void main(String[] args){ 22 //列出枚舉類的全部實例 23 for(SeasonEnum s:SeasonEnum.values()){ 24 System.out.println(s); 25 } 26 //直接訪問單個實例 27 new TestEnum().judge(SeasonEnum.SPRING); 28 } 29 } 30 輸出: 31 SPRING 32 SUMMER 33 FALL 34 WINTER 35 面朝大海,春暖花開~
枚舉類也是一種類,只是它是一種比較特殊的類,所以它同樣可使用屬性和方法。
1 public enum Gender{ 2 MALE,FEMALE; 3 public String name; 4 5 } 6 上面的Gender枚舉類裏定義了一個name屬性,而且將它定義成一個public訪問權限的屬性,下面使用該枚舉類 7 public class TestGender{ 8 public static void main(String[] args){ 9 Gender g = Enum.valueOf(Gender.class , "FEMALE"); 10 g.name = "女"; 11 System.out.println(g + "表明:" + g.name); 12 } 13 }
注意:Enum類的實例生成不是經過new的,而是經過方法valueOf來解決。
正如前面提到的,Java應該把全部類設計成良好封裝的類,因此不該該容許直接訪問Gender類的name屬性,而應該經過方法來控制訪問。
枚舉類能夠實現一個或多個接口。與普通類實現一個或多個接口徹底同樣,枚舉類實現一個或多個接口,也須要實現該接口所包含的方法。
1 public interface GenderDesc{ 2 void info(); 3 }
下面的類實現了這個接口
1 public String getName(){ 2 return this.name; 3 } 4 /* 5 public void info(){ 6 System.out.println("這是一個用於用於定義性別屬性的枚舉類"); 7 }
若是由枚舉類來實現接口裏的方法,則每一個枚舉值在調用該方法時,都有相同的行爲方式(由於方法體徹底同樣)。若是須要每一個枚舉值在調用該方法時呈現出不一樣的行爲方式,則可讓每一個枚舉值分別來實現該方法,每一個枚舉值提供不一樣的實現方式,從而讓不一樣枚舉值調用該方法時具備不一樣的行爲方式,下面的Gender枚舉類中,不一樣枚舉值對info方法的實現則各不相同。
1 public enum Gender implements GenderDesc{ 2 //此處的枚舉值必須調用對應構造器來建立 3 MALE("男"){ 4 public void info(){ 5 System.out.println("這個枚舉值表明男性"); 6 } 7 }, 8 FEMALE("女"){ 9 public void info(){ 10 System.out.println("這個枚舉值表明女性"); 11 } 12 }; 13 private String name; 14 //枚舉類的構造器只能使用private修飾 15 private Gender(String name){ 16 this.name = name; 17 } 18 }
上面的MALE和FEMALE後面跟了一個花括號,花括號部分其實是一個類體部分,這種狀況下,當建立枚舉值時,並非直接建立了Gender枚舉類的實例,而是至關於建立Gender的匿名子類的實例。也就是一個匿名內部類的類體部分,因此這個部分的代碼語法與前面介紹的匿名內部類語法大體類似,依然是枚舉類的匿名內部子類。
編譯上面的程序,能夠看到生成了Gender.class、Gender$1.class和Gender$2.class三個文件。這也就是說MALE和FEMALE其實是Gender匿名子類的實例,而不是Gender類的實例。
枚舉類的枚舉值就是實例值
假設有一個Operation枚舉類,它的四個枚舉值PLUS,MINUS,TIMES,DIVIDE分別表明加減乘除,爲此定義枚舉類以下
1 package chapter6; 2 3 public enum Operation { 4 PLUS,MINUS,TIMES,DIVIDE; 5 double eval (double x,double y){ 6 switch(this){ 7 case PLUS: 8 return x + y; 9 case MINUS: 10 return x - y; 11 case TIMES: 12 return x*y; 13 case DIVIDE: 14 return x/y; 15 default: 16 return 0; 17 } 18 } 19 public static void main(String[] args){ 20 System.out.println(Operation.PLUS.eval(2, 3)); 21 System.out.println(Operation.MINUS.eval(2, 3)); 22 System.out.println(Operation.TIMES.eval(2, 3)); 23 System.out.println(Operation.DIVIDE.eval(2, 3)); 24 } 25 }
上面的枚舉類能夠實現四個方法,this表明四個枚舉類的實例值。這四個值是肯定的,不能有其餘值。實際上Operation類的四個值對eval方法各有不一樣的實現。爲此能夠採用前面MALE/FEMALE的方法,讓它們分別爲四個枚舉值提供eval實現,而後在Operation類中定義一個eval的抽象方法。
package chapter6; public enum Operation2 { PLUS{ public double eval(double x,double y){ return x + y; } }, MINUS{ public double eval(double x,double y){ return x - y; } }, TIMES{ public double eval(double x,double y){ return x*y; } }, DIVIDE{ public double eval(double x,double y){ return x/y; } }; //提供抽象方法,可是是放在下面的 public abstract double eval(double x,double y); public static void main(String[] args){ System.out.println(Operation2.PLUS.eval(2, 3)); System.out.println(Operation2.MINUS.eval(2, 3)); System.out.println(Operation2.TIMES.eval(2, 3)); System.out.println(Operation2.DIVIDE.eval(2, 3)); } } 輸出: 5.0 -1.0 6.0 0.6666666666666666
編譯上面的程序會生成5個class文件,其實Operation2對應一個class文件,它的四個匿名內部子類分別各對應一個class文件。
枚舉類裏定義抽象方法時無需顯式使用abstract關鍵字將枚舉類定義成抽象類,但由於枚舉類須要顯式建立枚舉值,而不是做爲父類,因此定義每一個枚舉值時必須爲抽象方法提供實現,不然將出現編譯錯誤。
5 對象與垃圾回收
垃圾回收是Java語言的重要功能,當程序建立對象、數組等引用類型實體時,系統都會在堆內存爲之分配一塊內存區,對象就保存在這塊內存區中,當這塊內存再也不被引用變量引用時,這塊內存就變成垃圾,等待垃圾回收機制進行回收。垃圾回收機制具備以下特徵:
1.垃圾回收機制只負責回收堆內存中對象,不會回收任何物理資源(例如數據庫鏈接、網絡IO等資源)
2.程序沒法精確控制垃圾回收的運行,垃圾回收會在合適時候運行。當對象永久性地失去引用後,系統就會在合適時候回收它所佔的內存。
3.垃圾回收機制回收任何對象以前,總會先調用它的finalize方法,該方法可能使該對象從新復活(讓一個引用該變量從新引用該對象),從而致使垃圾回收機制取消回收。
當一個對象在堆內存中運行時,根據它被引用變量所引用的狀態,能夠把它所處的狀態分紅以下三種:
1.激活狀態:當一個對象被建立後,有一個以上的引用變量引用它,則這個對象在程序中處於激活狀態,程序可經過引用變量來調用該對象的屬性和方法。
2.去活狀態:若是程序中某個對象再也不有任何引用變量引用它,它就進入了去活狀態。在這個狀態下,系統的垃圾回收機制準備回收該對象所佔用的內存,在回收該對象以前,系統會調用全部去活狀態對象的finalize方法進行資源清理,若是系統在調用finalize方法重寫讓一個引用變量引用該對象,則這個對象會再次變爲激活狀態;不然該對象將進入死亡狀態
3.死亡狀態:當對象與全部變量的關聯都被切斷,且系統已經調用全部對象的finalize方法,依然沒有使該對象變成激活狀態,那這個對象將永久性地失去引用,最後變成死亡狀態。只有當一個對象處於死亡狀態時,系統纔會真正回收該對象所佔有的資源。
1 下面的程序說明了上面的原理: 2 package chapter6; 3 4 public class StatusTransfer { 5 public static void test(){ 6 String a = new String("Englis");① 7 a = new String("Deutsch");② 8 } 9 public static void main(String[] args){ 10 test();③ 11 } 12 13 }
當程序執行test方法①代碼時,代碼定義了一個a變量,並讓該變量指向字符串English,代碼執行結束後,字符串對象English處於激活狀態。當程序執行了test方法的②代碼後,代碼再次定義了Deutsch對象,並讓a變量指向這個對象,此時English處於去活狀態,而Deutsch處於激活狀態。
一個對象能夠被一個方法局部變量所引用,也能夠被其餘類的類屬性引用,或被其餘對象的實例屬性引用。當被類屬性引用時,只有類被銷燬,對象纔會進入去活狀態,當被其餘對象的實例屬性引用時,只有該對象被銷燬,該對象纔會進入去活狀態。
程序沒法精確控制Java垃圾回收的時機,但咱們依然能夠強制系統進行垃圾回收——只是這種機制是通知系統進行垃圾回收,但系統是否進行垃圾回收依然不肯定。大部分時候,強制垃圾回收會有效果,強制垃圾回收有以下兩個方法。
1.調用System類的gc()靜態方法:System.gc();
2.調用Runtime對象的gc()實例方法:Runtime.getRuntime().gc()
1 public class TestGc{ 2 private double height; 3 public static void main(String[] args){ 4 for (int i = 0 ; i < 4; i++){ 5 new TestGc(); 6 //System.gc(); 7 Runtime.getRuntime().gc(); 8 } 9 } 10 public void finalize(){ 11 System.out.println("系統正在清理TestGc對象的資源..."); 12 } 13 }
在垃圾回收機制回收某個對象所佔用的內存以前,一般要求程序調用適當的方法來清理資源,在沒有明確指定資源清理的狀況下,Java提供了默認機制來清理該對象的資源,這個方法是finalize,它是Object類的實例方法,方法原先爲:
protected void finalize() throws Throwable
當finalize()方法返回以後,對象消失,垃圾回收機制開始執行。方法原型中的throws Throwable表示能夠拋出任何類型的異常。
任何Java類均可以覆蓋Object類的finalize方法,在該方法中清理該對象佔用的資源。若是程序終止前始終沒有進行垃圾回收,則不會調用失去引用對象的finalize方法來清理資源。垃圾回收機制什麼時候調用對象的finalize方法是徹底透明的,只有當程序認爲須要更多額外內存時,垃圾回收機制纔會進行垃圾回收。
finalize方法有以下四個特色:
1.永遠不要主動調用某個對象的finalize方法,該方法應交給垃圾回收機制調用。
2.finalize方法什麼時候被調用,是否被調用具備不肯定性。不要把finalize方法當成必定會被執行的方法
3.當JVM執行去活對象的finalize方法時,可能使該對象或系統中其餘對象從新變成激活狀態
4.當JVM執行finalize方法時出現了異常,垃圾回收機制不會報告異常,程序繼續執行。
1 public class TestFinalize{ 2 private static TestFinalize tf = null; 3 public void info(){ 4 System.out.println("測試資源清理的finalize方法"); 5 } 6 public static void main(String[] args) throws Exception{ 7 //建立TestFinalize對象當即進入去活狀態 8 new TestFinalize(); 9 //通知系統進行資源回收 10 System.gc(); 11 System.runFinalization(); 12 //Thread.sleep(2000); 13 tf.info(); 14 } 15 public void finalize(){ 16 //讓tf引用到試圖回收的去活對象,即去活對象從新變成激活 17 tf = this; 18 } 19 }
上面程序中定義了一個TestFinalize類,重寫了finalize方法,該方法使一個tf引用變量引用的對象從去活對象從新變成激活狀態。
除此以外,System和Runtime類裏都提供了一個runFinalization方法,能夠強制垃圾回收機制調用系統去活對象的finalize方法。
對大部分對象而言,程序裏會有一個引用變量引用該對象,這種引用方式是最多見的引用方式。除此以外,java.lang.ref包下提供了三個類:SoftReference、PhantomReference和WeakReference,它們分別表明了系統對對象的三種引用方式:軟引用、虛引用和弱引用。所以Java語言對對象的引用有以下四種。
強引用
這是Java程序中最多見的引用方式,程序建立一個對象,並把這個對象賦給一個引用變量。程序經過該引用變量來操做實際的對象,前面介紹的對象和數組都是採用了這種強引用的方式。當一個對象被一個或一個以上的引用變量所引用時,它處於激活狀態,不可能被系統垃圾回收機制回收。
軟引用
軟引用須要經過SoftReference類來實現,當一個對象只具備軟引用時,它有可能被垃圾回收機制回收。對於只有軟引用的對象而言,當系統內存空間足夠時,它不會被系統回收,程序也可以使用該對象;當系統內存空間不足時,系統將會回收它。軟引用一般用於對內存敏感的程序中。
弱引用
弱引用經過WeakReference類實現,弱引用和軟引用很像,但弱引用級別更低。對於只有弱引用的對象而言,當系統垃圾回收機制運行時,無論系統內存是否足夠,總會回收該對象所佔用的內存。
虛引用
虛引用經過PhantomReference類實現,虛引用徹底相似於沒有引用。虛引用對對象自己沒有太大影響,對象甚至感受不到虛引用的存在。若是一個對象只有一個虛引用時,那它和沒有引用的效果大體相同。虛引用主要用於跟蹤對象唄垃圾回收的狀態,虛引用不能單獨使用,必須和引用隊列ReferenceQueue聯合使用。
軟、弱、虛引用三種都有一個get方法
1 public class TestReference{ 2 public static void main(String[] args) throws Exception{ 3 //建立一個字符串對象 4 String str = new String("Struts2權威指南"); 5 //建立一個弱引用,讓此弱引用引用到"Struts2權威指南"字符串 6 WeakReference wr = new WeakReference(str); 7 //切斷str引用和"Struts2權威指南"字符串之間的引用 8 str = null; 9 //取出弱引用所引用的對象 10 System.out.println(wr.get()); 11 //強制垃圾回收 12 System.gc(); 13 System.runFinalization(); 14 //再次取出弱引用所引用的對象 15 System.out.println(wr.get()); 16 } 17 }
爲了用 JAR 文件執行基本的任務,要使用做爲Java Development Kit 的一部分提供的 Java Archive Tool ( jar 工具)。用 jar 命令調用 jar 工具。表 1 顯示了一些常見的應用:
常見的 jar 工具用法
功能
命令
用一個單獨的文件建立一個 JAR 文件 jar cf jar-file input-file...
用一個目錄建立一個 JAR 文件 jar cf jar-file dir-name
建立一個未壓縮的 JAR 文件 jar cf0 jar-file dir-name
更新一個 JAR 文件 jar uf jar-file input-file...
查看一個 JAR 文件的內容 jar tf jar-file
提取一個 JAR 文件的內容 jar xf jar-file
從一個 JAR 文件中提取特定的文件 jar xf jar-file archived-file...
運行一個打包爲可執行 JAR 文件的應用程序 java -jar app.jar
jar包就是zip包,能夠用一些windows自帶的工具如winrar等解壓縮文件進行處理。使用WinRAR工具建立JAR包時候,由於工具自己不會自動添加清單文件,因此須要手動添加清單文件,即須要手動創建META-INF路徑,並在該路徑下創建MANIFEST.MF文件,該文件至少須要以下兩行:
Manifest-Version:1.0
Created-By:1.6.0_03(Sun Microsystem Inc.)
除此以外,Java還能生成兩種壓縮包:WAR包和EAR,WAR文件是Web Archive File,對應一個Web應用文檔,EAR是Enterprise Archive File對應企業應用文檔,有Web和EJB兩個部分組成。WAR、EAR和JAR包徹底同樣,至少改變了文件後綴而已。
當一個應用程序開發成功後,大體有三種發佈方式:
1.使用平臺相關的編譯器將整個應用編譯成平臺相關的可執行性文件。這種方式經常須要第三方編譯器支持,並且編譯生成的可執行性文件喪失了跨平臺特性,甚至可能有必定的性能降低。
2.爲整個應用編輯一個批處理文件。使用以下命令:
java package.MainClass
當客戶點擊上面的批處理文件時候,系統執行批處理文件的java命令,從而容許程序的主類。
3.將一個應用程序製做成可執行的JAR包,經過JAR包來發布應用程序。
建立可執行JAR包的關鍵在於:讓javaw命令知道JAR包中哪一個類是主類,javaw命令能夠經過運行該主類來運行程序,這就須要藉助於清單文件,須要在清單文件中增長以下一行:
Main-Class:test.Test
也就是test包下的Test類做爲主類。這樣javaw就知道從JAR包中的test.Test開始運行。
在清單文件中增長這一行的方法以下:
1.建立一個文本文件,裏面包含以下內容
Main-Class:<空格>test.Test<回車>
注意:上面的屬性文件要求很嚴格,冒號前面是key(即Main-Class),冒號後面的空格的後面是value,也就是test.Test。文件格式要求以下:
1.每行只能寫一個key-value對,key-value必須頂格寫。
2.key-value之間的冒號後緊跟一個空格
3.文件開頭沒有空行
4.文件必須以一行空行結束,也就是末尾以回車結束。
上面的文件能夠保存在任意位置,以任意文件名存放。
使用以下命令進行添加:
1 jar cvfm test.jar a.txt test
運行上面的命令後,在當前路徑下生產一個test.jar文件,查看文件能夠看到Main-Class爲:test.Test。表名該JAR包的主類
1.使用java命令,使用java運行時的語法是:java -jar test.jar2.使用javaw命令,使用javaw運行的語法是:javaw test.jar。