1. 程式人生 > >01-第一章 Java開發中通用的方法和準則

01-第一章 Java開發中通用的方法和準則

建議1:不用在常量和變數中出現易混淆的字母

     包括名全小寫,類名首字母全大寫,常量全部大寫並用下劃線分割,變數採用駝峰命名法(Camel Case)命名等。

     例如:

/**

* 數字後跟小寫字母l的問題

*/

public class Client {

     public static void main(String[] args) {

          long i = 1l;

          System.out.println("i的兩倍是:" + (i+i));

     }

}

     句中定義一個長整型變數1,但後面的字母‘l’識別符號在很多字型中都非常類似數字‘1’,所以很容易誤以為變數i的值為十一。

     因此,如果字母和數字必須混合使用,字母‘l’務必大寫,字母‘o’則增加註釋。

建議2:莫讓常量蛻變成變數

import java.util.Random;

/**

* 莫讓常量變成變數

*/

public class Client {    

     public static void main(String[] args) {

          System.out.println("常量會變哦:" + Const.RAND_CONST);

     }

}

/*介面常量*/

interface Const{

     //這還是常量嗎?

     public static final int RAND_CONST = new Random().nextInt();

}

     語句中雖然想要定義一個常量,但卻賦值了一個不確定的值,這樣使得程式可讀性非常差。

     常量就是常量,在編譯期必須確定。

建議3:三元操作符的型別務必一致

/**

* 三元操作符兩個運算元的型別必須一致

*/

public class Client {

     public static void main(String[] args) {

          int i = 80;

          String s = String.valueOf(i<100?90:100);

          String s1 = String.valueOf(i<100?90:100.0);

          System.out.println("兩者是否相等:"+s.equals(s1));

     }

}

     執行結果:兩者是否相等:false

     分析:

          三元操作符必須要返回一個數據,而且型別確定,不可能條件為真時返回int型別,條件為假時返回float型別,編譯器是不允許如此的,所以它會進行型別轉換。

     三元操作符型別轉換規則:

     a、如果兩個運算元不可轉換,則不做轉換,返回值為Object型別。

     b、若兩個運算元是明確型別的表示式(比如變數),則按照正常的二進位制數字來轉換,int型別轉換為long型別,long型別轉換為float型別等。

     c、若兩個運算元中有一個數字S,另一個是表示式,且其型別標識為T,那麼,若數字S在T的範圍內,則轉換為T型別;若S超出T型別的範圍,則T轉換為S型別。

     d、若兩個運算元都是直接量數字(Literal),則返回值型別為範圍較大者。

建議4:避免帶有變長引數的方法過載

     為了提高方法的靈活度和可複用性,我們經常要傳遞不確定數量的引數到方法中,在Java5之前常用的設計技巧就是把形參定義成Collection型別或其子類型別,或者是陣列型別,這種方法的缺點就是需要對空引數進行判斷和篩選,比如引入實參為null值和長度為0的Collection或陣列。而Java5引入變長引數(varags)就是為了更好地提高方法複用性,讓方法呼叫者可以“隨心所欲”地傳遞是引數量,當然變長引數也是要遵循一定規則的,比如變長引數必須是方法中的最後一個引數;一個方法不能定義多個變長引數等,這些規則要牢記,但是即使記住規則,往往還是會犯錯。

import java.text.NumberFormat;

/**

* 建議4:避免帶變長引數的方法的過載

*/

public class Client {

     //簡單折扣計算

     public void calPrice(int price,int discount){

          float knockdownPrice =price * discount / 100.0F;

          System.out.println("簡單折扣後的價格是:"+formateCurrency(knockdownPrice));

     }

     //複雜多折扣計算

     public void calPrice(int price,int... discounts){

          float knockdownPrice = price;

          for(int discount:discounts){

               knockdownPrice = knockdownPrice * discount / 100;

          }

          System.out.println("複雜折扣後的價格是:" +formateCurrency(knockdownPrice));

     }

     //格式化成本地貨幣形式

     private String formateCurrency(float price){

          return NumberFormat.getCurrencyInstance().format(price/100);

     }

