Java程式設計思想重點筆記(Java開發必看)
1. Java中的多型性理解(注意與C++區分)
-
Java中除了static方法和final方法(private方法本質上屬於final方法,因為不能被子類訪問)之外,其它所有的方法都是動態繫結,這意味著通常情況下,我們不必判定是否應該進行動態繫結—它會自動發生。
- final方法會使編譯器生成更有效的程式碼,這也是為什麼說宣告為final方法能在一定程度上提高效能(效果不明顯)。
- 如果某個方法是靜態的,它的行為就不具有多型性:
class StaticSuper { public static String staticGet() { return "Base staticGet()"
輸出:
Base staticGet() Derived dynamicGet()
-
建構函式並不具有多型性,它們實際上是static方法,只不過該static宣告是隱式的。
-
在父類建構函式內部呼叫具有多型行為的函式將導致無法預測的結果,因為此時子類物件還沒初始化,此時呼叫子類方法不會得到我們想要的結果。
class Glyph { void draw() { System.out.println("Glyph.draw()"); } Glyph() { System.out.println("Glyph() before draw()"); draw(); System.out.println("Glyph() after draw()"); } } class RoundGlyph extends Glyph { private int radius = 1; RoundGlyph(int r) { radius = r; System.out.println("RoundGlyph.RoundGlyph(). radius = " + radius); } void draw() { System.out.println("RoundGlyph.draw(). radius = " + radius); } } public class PolyConstructors { public static void main(String[] args) { new RoundGlyph(5); } }
輸出:
Glyph() before draw() RoundGlyph.draw(). radius = 0 Glyph() after draw() RoundGlyph.RoundGlyph(). radius = 5
為什麼會這樣輸出?這就要明確掌握Java中建構函式的呼叫順序:
(1)在其他任何事物發生之前,將分配給物件的儲存空間初始化成二進位制0; (2)呼叫基類建構函式。從根開始遞迴下去,因為多型性此時呼叫子類覆蓋後的draw()方法(要在呼叫RoundGlyph建構函式之前呼叫),由於步驟1的緣故,我們此時會發現radius的值為0; (3)按宣告順序呼叫成員的初始化方法; (4)最後呼叫子類的建構函式。
-
只有非private方法才可以被覆蓋,但是還需要密切注意覆蓋private方法的現象,這時雖然編譯器不會報錯,但是也不會按照我們所期望的來執行,即覆蓋private方法對子類來說是一個新的方法而非過載方法。因此,在子類中,新方法名最好不要與基類的private方法採取同一名字(雖然沒關係,但容易誤解,以為能夠覆蓋基類的private方法)。
-
Java類中屬性域的訪問操作都由編譯器解析,因此不是多型的。父類和子類的同名屬性都會分配不同的儲存空間,如下:
// Direct field access is determined at compile time. class Super { public int field = 0; public int getField() { return field; } } class Sub extends Super { public int field = 1; public int getField() { return field; } public int getSuperField() { return super.field; } } public class FieldAccess { public static void main(String[] args) { Super sup = new Sub(); System.out.println("sup.filed = " + sup.field + ", sup.getField() = " + sup.getField()); Sub sub = new Sub(); System.out.println("sub.filed = " + sub.field + ", sub.getField() = " + sub.getField() + ", sub.getSuperField() = " + sub.getSuperField()); } }
輸出:
sup.filed = 0, sup.getField() = 1 sub.filed = 1, sub.getField() = 1, sub.getSuperField() = 0
Sub子類實際上包含了兩個稱為field的域,然而在引用Sub中的field時所產生的預設域並非Super版本的field域,因此為了得到Super.field,必須顯式地指明super.field。
2. is-a關係和is-like-a關係
-
is-a關係屬於純繼承,即只有在基類中已經建立的方法才可以在子類中被覆蓋,如下圖所示: 基類和子類有著完全相同的介面,這樣向上轉型時永遠不需要知道正在處理的物件的確切型別,這通過多型來實現。
-
is-like-a關係:子類擴充套件了基類介面。它有著相同的基本介面,但是他還具有由額外方法實現的其他特性。 缺點就是子類中介面的擴充套件部分不能被基類訪問,因此一旦向上轉型,就不能呼叫那些新方法。
3. 執行時型別資訊(RTTI + 反射)
- 概念 RTTI:執行時型別資訊使得你可以在程式執行時發現和使用型別資訊。
-
使用方式 Java是如何讓我們在執行時識別物件和類的資訊的,主要有兩種方式(還有輔助的第三種方式,見下描述):
- 一種是“傳統的”RTTI,它假定我們在編譯時已經知道了所有的型別,比如
Shape s = (Shape)s1;
- 另一種是“反射”機制,它執行我們在執行時發現和使用類的資訊,即使用
Class.forName()
。 - 其實還有第三種形式,就是關鍵字
instanceof
,它返回一個bool值,它保持了型別的概念,它指的是“你是這個類嗎?或者你是這個類的派生類嗎?”。而如果用==或equals比較實際的Class物件,就沒有考慮繼承—它或者是這個確切的型別,或者不是。
- 一種是“傳統的”RTTI,它假定我們在編譯時已經知道了所有的型別,比如
-
工作原理 要理解RTTI在Java中的工作原理,首先必須知道型別資訊在執行時是如何表示的,這項工作是由稱為
Class物件
的特殊物件完成的,它包含了與類有關的資訊。Java送Class物件來執行其RTTI,使用類載入器的子系統實現。無論何時,只要你想在執行時使用型別資訊,就必須首先獲得對恰當的Class物件的引用,獲取方式有三種: (1)如果你沒有持有該型別的物件,則
Class.forName()
就是實現此功能的便捷途,因為它不需要物件資訊; (2)如果你已經擁有了一個感興趣的型別的物件,那就可以通過呼叫getClass()
方法來獲取Class引用了,它將返回表示該物件的實際型別的Class引用。Class包含很有有用的方法,比如:package rtti; interface HasBatteries{} interface WaterProof{} interface Shoots{} class Toy { Toy() {} Toy(int i) {} } class FancyToy extends Toy implements HasBatteries, WaterProof, Shoots { FancyToy() { super(1); } } public class RTTITest { static void printInfo(Class cc) { System.out.println("Class name: " + cc.getName() + ", is interface? [" + cc.isInterface() + "]"); System.out.println("Simple name: " + cc.getSimpleName()); System.out.println("Canonical name: " + cc.getCanonicalName()); } public static void main(String[] args) { Class c = null; try { c = Class.forName("rtti.FancyToy"); // 必須是全限定名(包名+類名) } catch(ClassNotFoundException e) { System.out.println("Can't find FancyToy"); System.exit(1); } printInfo(c); for(Class face : c.getInterfaces()) { printInfo(face); } Class up = c.getSuperclass(); Object obj = null; try { // Requires default constructor. obj = up.newInstance(); } catch (InstantiationException e) { System.out.println("Can't Instantiate"); System.exit(1); } catch (IllegalAccessException e) { System.out.println("Can't access"); System.exit(1); } printInfo(obj.getClass()); } }
輸出:
Class name: rtti.FancyToy, is interface? [false] Simple name: FancyToy Canonical name: rtti.FancyToy Class name: rtti.HasBatteries, is interface? [true] Simple name: HasBatteries Canonical name: rtti.HasBatteries Class name: rtti.WaterProof, is interface? [true] Simple name: WaterProof Canonical name: rtti.WaterProof Class name: rtti.Shoots, is interface? [true] Simple name: Shoots Canonical name: rtti.Shoots Class name: rtti.Toy, is interface? [false] Simple name: Toy Canonical name: rtti.Toy
(3)Java還提供了另一種方法來生成對Class物件的引用,即使用類字面常量。比如上面的就像這樣:
FancyToy.class;
來引用。這樣做不僅更簡單,而且更安全,因為它在編譯時就會受到檢查(因此不需要置於try語句塊中),並且它根除了對forName方法的引用,所以也更高效。類字面常量不僅可以應用於普通的類,也可以應用於介面、陣列以及基本資料型別。
注意:當使用“.class”來建立對Class物件的引用時,不會自動地初始化該Class物件,初始化被延遲到了對靜態方法(構造器隱式的是靜態的)或者非final靜態域(注意final靜態域不會觸發初始化操作)進行首次引用時才執行:。而使用Class.forName時會自動的初始化。
為了使用類而做的準備工作實際包含三個步驟:
- 載入:由類載入器執行。查詢位元組碼,並從這些位元組碼中建立一個Class物件
- 連結:驗證類中的位元組碼,為靜態域分配儲存空間,並且如果必需的話,將解析這個類建立的對其他類的所有引用。
- 初始化:如果該類具有超類,則對其初始化,執行靜態初始化器和靜態初始化塊。
這一點非常重要,下面通過一個例項來說明這兩者的區別:
package rtti; import java.util.Random; class Initable { static final int staticFinal = 47; static final int staticFinal2 = ClassInitialization.rand.nextInt(1000); static { System.out.println("Initializing Initable"); } } class Initable2 { static int staticNonFinal = 147; static { System.out.println("Initializing Initable2"); } } class Initable3 { static int staticNonFinal = 74; static { System.out.println("Initializing Initable3"); } } public class ClassInitialization { public static Random rand = new Random(47); public static void main(String[] args) { // Does not trigger initialization Class initable = Initable.class; System.out.println("After creating Initable ref"); // Does not trigger initialization System.out.println(Initable.staticFinal); // Does trigger initialization(rand() is static method) System.out.println(Initable.staticFinal2); // Does trigger initialization(not final) System.out.println(Initable2.staticNonFinal); try { Class initable3 = Class.forName("rtti.Initable3"); } catch (ClassNotFoundException e) { System.out.println("Can't find Initable3"); System.exit(1); } System.out.println("After creating Initable3 ref"); System.out.println(Initable3.staticNonFinal); } }
輸出:
After creating Initable ref 47 Initializing Initable 258 Initializing Initable2 147 Initializing Initable3 After creating Initable3 ref 74
-
RTTI的限制?如何突破? — 反射機制 如果不知道某個物件的確切型別,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檔案。下面的例子是用反射機制打印出一個類的所有方法(包括在基類中定義的方法):
package typeinfo; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.regex.Pattern; // Using reflection to show all the methods of a class. // even if the methods are defined in the base class. public class ShowMethods { private static String usage = "usage: \n" + "ShowMethods qualified.class.name\n" + "To show all methods in class or: \n" + "ShowMethods qualified.class.name word\n" + "To search for methods involving 'word'"; private static Pattern p = Pattern.compile("\\w+\\."); public static void main(String[] args) { if(args.length < 1) { System.out.println(usage); System.exit(0); } int lines = 0; try { Class<?> c = Class.forName(args[0]); Method[] methods = c.getMethods(); Constructor[] ctors = c.getConstructors(); if(args.length == 1) { for(Method method : methods) { System.out.println(p.matcher(method.toString()).replaceAll("")); } for(Constructor ctor : ctors) { System.out.println(p.matcher(ctor.toString()).replaceAll("")); } lines = methods.length + ctors.length; } else { for(Method method : methods) { if(method.toString().indexOf(args[1]) != -1) { System.out.println(p.matcher(method.toString()).replaceAll("")); lines++; } } for(Constructor ctor : ctors) { if(ctor.toString().indexOf(args[1]) != -1) { System.out.println(p.matcher(ctor.toString()).replaceAll("")); lines++; } } } } catch (ClassNotFoundException e) { System.out.println("No such Class: " + e); } } }
輸出:
public static void main(String[]) public final native void wait(long) throws InterruptedException public final void wait() throws InterruptedException public final void wait(long,int) throws InterruptedException public boolean equals(Object) public String toString() public native int hashCode() public final native Class getClass() public final native void notify() public final native void notifyAll() public ShowMethods()
4. 代理模式與Java中的動態代理
-
代理模式 在任何時刻,只要你想要將額外的操作從“實際”物件中分離到不同的地方,特別是當你希望能夠很容易地做出修改,從沒有使用額外操作轉為使用這些操作,或者反過來時,代理就顯得很有用(設計模式的關鍵是封裝修改)。例如,如果你希望跟蹤對某個類中方法的呼叫,或者希望度量這些呼叫的開銷,那麼你應該怎樣做呢?這些程式碼肯定是你不希望將其合併到應用中的程式碼,因此代理使得你可以很容易地新增或移除它們。
interface Interface { void doSomething(); void somethingElse(String arg); } class RealObject implements Interface { @Override public void doSomething() { System.out.println("doSomething."); } @Override public void somethingElse(String arg) { System.out.println("somethingElse " + arg); } } class SimpleProxy implements Interface { private Interface proxy; public SimpleProxy(Interface proxy) { this.proxy = proxy; } @Override public void doSomething() { System.out.println("SimpleProxy doSomething."); proxy.doSomething(); } @Override public void somethingElse(String arg) { System.out.println("SimpleProxy somethingElse " + arg); proxy.somethingElse(arg); } } public class SimpleProxyDemo { public static void consumer(Interface iface) { iface.doSomething(); iface.somethingElse("bonobo"); } public static void main(String[] args) { consumer(new RealObject()); consumer(new SimpleProxy(new RealObject())); } }
輸出:
doSomething. somethingElse bonobo SimpleProxy doSomething. doSomething. SimpleProxy somethingElse bonobo somethingElse bonobo
-
動態代理 Java的動態代理比代理的思想更向前邁進了一步,因為它可以動態地建立代理並動態地處理對所代理方法的呼叫。
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; class DynamicProxyHandler implements InvocationHandler { private Object proxy; public DynamicProxyHandler(Object proxy) { this.proxy = proxy; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("**