編寫高質量代碼:改善Java程序的151個建議(第1章:Java開發中通用的方法和準則___建議14~20)

做爲一個由影視圈轉行作Java的菜鳥來講,讀書是很關鍵的,本系列是用來記錄《編寫高質量代碼 改善java程序的151個建議》這本書的讀書筆記。方便本身查看,也方便你們查閱。java

建議14:使用序列化類的私有方法巧妙解決部分屬性持久化問題編程

建議15:break萬萬不可忘數組

建議16:易變業務使用腳本語言編寫安全

建議17:慎用動態編譯服務器

建議18:淺談Java instanceofapp

建議19:斷言絕對不是雞肋框架

建議20:不要只替換一個類分佈式

建議14:使用序列化類的私有方法巧妙解決部分屬性持久化問題ide

例如:一個計稅系統和一個HR系統,計稅系統須要從HR系統得到人員的姓名和基本工資,而HR系統的工資分爲兩部分:基本工資和績效工資,績效工資是保密的,不能泄露到外系統。性能

public class Salary implements Serializable {
    private static final long serialVersionUID = 2706085398747859680L;
    // 基本工資
    private int basePay;
    // 績效工資
    private int bonus;

    public Salary(int _basepay, int _bonus) {
        this.basePay = _basepay;
        this.bonus = _bonus;
    }
//Setter和Getter方法略
}
public class Person implements Serializable {

    private static final long serialVersionUID = 9146176880143026279L;

    private String name;

    private Salary salary;

    public Person(String _name, Salary _salary) {
        this.name = _name;
        this.salary = _salary;
    }

    //Setter和Getter方法略

}
public class Serialize {
    public static void main(String[] args) {
        // 基本工資1000元,績效工資2500元
        Salary salary = new Salary(1000, 2500);
        // 記錄人員信息
        Person person = new Person("張三", salary);
        // HR系統持久化,並傳遞到計稅系統
        SerializationUtils.writeObject(person);
    }
}
public class Deserialize {
    public static void main(String[] args) {
        Person p = (Person) SerializationUtils.readObject();
        StringBuffer buf = new StringBuffer();
        buf.append("姓名: "+p.getName());
        buf.append("\t基本工資: "+p.getSalary().getBasePay());
        buf.append("\t績效工資: "+p.getSalary().getBonus());
        System.out.println(buf);
    }
}

af478a1afde9b65ffcc8911a60f2900dc5d.jpg

但這個不符合需求,你可能會想到一下四種解決方案:

一、java 的transient關鍵字爲咱們提供了便利,你只須要實現Serilizable接口,將不須要序列化的屬性前添加關鍵字transient,序列化對象的時候,這個屬性就不會序列化到指定的目的地中。

static修飾的變量也不能序列化。

在bonus前加上關鍵字transient,使用transient關鍵字就標誌着salary失去了分佈式部署的功能,一旦出現性能問題,再想分佈式部署就不可能了,此方案否認。

注:分佈式部署是將數據分散的存儲於多臺獨立的機器設備上,採用可擴展的系統結構,利用多臺存儲服務器分擔存儲負擔,利用未知服務器定位存儲信息,提升了系統的可靠性、可用性和擴展性。

二、新增業務對象:增長一個Person4Tax類,徹底爲計稅系統服務,就是說它只有兩個屬性:姓名和基本工資。符合開閉原則,並且對原系統也沒有侵入性,只是增長了工做量而已。可是這個方法不是最優方法;

下面展現一個優秀的方案,其中實現了Serializable接口的類能夠實現兩個私有方法:writeObject和readObject,以影響和控制序列化和反序列化的過程。

public class Person implements Serializable {

    private static final long serialVersionUID = 9146176880143026279L;

    private String name;

    private transient Salary salary;

    public Person(String _name, Salary _salary) {
        this.name = _name;
        this.salary = _salary;
    }
    //序列化委託方法
    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject();
        oos.writeInt(salary.getBasePay());
    }
    //反序列化委託方法
    private void readObject(ObjectInputStream input)throws ClassNotFoundException, IOException {
        input.defaultReadObject();
        salary = new Salary(input.readInt(), 0);
    }
}