     public static void main(String[] args) {

          Client client = new Client();

          //499元的貨物,打75折

          client.calPrice(49900, 75);

     }

}    

上面程式中存在兩個過載的方法,程式執行時選擇了第一個。

     編譯器在選擇方法的時候會根據方法簽名(Method Signature)來確定呼叫哪個方法。然後根據實參的數量和型別確定呼叫哪個方法。編譯器之所以選擇兩個int型的實參而不是一個int型一個int陣列的方法,是因為int是一個原生資料型別,而且陣列本身是一個物件,編譯器想要偷懶,所以會選擇簡單的,只要符合編譯條件就通過。

變長引數的方法可以使用,但要儘量避免過載,否則也會使程式的可讀性降低。

建議5:別讓null值和空值威脅到變長方法

/**

* 帶有變長引數的方法過載,在呼叫時失敗。

*

*/

public class Client {

     public void methodA(String str,Integer... is){         

          System.out.println("Integer");

     }

     public void methodA(String str,String... strs){         

          System.out.println("String");

     }

     public static void main(String[] args) {

          Client client = new Client();

          client.methodA("China", 0);

          client.methodA("China", "People");

          client.methodA("China");

          client.methodA("China",null);

     }

}

程式中client.methodA("China");和client.methodA("China",null);兩處編譯不通過,提示相同:方法模糊不清,編譯器不知道呼叫哪一個方法。

     該Client類違反了KISS原則(Keep it Simple, Stupid, 即懶人原則),按照此規則設計的方法應該很容易呼叫。

對於client.methodA("China",null);方法,直接量null是沒有型別的,雖然兩個方法都符合呼叫請求,但不知道呼叫哪一個,於是報錯了。另外呼叫者最好不該隱藏實參型別,這樣的話不僅僅需要呼叫者猜測該呼叫哪個方法,而且被呼叫者也產生內部邏輯混亂。應該修改如下:

/**

* 帶有變長引數的方法過載,在呼叫時失敗。

*

*/

public class Client {

     public void methodA(String str,Integer... is){         

          System.out.println("Integer");

     }

     public void methodA(String str,String... strs){         

          System.out.println("String");

     }

     public static void main(String[] args) {

          Client client = new Client();

          String[] strs = null;

          client.methodA("China",strs);

     }

}

建議6:重寫變長方法也循規蹈矩

     重寫必須滿足的條件:

     1、重寫方法不能縮小訪問許可權。

     2、引數列表必須與被重寫方法相同。

     3、返回型別必須與被重寫方法的相同或是其子類。

     4、重寫方法不能丟擲新的異常,或者超出父類範圍的異常,但是可以丟擲更少、更有限的異常,或者不丟擲異常。

     引數列表相同指:引數數量相同、型別相同、順序相同

/**

* 覆寫變長方法也循規蹈矩

*/

public class Client {

     public static void main(String[] args) {

          //向上轉型

          Base  base = new Sub();

          base.fun(100, 50);

          //不轉型

          Sub sub = new Sub();

          //sub.fun(100, 50);

     }

}

//基類

class Base{

     void fun(int price,int... discounts){

          System.out.println("Base……fun");

     }    

}

//子類,覆寫父類方法

class Sub extends Base{

     @Override

     void fun(int price,int[] discounts){

          System.out.println("Sub……fun");

     }

}

     程式中子類呼叫方法的地方會編譯錯誤,因為int型別陣列也是一種物件,編譯器並不會把int型別轉換為int型別陣列。由於父類的方法是變長引數,所以會自動轉換為int型別陣列。

建議7:警惕自增的陷阱

/**

* 警惕自增的陷阱

*

*/

public class Client {

     public static void main(String[] args) {

          int count =0;

          for(int i=0;i<10;i++){

               count=count++;

          }

          System.out.println("count="+count);

     }

}

class Mock{

     public static void main(String[] args) {

          int count =0;

          for(int i=0;i<10;i++){

               count=mockAdd(count);

          }

          System.out.println("count="+count);

     }

