天天看點

【轉載】Java程式設計思想重點筆記(Java開發必看)

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() = 0

    Sub子類實際上包含了兩個稱為field的域,然而在引用Sub中的field時所産生的預設域并非Super版本的field域,是以為了得到Super.field,必須顯式地指明super.field。

2. is-a關系和is-like-a關系

  • is-a關系屬于純繼承,即隻有在基類中已經建立的方法才可以在子類中被覆寫,如下圖所示:
    【轉載】Java程式設計思想重點筆記(Java開發必看)
    基類和子類有着完全相同的接口,這樣向上轉型時永遠不需要知道正在處理的對象的确切類型,這通過多态來實作。
  • is-like-a關系:子類擴充了基類接口。它有着相同的基本接口,但是他還具有由額外方法實作的其他特性。
    【轉載】Java程式設計思想重點筆記(Java開發必看)
    缺點就是子類中接口的擴充部分不能被基類通路,是以一旦向上轉型,就不能調用那些新方法。

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

類庫一起對反射的概念進行了支援,該類庫包含了

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("*** 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對象一起使用。

繼續閱讀