1.Java中除了static方法和final方法之外,其它所有的方法都是動態綁定,如同C++的虛函數,但是我們不需要顯示的聲明。
private方法本質上屬於final方法(因此不能被子類訪問)。
構造函數本質上屬於static方法,只不過該static聲明是隱式的。
final方法會使編譯器生成更有效的代碼,這也是為什么說聲明為final方法能在一定程度上提高性能(效果不明顯)。
如果某個方法是靜態的,它的行為就不具有多態性。
在父類構造函數內部調用具有多態行為的函數將導致無法預測的結果,因為此時子類對象還沒初始化,此時調用子類方法不會得到我們想要的結果。
只有非private方法才可以被覆蓋,但覆蓋private方法對子類來說是一個新的方法而非重載方法。因此,在子類中,新方法名最好不要與基類的private方法采取同一名字。
Java類中屬性域的訪問操作都由編譯器解析,因此不是多態的。父類和子類的同名屬性都會分配不同的存儲空間。為了得到父類的屬性field,必須顯式地指明super.field。
2.is-a關系和is-like-a關系
is-a關系屬於純繼承,即只有在基類中已經建立的方法才可以在子類中被覆蓋。基類和子類有着完全相同的接口,這樣向上轉型時永遠不需要知道正在處理的對象的確切類型,這通過多態來實現。
is-like-a關系:子類擴展了基類接口。它有着相同的基本接口,但是他還具有由額外方法實現的其他特性。缺點就是子類中接口的擴展部分不能被基類訪問,因此一旦向上轉型,就不能調用那些新方法。
has-a關系:某個類包含另一個類或接口的引用。
3.Java中構造函數的調用順序
a.在其他任何事物發生之前,將分配給對象的存儲空間初始化成二進制0。
b.調用基類構造函數。
c.按聲明順序調用成員的初始化方法。
d.最后調用子類的構造函數。
由於存在表態數據,實際過程為
b.繼承體系的所有靜態成員初始化(先父類,后子類)
c.父類初始化完成(普通成員的初始化-->構造函數的調用)
d.子類初始化(普通成員-->構造函數)
4.運行時類型信息(RTTI + 反射)
運行時類型信息使得你可以在程序運行時發現和使用類型信息。
Java是如何讓我們在運行時識別對象和類的信息的,主要有3種方式
“傳統的”RTTI,它假定我們在編譯時已經知道了所有的類型,比如Shape s = (Shape)s1;
“反射”機制,它運行我們在運行時發現和使用類的信息,即使用Class.forName()
。
關鍵字instanceof
,它返回一個bool值,它保持了類型的概念,它指的是“你是這個類嗎?或者你是這個類的派生類嗎?”。而如果用==或equals比較實際的Class對象,就沒有考慮繼承—它或者是這個確切的類型,或者不是。
RTTI:Run-Time Type Identification,即運行時類型識別,是指在運行時識別一個對象的類型,其對應的類是Class對象,每個java里面的類都對應一個Class對象(在編寫並且編譯后),這個對象被保存在這個類的同名class文件里。
要理解RTTI在Java中的工作原理,首先必須知道類型信息在運行時是如何表示的,這項工作是由稱為Class對象
的特殊對象完成的,它包含了與類有關的信息。Java送Class對象來執行其RTTI,使用類加載器的子系統實現。
無論何時,只要你想在運行時使用類型信息,就必須首先獲得對恰當的Class對象的引用,獲取方式有三種:
a.如果你沒有持有該類型的對象,則Class.forName()
就是實現此功能的便捷途,因為它不需要對象信息。
b.如果你已經擁有了一個感興趣的類型的對象,那就可以通過調用getClass()
方法來獲取Class引用了,它將返回表示該對象的實際類型的Class引用。
c.使用類字面常量。比如這樣:String.class;
來引用。
這樣做不僅更簡單,而且更安全,因為它在編譯時就會受到檢查(因此不需要置於try語句塊中),並且它根除了對forName方法的引用,所以也更高效。類字面常量不僅可以應用於普通的類,也可以應用於接口、數組以及基本數據類型。
注意:
當使用“.class”來創建對Class對象的引用時,不會自動地初始化該Class對象,初始化被延遲到了對靜態方法(構造器隱式的是靜態的)或者非final靜態域(注意final靜態域不會觸發初始化操作)進行首次引用時才執行。
而使用Class.forName時會自動的初始化。
為了使用類而做的准備工作實際包含三個步驟:
- 加載:由類加載器執行。查找字節碼,並從這些字節碼中創建一個Class對象
- 鏈接:驗證類中的字節碼,為靜態域分配存儲空間,並且如果必需的話,將解析這個類創建的對其他類的所有引用。
- 初始化:如果該類具有超類,則對其初始化,執行靜態初始化器和靜態初始化塊。
如果不知道某個對象的確切類型,RTTI可以告訴你,但是有一個限制:這個類型在編譯時必須已知,這樣才能使用RTTI識別它,也就是在編譯時,編譯器必須知道所有要通過RTTI來處理的類。如果要突破這個限制就需要使用反射機制。
反射的原理:
Class
類與java.lang.reflect
類庫一起對反射的概念進行了支持,該類庫包含了Field
、Method
以及Constructor
類(每個類都實現了Member
接口)。這些類型的對象是由JVM在運行時創建的,用以表示未知類里對應的成員。這樣你就可以使用Constructor
創建新的對象,用get()/set()
方法讀取和修改與Field
對象關聯的字段,用invoke()
方法調用與Method
對象關聯的方法。另外,還可以調用getFields()、getMethods()和getConstructors()
等很便利的方法,以返回表示字段、方法以及構造器的對象的數組。這樣,匿名對象的類信息就能在運行時被完全確定下來,而在編譯時不需要知道任何事情。
反射與RTTI的區別
當通過反射與一個未知類型的對象打交道時,JVM只是簡單地檢查這個對象,看它屬於哪個特定的類(就像RTTI那樣),在用它做其他事情之前必須先加載那個類的Class
對象,因此,那個類的.class
文件對於JVM來說必須是可獲取的:要么在本地機器上,要么可以通過網絡取得。所以RTTI與反射之間真正的區別只在於:對RTTI來說,編譯器在編譯時打開和檢查.class文件(也就是可以用普通方法調用對象的所有方法);而對於反射機制來說,.class文件在編譯時是不可獲取的,所以是在運行時打開和檢查.class文件。
5.代理模式與Java中的動態代理
代理模式
在任何時刻,只要你想要將額外的操作從“實際”對象中分離到不同的地方,特別是當你希望能夠很容易地做出修改,從沒有使用額外操作轉為使用這些操作,或者反過來時,代理就顯得很有用(設計模式的關鍵是封裝修改)。例如,如果你希望跟蹤對某個類中方法的調用,或者希望度量這些調用的開銷,那么你應該怎樣做呢?這些代碼肯定是你不希望將其合並到應用中的代碼,因此代理使得你可以很容易地添加或移除它們。
動態代理
Java的動態代理比代理的思想更向前邁進了一步,因為它可以動態地創建代理並動態地處理對所代理方法的調用。
測試代碼
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; interface Interface { void doSomething(); void somethingElse(String arg); } class RealObject implements Interface { @Override public void doSomething() { System.out.println("[RealObject]doSomething."); } @Override public void somethingElse(String arg) { System.out.println("[RealObject]somethingElse:" + arg); } } class DynamicProxyHandler implements InvocationHandler { private Object mProxy; public DynamicProxyHandler(Object proxy) { mProxy = proxy; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("proxy class: " + proxy.getClass()); System.out.println("method: " + method + ". args: " + args); System.out.println(); if (args != null) { for (Object arg : args) System.out.println(" " + arg); } return method.invoke(this.mProxy, args); } } class SimpleDynamicProxy { public static void consumer(Interface iface) { iface.doSomething(); iface.somethingElse("哈哈"); } public static void main(String[] args) { RealObject real = new RealObject(); consumer(real); System.out.println("-- insert a proxy and call again --"); Interface proxy = (Interface) Proxy.newProxyInstance( Interface.class.getClassLoader(), new Class[] { Interface.class }, new DynamicProxyHandler(real)); consumer(proxy); } }
結果
[RealObject]doSomething. [RealObject]somethingElse:哈哈 -- insert a proxy and call again -- proxy class: class $Proxy0 method: public abstract void Interface.doSomething(). args: null [RealObject]doSomething. proxy class: class $Proxy0 method: public abstract void Interface.somethingElse(java.lang.String). args: [Ljava.lang.Object;@6adcc4e2 哈哈 [RealObject]somethingElse:哈哈
6.訪問控制權限
Java訪問權限修飾詞:public、protected、包訪問權限(默認訪問權限,有時也稱friendly)和private。
包訪問權限:當前包中的所有其他類對那個成員具有訪問權限,但對於這個包之外的所有類,這個成員卻是private。
protected:繼承訪問權限。protected使基類某些成員訪問權限賦予派生類。protected也提供包訪問權限,即相同包內的其他類都可以訪問protected元素。protected指明對類用戶而言,這是private的,但對於任何繼承於此類的導出類或其他任何位於同一個包內的類來說,它卻是可以訪問的”。
7.組合和繼承之間的選擇
組合和繼承都允許在新的類中放置子對象,組合是顯式的這樣做,而繼承則是隱式的做。
組合技術通常用於想在新類中使用現有類的功能而非它的接口這種情形。即在新類中嵌入某個對象,讓其實現所需要的功能,但新類的用戶看到的只是為新類所定義的接口,而非所嵌入對象的接口。為取得此效果,需要在新類中嵌入一個現有類的private對象。但有時,允許類的用戶直接訪問新類中的組合成分是極具意義的,即將成員對象聲明為public。如果成員對象自身都隱藏了具體實現,那么這種做法是安全的。當用戶能夠了解到你正在組裝一組部件時,會使得端口更加易於理解。比如Car對象可由public的Engine對象、Wheel對象、Window對象和Door對象組合。但務必要記得這僅僅是一個特例,一般情況下應該使域成為private。
在繼承的時候,使用某個現有類,並開發一個它的特殊版本。通常,這意味着你在使用一個通用類,並為了某種特殊需要而將其特殊化。稍微思考一下就會發現,用一個“交通工具”對象來構成一部“車子”是毫無意義的,因為“車子”並不包含“交通工具”,它僅是一種交通工具(is-a關系)。
“is-a”(是一個)的關系是用繼承來表達的,而“has-a”(有一個)的關系則是用組合來表達的。
到底是該用組合還是繼承,一個最清晰的判斷方法就是問一問自己是否需要從新類向基類進行向上轉型,需要的話就用繼承,不需要的話就用組合方式。
8.final關鍵字
當final修飾的是基本數據類型時,它指的是數值恆定不變(就是編譯期常量,如果是static final修飾,則強調只有一份),而對對象引用而不是基本類型運用final時,其含義會有一點令人迷惑,因為用於對象引用時,final使引用恆定不變,一旦引用被初始化指向一個對象,就無法再把它指向另一個對象。然而,對象其自身卻是可以被修改的,Java並未提供使任何對象恆定不變的途徑(但可以自己編寫類以取得使對象恆定不變的效果),這一限制同樣適用數組,它也是對象。
使用final方法真的可以提高程序效率嗎?
將一個方法設成final后,編譯器就可以把對那個方法的所有調用都置入“嵌入”調用里。只要編譯器發現一個final方法調用,就會(根據它自己的判斷)忽略為執行方法調用機制而采取的常規代碼插入方法(將自變量壓入堆棧;跳至方法代碼並執行它;跳回來;清除堆棧自變量;最后對返回值進行處理)。相反,它會用方法主體內實際代碼的一個副本來替換方法調用。這樣做可避免方法調用時的系統開銷。當然,若方法體積太大,那么程序也會變得雍腫,可能受不到嵌入代碼所帶來的任何性能提升。因為任何提升都被花在方法內部的時間抵消了。
虛擬機(特別是hotspot技術)能自動偵測這些情況,並頗為“明智”地決定是否嵌入一個final 方法。然而,最好還是不要完全相信編譯器能正確地作出所有判斷。通常,只有在方法的代碼量非常少,或者想明確禁止方法被覆蓋的時候,才應考慮將一個方法設為final。
類內所有private 方法都自動成為final。由於我們不能訪問一個private 方法,所以它絕對不會被其他方法覆蓋(若強行這樣做,編譯器會給出錯誤提示)。可為一個private方法添加final指示符,但卻不能為那個方法提供任何額外的含義。
記住:只有在方法的代碼量非常少,或者想明確禁止方法被覆蓋的時候,才應考慮將一個方法設為final。
9.內部類
為什么需要內部類? 解決了多繼承的問題,繼承具體或抽象類。
一般來說,內部類繼承自某個類或實現某個接口,內部類的代碼操作創建它的外圍類的對象。所以可以認為內部類提供了某種進入其外圍類的窗口。
內部類最吸引人的原因是:每個內部類都能獨立地繼承自一個(接口的)實現,所以無論外圍類是否已經繼承了某個(接口的)實現,對於內部類都沒有影響。
如果沒有內部類提供的、可以繼承多個具體的或抽象的類的能力,一些設計與編程問題就很難解決。從這個角度看,內部類使得多重繼承的解決方案變得完整。接口解決了部分問題,而內部類有效的實現了“多重繼承”。也就是說,內部類允許繼承多個非接口類型。
考慮這樣一種情形:如果必須在一個類中以某種方式實現兩個接口。由於接口的靈活性,你有兩種選擇:使用單一類或者使用內部類。但如果擁有的是抽象的類或具體的類,而不是接口,那就只能使用內部類才能實現多重繼承。
使用內部類,還可以獲得其他一些特性:
- 內部類可以有多個實例,每個實例都有自己的狀態信息,並且與其外圍類對象的信息相互獨立。
- 在單個外圍類中,可以讓多個內部類以不同的方式實現同一個接口或繼承同一個類。
- 創建內部類對象的時刻並不依賴於外圍類對象的創建。
- 內部類並沒有令人迷惑的is-a關系,它就是一個獨立的實體。
10.String類型
String類型是一個不可變的類型,任何對String對象改變的操作都會生成一個新的實例。
用於String的“+”與“+=”是Java中僅有的兩個重載過的操作符,而Java並不允許程序員重載任何操作符。
考慮到效率因素,編譯器會對String的多次+操作進行優化,優化使用StringBuilder
操作。但是在存在一個循環執行+時,手動使用StringBuilder代替String。考慮下面的代碼,在每一次For循環之內,都將創建一個StringBuilder對象。
public String implicit(String[] fields) { String result = ""; for(int i = 0; i < fields.length; i++) result += fields[i]; return result; }
此種情況下,請使用下面的方法替代
public String explicit(String[] fields) { StringBuilder result = new StringBuilder(); for(int i = 0; i < fields.length; i++) result.append(fields[i]); return result.toString(); }
當你為一個類重寫toString()方法時,如果字符串操作比較簡單,那就可以信賴編譯器,它會為你合理地構造最終的字符串結果。但是,如果你要在toString()方法中使用循環,那么最好自己創建一個StringBuilder對象,用它來構造最終的結果
11.序列化控制
當我們對序列化進行控制時,可能某個特定子對象不想讓Java序列化機制自動保存與恢復。如果子對象表示的是我們不希望將其序列化的敏感信息(如密碼),通常會面臨這種情況。即使對象中的這些信息是private屬性,一經序列化處理,人們就可以通過讀取文件或者攔截網絡傳輸的方式來訪問到它。有兩種辦法可以防止對象的敏感部分被序列化:
實現Externalizable
代替實現Serializable
接口來對序列化過程進行控制,Externalizable
繼承了Serializable
接口,同時增添了兩個方法:writeExternal()
和readExternal()
。
兩者在反序列化時的區別:
- 對Serializable對象反序列化時,由於Serializable對象完全以它存儲的二進制位為基礎來構造,因此並不會調用任何構造函數,因此Serializable類無需默認構造函數,但是當Serializable類的父類沒有實現Serializable接口時,反序列化過程會調用父類的默認構造函數,因此該父類必需有默認構造函數,否則會拋異常。
- 對Externalizable對象反序列化時,會先調用類的不帶參數的構造方法,這是有別於默認反序列方式的。如果把類的不帶參數的構造方法刪除,或者把該構造方法的訪問權限設置為private、默認或protected級別,會拋出java.io.InvalidException: no valid constructor
異常,因此Externalizable對象必須有默認構造函數,而且必需是public的。
- Externalizable
的替代方法:如果不是特別堅持實現Externalizable接口,那么還有另一種方法。我們可以實現Serializable
接口,並添加writeObject()
和readObject()
的方法。一旦對象被序列化或者重新裝配,就會分別調用那兩個方法。也就是說,只要提供了這兩個方法,就會優先使用它們,而不考慮默認的序列化機制。
這些方法必須含有下列准確的簽名:
private void writeObject(ObjectOutputStream stream) throws IOException;
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException
- 可以用transient
關鍵字逐個字段地關閉序列化,它的意思是“不用麻煩你保存或恢復數據—我自己會處理的”。由於Externalizable對象在默認情況下不保存它們的任何字段,所以transient關鍵字只能和Serializable對象一起使用。
12.泛型
無論所時,只要可以就應該盡量使用泛型方法。也就是說,如果使用泛型方法可以取代將整個類泛型化,那么就應該只使用泛型方法,因為它可以使事情更清楚明白。另外,對於一個static的方法而言,無法訪問泛型類的類型參數,所以,如果static方法需要使用泛型能力,就必須使其成為泛型方法。

1 class Order<T> { 2 public final T tFinal; 3 public Order(T v) { 4 tFinal = v; 5 } 6 public T getT() { 7 return tFinal; 8 } 9 10 public static <T> void staticFun(Order o) { // 這里必須使用泛型方法 11 T t = (T) o.getT(); 12 System.out.println(t.getClass()); 13 } 14 15 public void nonstaticFun(Order o) { 16 T t = (T) o.getT(); 17 System.out.println(t.getClass()); 18 } 19 20 @Override 21 public String toString() { 22 return tFinal.getClass().getCanonicalName(); 23 } 24 }
參考資料:
《Java編程思想》