Java編程思想,Java學習必讀經典,無論是初學者仍是大牛都值得一讀,這裏總結書中的重點知識,這些知識不只常常出如今各大知名公司的筆試面試過程當中,並且在大型項目開發中也是經常使用的知識,既有簡單的概念理解題(好比is-a關係和has-a關係的區別),也有深刻的涉及RTTI和JVM底層反編譯知識。html
Java中除了static方法和final方法(private方法本質上屬於final方法,由於不能被子類訪問)以外,其它全部的方法都是動態綁定,這意味着一般狀況下,咱們沒必要斷定是否應該進行動態綁定—它會自動發生。java
class StaticSuper { public static String staticGet() { return "Base staticGet()"; } public String dynamicGet() { return "Base dynamicGet()"; } } class StaticSub extends StaticSuper { public static String staticGet() { return "Derived staticGet()"; } public String dynamicGet() { return "Derived dynamicGet()"; } } public class StaticPolymorphism { public static void main(String[] args) { StaticSuper sup = new StaticSub(); System.out.println(sup.staticGet()); System.out.println(sup.dynamicGet()); } }
Base staticGet()
Derived dynamicGet()程序員
構造函數並不具備多態性,它們其實是static方法,只不過該static聲明是隱式的。所以,構造函數不可以被override。面試
在父類構造函數內部調用具備多態行爲的函數將致使沒法預測的結果,由於此時子類對象還沒初始化,此時調用子類方法不會獲得咱們想要的結果。算法
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方法)。cookie
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。
is-a關係屬於純繼承,即只有在基類中已經創建的方法才能夠在子類中被覆蓋,以下圖所示:
基類和子類有着徹底相同的接口,這樣向上轉型時永遠不須要知道正在處理的對象的確切類型,這經過多態來實現。
is-like-a關係:子類擴展了基類接口。它有着相同的基本接口,可是他還具備由額外方法實現的其餘特性。
缺點就是子類中接口的擴展部分不能被基類訪問,所以一旦向上轉型,就不能調用那些新方法。
使用方式
Java是如何讓咱們在運行時識別對象和類的信息的,主要有兩種方式(還有輔助的第三種方式,見下描述):
Shape s = (Shape)s1;
Class.forName()
。instanceof
,它返回一個bool值,它保持了類型的概念,它指的是「你是這個類嗎?或者你是這個類的派生類嗎?」。而若是用==或equals比較實際的Class對象,就沒有考慮繼承—它或者是這個確切的類型,或者不是。工做原理
要理解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
能夠突破這個限制嗎?是的,突破它的就是反射機制。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])