     public static int mockAdd(int count){

          //先儲存初始值

          int temp =count;

          //做自增操作

          count = count+1;

          //返回原始值

          return temp;

     }

}

     Client的main函式中count的值依然是0。

count++是一個表示式,返回值是count自加前的值。即count=count++;就相當於count=mockAdd(count);

若要修改這種問題只需把count=count++改為count++

這種情況PHP和Java的處理方式相同,但是C++中count=count++和count++是相同的。

建議8:不要讓就語法困擾你

/**

* 不用讓舊語法困擾你

*

*/

public class Client {

    public static void main(String[] args) {

        //資料定義及初始化

        int fee=200;

        //其他業務處理

        saveDefault:save(fee);

        //其他業務處理

    }

    static void saveDefault(){

    static void save(int fee){

    }

}

語句saveDefault:save(fee);使用的語法是C語言中用到的標號,用於goto語句。

     雖然Java拋棄了goto語法,但還是保留了該關鍵字,只是不進行語義處理而已,與此類似的還有const關鍵字。

     Java雖然沒有goto,但是擴充套件了break和continue關鍵字,它們的後面都可以加上標號做跳轉,完全實現了goto功能,但同時也把goto的詬病帶了進來。在閱讀大牛的開源程式時,根本就看不到break或continue後跟標號的情況,甚至break和continue都很少看到,這是提高程式碼可讀性很好的一個方法,所以要儘量摒棄舊語法。

建議9:少用靜態匯入

     從Java5開始引入了靜態匯入語法(import static),其目的是為了減少字元輸入量,提高程式碼的可閱讀性。

     但是濫用靜態匯入會使程式更難閱讀,更難維護。靜態匯入後,程式碼中就不用再寫類名了,但是我們知道類是"一些事物的描述",缺少了類名的修飾,靜態屬性和靜態方法的表象意義就可以被無限放大,這會讓閱讀者很難弄清楚其屬性或方法代表何意,甚至是哪個類的屬性(方法)都有思考一番。例如:

package com.company.section3;

import java.text.NumberFormat;

import static java.lang.Double.*;

import static java.lang.Math.*;

import static java.lang.Integer.*;

import static java.text.NumberFormat.*;

public class Client {

     //輸入半徑和精度要求,計算面積

     public static void main(String[] args) {

          double s = PI * parseDouble(args[0]);

          NumberFormat nf = getInstance();

          nf.setMaximumFractionDigits(parseInt(args[1]));         

          formatMessage(nf.format(s));

     }

     //格式化訊息輸出

     public static void formatMessage(String s){

          System.out.println("圓面積是:"+s);

     }

}

程式中NumberFormat nf = getInstance();一句中的getInstance()讓人摸不著頭腦,不能直接鮮明的看到這個方法是哪個類的。

     所以對於靜態匯入,一定要遵循兩個原則:

     》不使用*(星號萬用字元,除非是匯入靜態常量類(只包含常量的類或介面))。

     》方法名是具有明確、清晰表象意義的工具類。

建議10:不要在本類中覆蓋靜態匯入的變數和方法

     如果在本類中覆蓋了靜態匯入的變數和方法,那麼在呼叫的時候會呼叫本類中的變數和方法,這符合編譯器的“最短路徑”原則。

     “最短路徑”原則:如果能夠在本類中查詢到變數、常量、方法,就不會到其他包或父類、介面中查詢,以確保本類中的屬性、方法優先。

     因此,如果要變更一個被靜態匯入的方法,最好的辦法是在原始類中重構,而不是在本類中覆蓋。

建議11:養成良好習慣,顯示宣告UID

     首先介紹一下序列化和反序列化:

     類實現Serializable介面的目的是為了可持久化,比如網路傳輸和本地儲存,為系統在分佈和異構部署提供先決條件。

     在序列化和反序列化的過程中,如果兩邊類版本不一致(例如增加了個屬性)。反序列化時就會報一個InvalidClassException異常。

     那麼如何解決這種版本不一致的問題呢?

     SerialVersionUID,也叫作流識別符號(Stream Unique Identifier),即類的版本定義,它可以顯示宣告,也可以隱式宣告。顯示宣告格式如下:

     private static final long serialVersionUID = XXXXXL;

     隱式宣告由編譯器自動通過包名、類名、繼承關係、非私有的方法和屬性,以及引數、返回值等組多因子計算得出的。(所以屬性改動了,版本就不一致了)。

     但如果顯示聲明瞭serialVersionUID,JVM在反序列化時會根據serialVersionUID判斷版本,如果相同,則認為類沒有發生改變,可以把資料流load為例項物件,如果不同,這會丟擲InvalidClassException異常。

     如果顯示聲明瞭標識,但是兩個類卻不同(例如增加了屬性),則在反序列化中不會報錯,這提高了程式碼的健壯性,但這種情況帶來的後果是反序列時無法反序列出現在的屬性,從而引起兩邊資料不一致。

     所以顯示宣告serialVersionUID可以避免物件不一致,但儘量不要以這種方式向JVM”撒謊“。

建議12:避免用序列化類在建構函式為不變數賦值

     即final修飾的變數。

     因為反序列化時建構函式不會執行,如果在在建構函式中為不變數賦值,反序列化時不會執行建構函式,因此建構函式對該變數做的操作就得不到,所以反序列化後該變數依然是老版本的值。

建議13:避免為final變數複雜賦值

     建議12中說的賦值中的值是指的簡單物件。簡單物件包括8個基本型別,以及陣列、字串(字串情況很複雜,不通過new關鍵字生成String物件的情況下,final變數的賦值與基本型別相同),但是不能方法賦值。

     其中原理是這樣的,序列化時儲存到磁碟上(或網路傳輸)的物件檔案包括兩部分:

     (1)類描述資訊

          包括包路徑、繼承關係、訪問許可權、變數描述、變數訪問許可權、方法簽名、返回值,以及變數的關聯類資訊。要注意的一點是,它並不是class檔案的翻版,它不記錄方法、建構函式、static變數等的具體實現。之所以類描述會被儲存,很簡單,是因為能去也能回來,這保證發序列化的健壯執行。

     (2)非瞬態(transient關鍵字)和非靜態(static關鍵字)的例項變數值

          當值為基本型別時,就被直接儲存下來,如果是複雜物件,則該物件和關聯類資訊一起儲存,並且持續遞迴下去(關聯類也必須實現Serializable介面,否則出現序列化異常),也就是說遞迴後還是基本資料類的儲存。

     正是因為這兩點,一個持久化後的物件檔案會比一個class檔案大很多

     總結一下,反序列化時final變數在一下情況下不會被重新賦值:

     》通過建構函式為final變數賦值。

     》通過方法返回值為final變數賦值。

     》final修飾的屬性不是基本型別。

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

     序列化過程中除了給不需要持久化的屬性上加瞬態關鍵字(transient關鍵字)之外,還有另一個方法。

     實現了Serializable介面的類可以實現兩個私有方法:writeObject和readObject,在方法的實現中只處理需要處理的部分屬性即可。

建議15:break萬萬不可忘

     在寫switch語句時,每個case後必須帶有break。

     為了防止這種情況,可以在IDE中設定警告級別:

     Performaces->Java->Compiler->Errors/Warnings->Potential Programming probems,然後修改“switch

”case fall-through為Errors級別。

建議16:易變業務使用指令碼語言編寫

     指令碼語言的特性有靈活、便捷、簡單。(如PHP、Ruby、Groovy、JavaScript等),而且是在執行期解釋執行。

     這正是Java所缺少的。

     於是Java6開始正是支援指令碼語言,但是指令碼語言較多。於是JCP(Java Community Process)提出了JSR規範,只要符合該規範的語言都可以在Java平臺上執行(它對JavaScript是預設支援的)。

     所以也可以自己寫個指令碼語言,然後再實現ScriptEngine,即可在Java平臺上執行。

建議17:慎用動態編譯

     從Java6開始支援動態編譯,可以在執行期直接編譯.java檔案,執行.class,並且能夠獲得相關的輸入輸出,甚至還能監聽相關的事件。