其它代碼不作任何改動,運行以後結果爲:

08661a2169c7a43566650ae1fac47f496a5.jpg

這裏用到了序列化的獨有機制:序列化回調。

Java調用ObjectOutputStream類把一個對象轉換成數據流時,會經過反射(refection)檢查被序列化的類是否有writeObject方法,而且檢查其實否符合私有,無返回值的特性,如有,則會委託該方法進行對象序列化,若沒有,則由ObjectOutputStream按照默認規則繼續序列化。一樣,從流數據恢復成實例對象時,也會檢查是否有一個私有的readObject方法,若是有經過該方法讀取屬性值。

① oos.defaultWriteObject():告知JVM按照默認規則寫入對象

② ois.defaultWriteObject():告知JVM按照默認規則讀出對象

③ oos.writeXX和ois.readXX

分別是寫入和對出響應的值,相似一個隊列,先進先出,若是此處有複雜的數據邏輯,建議按封裝Collection對象處理。

上面的方式也是Person失去了分佈式部署的能了,確實是,可是HR系統的難點和重點是薪水的計算,特別是績效工資,它所依賴的參數很複雜,計算公式也不簡單(通常是引入腳本語言,個性化公式定製)而相對來講Person類基本上都是靜態屬性,計算的可能性不大,因此即便爲性能考慮,Person類爲分佈式部署的意義也不大。

既然這樣,爲什麼不直接使用transient???

建議15:break萬萬不可忘

建議16:易變業務使用腳本語言編寫

好比PHP、Ruby、groovy、JavaScript等

建議17:慎用動態編譯

動態編譯一直是Java的夢想,從Java6開始支持動態編譯,能夠在運行期直接編譯.Java文件,執行.class文件,而且得到相關的輸入輸出,甚至還能監聽相關的事件。

一、概念

靜態編譯:一次性編譯,在編譯的時候把你全部的模塊都編譯進去。

動態編譯:按需編譯,程序在運行的時候,用到哪一個模塊就編譯哪一個模塊。

二、代碼實例

public class Ay{
    public static void main(String[] args) throws Exception{
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        int flag = compiler.run(null, null, null,"D:\\HelloWorld.java");
        System.out.println(flag == 0 ? "編譯成功" : "編譯失敗");
    }
}

/**
 * D盤放置的類的內容
 */
