Java編程思想,Java學習必讀經典,不管是初學者還是大牛都值得一讀,這里總結書中的重點知識,這些知識不僅經常出現在各大知名公司的筆試面試過程中,而且在大型項目開發中也是常用的知識,既有簡單的概念理解題(比如is-a關系和has-a關系的區別),也有深入的涉及RTTI和JVM底層反編譯知識。
1. Java中的多態性理解(注意與C++區分)
-
Java中除了static方法和final方法(private方法本質上屬於final方法,因為不能被子類訪問)之外,其它所有的方法都是動態綁定,這意味着通常情況下,我們不必判定是否應該進行動態綁定—它會自動發生。
- final方法會使編譯器生成更有效的代碼,這也是為什么說聲明為final方法能在一定程度上提高性能(效果不明顯)。
- 如果某個方法是靜態的,它的行為就不具有多態性:
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方法)。
-
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() = 0Sub子類實際上包含了兩個稱為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