     Java的動態編譯對源提供了多個渠道。比如可以是字串,可以是文字,也可以是編譯過的位元組碼檔案,甚至可以是存放在資料庫中的明文程式碼或是位元組碼。總之,只要是符合Java規範的就都可以在執行期動態載入,其實現方式就是實現JavaFileObject介面,重寫getCharContent、openInputStream、openOutputStream,或者實現JDK已經提供的兩個SimpleJavaFileObject、ForwardingJavaFileObject。

     因為靜態編譯基本已經可以滿足我們絕大多是需求,所以動態編譯用的很少。即使真的需要,也有很好的代替方案,比兔Ruby、Groovy等無縫的指令碼語言。

     使用動態編譯時需要注意一下幾點:

     (1)在框架中謹慎使用

          比如在Struts中使用動態編譯,動態實現一個類,它若繼承自ActionSupport就希望它成為一個Action,能做到,但是debug很困難;在比如在Spring中,寫一個動態類,要讓它動態注入到Spring容器中,這是需要花費老大功夫的。

     (2)不用在要求高效能的專案中使用

          動態編譯必究需要一個編譯的過程,與靜態編譯相比多了一個執行環節,因此在高效能專案中不要使用動態編譯。不過,如果是工具類專案中它則可以很好地發揮其優越性,比如在Eclipse工具寫一個外掛,就可以很好的使用動態編譯,不用重啟即可實現執行、除錯功能,非常方便。

     (3)動態編譯要考慮安全問題

          如果你在web頁面上提供了一個功能,允許上傳一個Java檔案然後執行,那就等於說;“我的機器沒有密碼,大家都來看我的隱私吧”,這是非常典型的注入漏洞,只有上傳一個而已Java程式就可以讓你所有的安全工作毀於一旦。

     (4)記錄動態編譯過程

          建議記錄原始檔、目標檔案、編譯過程、執行過程等日誌,不僅僅是為了診斷,還是為了安全和審計,對Java專案來說,空中編譯和執行時很不讓人放心的,留下這些依據可以更好的優化程式。

建議18:避免instanceof非預期結果

     instanceof是一個簡單的二元操作符,它是用來判斷一個物件是否是一個類例項的。只有操作符兩邊的類有繼承或者實現關係就可以編譯通過。

 instanceof只能用於物件的判斷,不能用於基本型別的判斷。

     若有null則返回false。

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

     斷言在很多語言中都存在,在防禦式程式設計中經常會用斷言(Assertion)對引數和環境做出判斷,避免程式因不當的輸入或錯誤的環境而產生邏輯異常。斷言的基本語法:

     assert <布林表示式>

     assert <布林表示式> : <錯誤資訊>

     在布林表示式為假時,丟擲AssetionError錯誤,並附帶了錯誤資訊。assert的語法簡單,有一些兩個特性

     (1)assert預設不啟用(要啟用就需要在編譯、執行時附加上相關的關鍵字)

     (2)assert丟擲異常AssertionError是繼承自Error的

     斷言在兩種情況下不可使用:

     (1)在對外公開的方法中

     (2)在執行邏輯程式碼的情況下

     一般在以下情況下使用:

     (1)在私有方法中設定assert作為輸入引數的校驗

     (2)流程控制中不可能達到的區域

     (3)建立程式探針

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

    我們經常在系統中定義一個常量介面(或常量類),已囊括系統中所涉及的常量,從而簡化程式碼,方便開發,在很多的開源專案中已採取了類似方式。

     但在原始時代(非IDE編碼)情況下,若改動了常量類中的常量值,則另一個引用該值的類若不重新編譯,則還是記錄的常量類中原來的常量值(因為final修飾的j常量,編譯器會任務它是穩定態的,所以在編譯時直接把值編譯到位元組碼中,避免了在執行期的引用,所以若改變了常量類中的final常量值,除了重新編譯該常量類之外還要重新編譯引用類)。

     當然IDE編碼時會自動處理這種情況。

     釋出應用程式系統是禁止使用類檔案替換方式,整體war包釋出才是萬全之策。

歡迎關注公眾號:零點小時光

lingdianxiaoshiguang