public class HelloWorld {

    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

解釋一下:
第一個參數:爲java編譯器提供參數
第二個參數:獲得java編譯器的輸出信息
第三個參數:接受編譯器的錯誤信息
第四個參數:可變參數(是一個String數組)能傳入一個或多個java源文件
返回值:0表示編譯成功,非0表示編譯失敗

三、動態運行編譯好的類

public class Ay{
    public static void main(String[] args) throws Exception{
        //得到系統的java編譯器
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        //編譯文件,編譯成功返回 0 不然 返回 1
        int flag = compiler.run(null, null, null,"D:\\HelloWorld.java");
        System.out.println(flag == 0 ? "編譯成功" : "編譯失敗");
        //指定class路徑,默認和源代碼路徑一致,加載class
        URLClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:/d:/")});
        Object printer = classLoader.loadClass("HelloWorld").newInstance();
        System.out.println(printer.toString());

    }
}

運行結果:

編譯成功
HelloWorld@4c583ecf

四、慎用動態編譯

① 在框架中謹慎使用

好比要在struts中使用動態編譯,動態實現一個類,它若繼承自ActionSupport就但願它成爲一個Action。能作到,可是debug很困難;再好比在Spring中,寫一個動態類,要讓它注入到Spring容器中,這是須要花費老大功夫的。

② 不要在要求高性能的項目中使用

動態編譯畢竟須要一個編譯過程,與靜態編譯相比多了一個執行環節,所以在高性能的項目中不要使用動態編譯

③ 動態編譯要考慮安全問題

它是很是典型的注入漏洞,只要上傳一個惡意Java程序就可讓你全部的安全工做毀於一旦。

④ 記錄動態編譯過程

建議記錄源文件、目標文件、編譯過程、執行過程等日誌。不只僅爲了診斷,仍是爲了安全和審計,對Java項目來講,動態編譯和運行時很不讓人放心的,留下這些依據能夠更好地優化程序。

建議18:淺談Java instanceof

一、instanceof是Java中的二元運算符,左邊是對象,右邊是類;當對象是右邊類或子類所建立的對象時,返回true,否者,返回false。

注:

① 類的實例包含自己的實例,以及全部直接或間接子類的實例

② instanceof左邊顯示聲明的類型與右邊操做元必須是同種類或存在繼承關係,也就是說須要位於同一個繼承樹,否者會編譯報錯

二、instanceof用法

① 左邊的對象實例不能是基本數據類型

② 左邊的對象和右邊的類不在同一個繼承樹上

③ null用instanceof跟任何類型比較時都是false

建議19:斷言絕對不是雞肋

一、簡介

斷言也就是所謂的assert,是jdk1.4中加入的新功能。

他主要使用在代碼開發和測試階段,用於對某些關鍵數據的判斷,若是這個關鍵數據不是你程序所預期的數據,程序就提出警告或退出。

當軟件正式發佈後,能夠取消斷言部分的代碼。

二、語法:

assert<布爾表達式>

assert<布爾表達式> : <錯誤信息>

在布爾表達式爲假時,跑出AssertionError錯誤,並附帶了錯誤信息。

assert的語法比較簡單,有如下兩個特性:

① assert默認是不開啓的

而後再VM欄裏輸入-enableassertions或者-ea便可

② assert跑出的異常AssertionError繼承自Error

斷言失敗後,JVM會跑出一個AssertionError的錯誤,它繼承自Error,這是一個錯誤,不可恢復。

三、assert的使用禁忌

① 對外的公開方法中不可以使用

防護式編程最核心的部分就是:全部的外部因素(輸入參數、環境變量、上下文)都是「邪惡」的,都存在着企圖摧毀程序的罪惡本源,爲了抵制它,咱們要在程序到處設置合法性檢查,不知足條件就不執行後續程序,以保護後續程序的正確性,但此時不能用斷言作輸入檢查,特別是公開方法。

② 在執行邏輯代碼的狀況下

assert的支持是可選的,在開發時運行,生產環境下中止運行便可,所以在assert的布爾表達式中不能執行邏輯代碼,否者會由於環境的不一樣產生不一樣的邏輯。

public void doSomething(List list, Object element) {
        assert list.remove(element) : "刪除元素" + element + "失敗";
        /*業務處理*/
    }

這段代碼在assert啓用的環境下,沒有任何問題,可是一旦投入生產環境,就不會啓用斷言了,這個方法就完全完蛋,list的刪除動做永遠不會執行,永遠不會報錯或異常,由於根本沒有執行!

四、assert的使用場景

按照正常的執行邏輯不可能到達的代碼區域可使用assert。

① 在私有方法中放置assert做爲輸入參數的校驗

私有方法的使用者是本身,是本身能夠控制的,所以加上assert能夠更好地預防本身犯錯或者無心的程序犯錯。

② 流程控制中不可能到達的區域

程序執行到assert這裏就是錯誤的

③ 創建程序探針

咱們可能會在一段程序中定義兩個變量,分別代兩個不一樣的業務含義,可是二者有固定的關係,例如:var1=var2 * 2,那咱們就能夠在程序中處處設"樁"了,斷言這二者的關係,若是不知足即代表程序已經出現了異常,業務也就沒有必要運行下去了。

建議20:不要只替換一個類

注意:發佈應用系統時禁止使用類文件替換方式,總體WAR包發佈纔是萬全之策。

 

編寫高質量代碼:改善Java程序的151個建議@目錄

相關文章
相關標籤/搜索