建議1:不用在常量和變量中出現易混淆的字母
包括名全小寫,類名首字母全大寫,常量全部大寫並用下劃線分割,變量採用駝峯命名法(Camel Case)命名等。
例如:
package com.company;
/**
* 數字後跟小寫字母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:莫讓常量蛻變成變量
package com.company;
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:三元操作符的類型務必一致
package com.company;
/**
* 三元操作符兩個操作數的類型必須一致
*/
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)就是爲了更好地提高方法複用性,讓方法調用者可以“隨心所欲”地傳遞是參數量,當然變長參數也是要遵循一定規則的,比如變長參數必須是方法中的最後一個參數;一個方法不能定義多個變長參數等,這些規則要牢記,但是即使記住規則,往往還是會犯錯。
package com.company;
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值和空值威脅到變長方法
package com.company.section1;
/**
* 帶有變長參數的方法重載,在調用時失敗。
*
*/
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是沒有類型的,雖然兩個方法都符合調用請求,但不知道調用哪一個,於是報錯了。另外調用者最好不該隱藏實參類型,這樣的話不僅僅需要調用者猜測該調用哪個方法,而且被調用者也產生內部邏輯混亂。應該修改如下:
package com.company.section2;
/**
* 帶有變長參數的方法重載,在調用時失敗。
*
*/
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、重寫方法不能拋出新的異常,或者超出父類範圍的異常,但是可以拋出更少、更有限的異常,或者不拋出異常。
參數列表相同指:參數數量相同、類型相同、順序相同
package com.company;
/**
* 覆寫變長方法也循規蹈矩
*/
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:警惕自增的陷阱
package com.company;
/**
* 警惕自增的陷阱
*
*/
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:不要讓就語法困擾你
package com.company;
/**
* 不用讓舊語法困擾你
*
*/
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