Java編程思想重點筆記(Java開發必看)

Java編程思想,Java學習必讀經典,無論是初學者仍是大牛都值得一讀,這裏總結書中的重點知識,這些知識不只常常出如今各大知名公司的筆試面試過程當中,並且在大型項目開發中也是經常使用的知識,既有簡單的概念理解題(好比is-a關係和has-a關係的區別),也有深刻的涉及RTTI和JVM底層反編譯知識。java

 


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)最後調用子類的構造函數。cookie

  • 只有非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在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類庫一塊兒對反射的概念進行了支持,該類庫包含了FieldMethod以及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("*** proxy: " + proxy.getClass() +
                    ". method: " + method + ". args: " + args);
            if(args != null) {
                for(Object arg : args)
                    System.out.println(" " + arg);
            }
            return method.invoke(this.proxy, args);
        }
    }
    
    public class SimpleDynamicProxy {
    
        public static void consumer(Interface iface) {
            iface.doSomething();
            iface.somethingElse("bonobo");
        }
    
        public static void main(String[] args) {
            RealObject real = new RealObject();
            consumer(real);
            // insert a proxy and call again:
            Interface proxy = (Interface)Proxy.newProxyInstance(
                    Interface.class.getClassLoader(), 
                    new Class[]{ Interface.class },
                    new DynamicProxyHandler(real));
    
            consumer(proxy);
        }
    
    }
    


    輸出:

    doSomething.
    somethingElse bonobo
    *** proxy: class typeinfo.\$Proxy0. method: public abstract void typeinfo.Interface.doSomething(). args: null
    doSomething.
    *** proxy: class typeinfo.\$Proxy0. method: public abstract void typeinfo.Interface.somethingElse(java.lang.String). args: [Ljava.lang.Object;@6a8814e9
    bonobo
    somethingElse bonobo

5. 即時編譯器技術 — JIT

Java虛擬機中有許多附加技術用以提高速度,尤爲是與加載器操做相關的,被稱爲「即時」(Just-In-Time,JIT)編譯器的技術。這種技術能夠把程序所有或部分翻譯成本地機器碼(這原本是JVM的工做),程序運行速度所以得以提高。當須要裝載某個類時,編譯器會先找到其.class文件,而後將該類的字節碼裝入內存。此時,有兩種方案可供選擇:
(1)一種就是讓即時編譯器編譯全部代碼。但這種作法有兩個缺陷:這種加載動做散落在整個程序生命週期內,累加起來要花更多時間;而且會增長可執行代碼的長度(字節碼要比即時編譯器展開後的本地機器碼小不少),這將致使頁面調度,從而下降程序速度。
(2)另外一種作法稱爲惰性評估(lazy evaluation),意思是即時編譯器只在必要的時候才編譯代碼,這樣,從不會被執行的代碼也許就壓根不會被JIT所編譯。新版JDK中的Java HotSpot技術就採用了相似方法,代碼每次被執行的時候都會作一些優化,因此執行的次數越多,它的速度就越快。

6. 訪問控制權限

  • Java訪問權限修飾詞:public、protected、包訪問權限(默認訪問權限,有時也稱friendly)和private。
  • 包訪問權限:當前包中的全部其餘類對那個成員具備訪問權限,但對於這個包以外的全部類,這個成員倒是private。
  • protected:繼承訪問權限。有時基類的建立者會但願有某個特定成員,把對它的訪問權限賦予派生類而不是全部類。這就須要protected來完成這一工做。protected也提供包訪問權限,也就是說,相同包內的其餘類均可以訪問protected元素。protected指明「就類用戶而言,這是private的,但對於任何繼承於此類的導出類或其餘任何位於同一個包內的類來講,它倒是能夠訪問的」。好比:
    基類:
    package access.cookie;
    public class Cookie {
        public Cookie() {
            System.out.println("Cookie Constructor");
        }
    
        void bite() {  // 包訪問權限,其它包即便是子類也不能訪問它
            System.out.println("bite");
        }
    }
    

    子類:
    package access.dessert;
    import access.cookie.Cookie;
    
    public class ChocolateChip extends Cookie {
    
        public ChocolateChip() {
            System.out.println("ChocolateChip constructor");
        }
    
        public void chomp() {
            bite();  // error, the method bite() from the type Cookie is not visible
        }
    }
    

    能夠發現子類並不能訪問基類的包訪問權限方法。此時能夠將Cookie中的bite指定爲public,但這樣作全部的人就都有了訪問權限,爲了只容許子類訪問,能夠將bite指定爲protected便可。

7. 組合和繼承之間的選擇

  • 組合和繼承都容許在新的類中放置子對象,組合是顯式的這樣作,而繼承則是隱式的作。
  • 組合技術一般用於想在新類中使用現有類的功能而非它的接口這種情形。即在新類中嵌入某個對象,讓其實現所須要的功能,但新類的用戶看到的只是爲新類所定義的接口,而非所嵌入對象的接口。爲取得此效果,須要在新類中嵌入一個現有類的private對象。但有時,容許類的用戶直接訪問新類中的組合成分是極具意義的,即將成員對象聲明爲public。若是成員對象自身都隱藏了具體實現,那麼這種作法是安全的。當用戶可以瞭解到你正在組裝一組部件時,會使得端口更加易於理解。好比Car對象可由public的Engine對象、Wheel對象、Window對象和Door對象組合。但務必要記得這僅僅是一個特例,通常狀況下應該使域成爲private
  • 在繼承的時候,使用某個現有類,並開發一個它的特殊版本。一般,這意味着你在使用一個通用類,併爲了某種特殊須要而將其特殊化。稍微思考一下就會發現,用一個「交通工具」對象來構成一部「車子」是毫無心義的,由於「車子」並不包含「交通工具」,它僅是一種交通工具(is-a關係)。
  • 「is-a」(是一個)的關係是用繼承來表達的,而「has-a」(有一個)的關係則是用組合來表達的
  • 究竟是該用組合仍是繼承,一個最清晰的判斷方法就是問一問本身是否須要重新類向基類進行向上轉型,須要的話就用繼承,不須要的話就用組合方式。

8. final關鍵字

  • 對final關鍵字的誤解
    當final修飾的是基本數據類型時,它指的是數值恆定不變(就是編譯期常量,若是是static final修飾,則強調只有一份),而對對象引用而不是基本類型運用final時,其含義會有一點使人迷惑,由於用於對象引用時,final使引用恆定不變,一旦引用被初始化指向一個對象,就沒法再把它指向另外一個對象。然而,對象其自身倒是能夠被修改的,Java並未提供使任何對象恆定不變的途徑(但能夠本身編寫類以取得使對象恆定不變的效果),這一限制一樣適用數組,它也是對象。
  • 使用final方法真的能夠提升程序效率嗎?
    將一個方法設成final後,編譯器就能夠把對那個方法的全部調用都置入「嵌入」調用裏。只要編譯器發現一個final方法調用,就會(根據它本身的判斷)忽略爲執行方法調用機制而採起的常規代碼插入方法(將自變量壓入堆棧;跳至方法代碼並執行它;跳回來;清除堆棧自變量;最後對返回值進行處理)。相反,它會用方法主體內實際代碼的一個副原本替換方法調用。這樣作可避免方法調用時的系統開銷。固然,若方法體積太大,那麼程序也會變得雍腫,可能受到到不到嵌入代碼所帶來的任何性能提高。由於任何提高都被花在方法內部的時間抵消了。

在最近的Java版本中,虛擬機(特別是hotspot技術)能自動偵測這些狀況,並頗爲「明智」地決定是否嵌入一個final 方法。然而,最好仍是不要徹底相信編譯器能正確地做出全部判斷。一般,只有在方法的代碼量很是少,或者想明確禁止方法被覆蓋的時候,才應考慮將一個方法設爲final。

類內全部private 方法都自動成爲final。因爲咱們不能訪問一個private 方法,因此它絕對不會被其餘方法覆蓋(若強行這樣作,編譯器會給出錯誤提示)。可爲一個private方法添加final指示符,但卻不能爲那個方法提供任何額外的含義。

9. 策略設計模式與適配器模式的區別

  • 策略設計模式
    建立一個可以根據所傳遞的參數對象的不一樣而具備不一樣行爲的方法,被稱爲策略設計模式,這類方法包含所要執行的算法中固定不變的部分,而「策略」包含變化的部分。策略就是傳遞進去的參數對象,它包含要執行的代碼。
  • 適配器模式
    在你沒法修改你想要使用的類時,可使用適配器模式,適配器中的代碼將接受你所擁有的接口,併產生你所須要的接口。

10. 內部類

  • 內部類與組合是徹底不一樣的概念,這一點很重要。
  • 爲何須要內部類? — 主要是解決了多繼承的問題,繼承具體或抽象類

    • 通常來講,內部類繼承自某個類或實現某個接口,內部類的代碼操做建立它的外圍類的對象。因此能夠認爲內部類提供了某種進入其外圍類的窗口。
    • 內部類最吸引人的緣由是:每一個內部類都能獨立地繼承自一個(接口的)實現,因此不管外圍類是否已經繼承了某個(接口的)實現,對於內部類都沒有影響。
    • 若是沒有內部類提供的、能夠繼承多個具體的或抽象的類的能力,一些設計與編程問題就很難解決。從這個角度看,內部類使得多重繼承的解決方案變得完整。接口解決了部分問題,而內部類有效的實現了「多重繼承」。也就是說,內部類容許繼承多個非接口類型。

    考慮這樣一種情形:若是必須在一個類中以某種方式實現兩個接口。因爲接口的靈活性,你有兩種選擇:使用單一類或者使用內部類。但若是擁有的是抽象的類或具體的類,而不是接口,那就只能使用內部類才能實現多重繼承。

使用內部類,還能夠得到其餘一些特性:
- 內部類能夠有多個實例,每一個實例都有本身的狀態信息,而且與其外圍類對象的信息相互獨立。
- 在單個外圍類中,可讓多個內部類以不一樣的方式實現同一個接口或繼承同一個類。
- 建立內部類對象的時刻並不依賴於外圍類對象的建立。
- 內部類並無使人迷惑的is-a關係,它就是一個獨立的實體。

11. String類型 — 不可變

  • 用於String的「+」與「+=」是Java中僅有的兩個重載過的操做符,而Java並不容許程序員重載任何操做符。
  • 考慮到效率因素,編譯器會對String的屢次+操做進行優化,優化使用StringBuilder操做(javap -c class字節碼文件名 命令查看具體優化過程)。這讓你以爲能夠隨意使用String對象,反正編譯器會爲你自動地優化性能。但編譯器能優化到什麼程度還很差說,不必定能優化到使用StringBuilder代替String相同的效果。好比:
    public class WitherStringBuilder {
        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();
        }
    }
    

    運行javap -c WitherStringBuilder,能夠看到兩個方法對應的字節碼。
    implicit方法:

    public java.lang.String implicit(java.lang.String[]);
    Code:
    0: ldc #16 // String
    2: astore_2
    3: iconst_0
    4: istore_3
    5: goto 32
    8: new #18 // class java/lang/StringBuilder

    11: dup
    12: aload_2
    13: invokestatic #20 // Method java/lang/String.valueOf:(
    Ljava/lang/Object;)Ljava/lang/String;
    16: invokespecial #26 // Method java/lang/StringBuilder.」<
    init>」:(Ljava/lang/String;)V
    19: aload_1
    20: iload_3
    21: aaload
    22: invokevirtual #29 // Method java/lang/StringBuilder.ap
    pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    25: invokevirtual #33 // Method java/lang/StringBuilder.to
    String:()Ljava/lang/String;
    28: astore_2
    29: iinc 3, 1
    32: iload_3
    33: aload_1
    34: arraylength
    35: if_icmplt 8
    38: aload_2
    39: areturn
    public java.lang.String implicit(java.lang.String[]);
    Code:
    0: ldc #16 // String
    2: astore_2
    3: iconst_0
    4: istore_3
    5: goto 32
    8: new #18 // class java/lang/StringBuilder
    11: dup
    12: aload_2
    13: invokestatic #20 // Method java/lang/String.valueOf:(
    Ljava/lang/Object;)Ljava/lang/String;
    16: invokespecial #26 // Method java/lang/StringBuilder.」<
    init>」:(Ljava/lang/String;)V
    19: aload_1
    20: iload_3
    21: aaload
    22: invokevirtual #29 // Method java/lang/StringBuilder.ap
    pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    25: invokevirtual #33 // Method java/lang/StringBuilder.to
    String:()Ljava/lang/String;
    28: astore_2
    29: iinc 3, 1
    32: iload_3
    33: aload_1
    34: arraylength
    35: if_icmplt 8
    38: aload_2
    39: areturn

能夠發現,StringBuilder是在循環以內構造的,這意味着每通過循環一次,就會建立一個新的StringBuilder對象。

explicit方法:

public java.lang.String explicit(java.lang.String[]);
Code:
0: new #18 // class java/lang/StringBuilder
3: dup
4: invokespecial #45 // Method java/lang/StringBuilder.」<
init>」:()V
7: astore_2
8: iconst_0
9: istore_3
10: goto 24
13: aload_2
14: aload_1
15: iload_3
16: aaload
17: invokevirtual #29 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: pop
21: iinc 3, 1
24: iload_3
25: aload_1
26: arraylength
27: if_icmplt 13
30: aload_2
31: invokevirtual #33 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
34: areturn
}

能夠看到,不只循環部分的代碼更簡短、更簡單,並且它只生成了一個StringBuilder對象。顯式的建立StringBuilder還容許你預先爲其指定大小。若是你已經知道最終的字符串大概有多長,那預先指定StringBuilder的大小能夠避免屢次從新分配緩衝。


####總結
所以,當你爲一個類重寫toString()方法時,若是字符串操做比較簡單,那就能夠信賴編譯器,它會爲你合理地構造最終的字符串結果。可是,若是你要在toString()方法中使用循環,那麼最好本身建立一個StringBuilder對象,用它來構造最終的結果。


  • System.out.printf()System.out.format()方法模仿自C的printf,能夠格式化字符串,二者是徹底等價的。

Java中,全部新的格式化功能都由java.util.Formatter類處理。
String.format()方法參考了C中的sprintf()方法,以生成格式化的String對象,是一個static方法,它接受與Formatter.format()方法同樣的參數,但返回一個String對象。當你只需使用format()方法一次的時候,該方法很方便。

import java.util.Arrays;
import java.util.Formatter;

public class SimpleFormat {

    public static void main(String[] args) {
        int x = 5;
        double y = 5.324667;
        System.out.printf("Row 1: [%d %f]\n", x, y);
        System.out.format("Row 1: [%d %f]\n", x, y);

        Formatter f = new Formatter(System.out);
        f.format("Row 1: [%d %f]\n", x, y);

        String str = String.format("Row 1: [%d %f]\n", x, y);
        System.out.println(str);

        Integer[][] a = {
            {1, 2, 3}, {4, 5, 6},
            {7, 8, 3}, {9, 10, 6}
        };
        System.out.println(Arrays.deepToString(a));
    }
}

12. 序列化控制

  • 當咱們對序列化進行控制時,可能某個特定子對象不想讓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對象一塊兒使用。

相關文章
相關標籤/搜索