程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 編寫高質量代碼:改善Java程序的151個建議(第1章:JAVA開發中通用的方法和准則___建議16~20),java151

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

編輯:JAVA綜合教程

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


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

  Java世界一直在遭受著異種語言的入侵,比如PHP,Ruby,Groovy、Javascript等,這些入侵者都有一個共同特征:全是同一類語言-----腳本語言,它們都是在運行期解釋執行的。為什麼Java這種強編譯型語言會需要這些腳本語言呢?那是因為腳本語言的三大特征,如下所示:

  腳本語言的這些特性是Java缺少的,引入腳本語言可以使Java更強大,於是Java6開始正式支持腳本語言。但是因為腳本語言比較多,Java的開發者也很難確定該支持哪種語言,於是JSCP(Java Community ProCess)很聰明的提出了JSR233規范,只要符合該規范的語言都可以在Java平台上運行(它對JavaScript是默認支持的)。

  簡單看看下面這個小例子:

function formual(var1, var2){
     return var1 + var2 * factor;
 }

這就是一個簡單的腳本語言函數,可能你會很疑惑:factor(因子)這個變量是從那兒來的?它是從上下文來的,類似於一個運行的環境變量。該js保存在C:/model.js中,下一步需要調用JavaScript公式,代碼如下:

 1 import java.io.FileNotFoundException;
 2 import java.io.FileReader;
 3 import java.util.Scanner;
 4 
 5 import javax.script.Bindings;
 6 import javax.script.Invocable;
 7 import javax.script.ScriptContext;
 8 import javax.script.ScriptEngine;
 9 import javax.script.ScriptEngineManager;
10 import javax.script.ScriptException;
11 
12 public class Client16 {
13     public static void main(String[] args) throws FileNotFoundException,
14             ScriptException, NoSuchMethodException {
15         // 獲得一個JavaScript執行引擎
16         ScriptEngine engine = new ScriptEngineManager().getEngineByName("javascript");
17         // 建立上下文變量
18         Bindings bind = engine.createBindings();
19         bind.put("factor", 1);
20         // 綁定上下文,作用於是當前引擎范圍
21         engine.setBindings(bind, ScriptContext.ENGINE_SCOPE);
22         Scanner input =new Scanner(System.in);
23         
24         while(input.hasNextInt()){
25             int first = input.nextInt();
26             int second = input.nextInt();
27             System.out.println("輸入參數是:"+first+","+second);
28             // 執行Js代碼
29             engine.eval(new FileReader("C:/model.js"));
30             // 是否可調用方法
31             if (engine instanceof Invocable) {
32                 Invocable in = (Invocable) engine;
33                 // 執行Js中的函數
34                 Double result = (Double) in.invokeFunction("formula", first, second);
35                 System.out.println("運算結果是:" + result.intValue());
36             }
37         }
38 
39     }
40 }

上段代碼使用Scanner類接受鍵盤輸入的兩個數字,然後調用JavaScript腳本的formula函數計算其結果,注意,除非輸入了一個非int數字,否則當前JVM會一直運行,這也是模擬生成系統的在線變更情況。運行結果如下:

 輸入參數是;1,2  運算結果是:3

此時,保持JVM的運行狀態,我們修改一下formula函數,代碼如下:

function formual(var1, var2){
     return var1 + var2 - factor;
 }

其中,乘號變成了減號,計算公式發生了重大改變。回到JVM中繼續輸入,運行結果如下:

輸入參數:1,2  運行結果是:2

     修改Js代碼,JVM沒有重啟,輸入參數也沒有任何改變,僅僅改變腳本函數即可產生不同的效果。這就是腳本語言對系統設計最有利的地方:可以隨時發布而不用部署;這也是我們javaer最喜愛它的地方----即使進行變更,也能提供不間斷的業務服務。

   Java6不僅僅提供了代碼級的腳本內置,還提供了jrunscript命令工具,它可以再批處理中發揮最大效能,而且不需要通過JVM解釋腳本語言,可以直接通過該工具運行腳本。想想看。這是多麼大的誘惑力呀!而且這個工具是可以跨操作系統的,腳本移植就更容易了。

建議17:慎用動態編譯

   動態編譯一直是java的夢想,從Java6開始支持動態編譯了,可以再運行期直接編譯.java文件,執行.class,並且獲得相關的輸入輸出,甚至還能監聽相關的事件。不過,我們最期望的還是定一段代碼,直接編譯,然後運行,也就是空中編譯執行(on-the-fly),看如下代碼:

 1 import java.io.IOException;
 2 import java.lang.reflect.Method;
 3 import java.net.URI;
 4 import java.util.ArrayList;
 5 import java.util.Arrays;
 6 import java.util.List;
 7 
 8 import javax.tools.JavaCompiler;
 9 import javax.tools.JavaFileObject;
10 import javax.tools.SimpleJavaFileObject;
11 import javax.tools.StandardJavaFileManager;
12 import javax.tools.ToolProvider;
13 
14 public class Client17 {
15     public static void main(String[] args) throws Exception {
16         // Java源代碼
17         String sourceStr = "public class Hello { public String sayHello (String name) {return \"Hello,\"+name+\"!\";}}";
18         // 類名及文件名
19         String clsName = "Hello";
20         // 方法名
21         String methodName = "sayHello";
22         // 當前編譯器
23         JavaCompiler cmp = ToolProvider.getSystemJavaCompiler();
24         // Java標准文件管理器
25         StandardJavaFileManager fm = cmp.getStandardFileManager(null, null,
26                 null);
27         // Java文件對象
28         JavaFileObject jfo = new StringJavaObject(clsName, sourceStr);
29         // 編譯參數,類似於javac <options>中的options
30         List<String> optionsList = new ArrayList<String>();
31         // 編譯文件的存放地方,注意:此處是為Eclipse工具特設的
32         optionsList.addAll(Arrays.asList("-d", "./bin"));
33         // 要編譯的單元
34         List<JavaFileObject> jfos = Arrays.asList(jfo);
35         // 設置編譯環境
36         JavaCompiler.CompilationTask task = cmp.getTask(null, fm, null,
37                 optionsList, null, jfos);
38         // 編譯成功
39         if (task.call()) {
40             // 生成對象
41             Object obj = Class.forName(clsName).newInstance();
42             Class<? extends Object> cls = obj.getClass();
43             // 調用sayHello方法
44             Method m = cls.getMethod(methodName, String.class);
45             String str = (String) m.invoke(obj, "Dynamic Compilation");
46             System.out.println(str);
47         }
48 
49     }
50 }
51 
52 class StringJavaObject extends SimpleJavaFileObject {
53     // 源代碼
54     private String content = "";
55 
56     // 遵循Java規范的類名及文件
57     public StringJavaObject(String _javaFileName, String _content) {
58         super(_createStringJavaObjectUri(_javaFileName), Kind.SOURCE);
59         content = _content;
60     }
61 
62     // 產生一個URL資源路徑
63     private static URI _createStringJavaObjectUri(String name) {
64         // 注意,此處沒有設置包名
65         return URI.create("String:///" + name + Kind.SOURCE.extension);
66     }
67 
68     // 文本文件代碼
69     @Override
70     public CharSequence getCharContent(boolean ignoreEncodingErrors)
71             throws IOException {
72         return content;
73     }
74 }

上面代碼較多,可以作為一個動態編譯的模板程序。只要是在本地靜態編譯能夠實現的任務,比如編譯參數,輸入輸出,錯誤監控等,動態編譯都能實現。

  Java的動態編譯對源提供了多個渠道。比如,可以是字符串,文本文件,字節碼文件,還有存放在數據庫中的明文代碼或者字節碼。匯總一句話,只要符合Java規范的就可以在運行期動態加載,其實現方式就是實現JavaFileObject接口,重寫getCharContent、openInputStream、openOutputStream,或者實現JDK已經提供的兩個SimpleJavaFileObject、ForwardingJavaFileObject,具體代碼可以參考上個例子。

  動態編譯雖然是很好的工具,讓我們可以更加自如的控制編譯過程,但是在我們目前所接觸的項目中還是使用較少。原因很簡單,靜態編譯已經能夠幫我們處理大部分的工作,甚至是全部的工作,即使真的需要動態編譯,也有很好的替代方案,比如Jruby、Groovy等無縫的腳本語言。另外,我們在使用動態編譯時,需要注意以下幾點:

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

 instanceof是一個簡單的二元操作符,它是用來判斷一個對象是否是一個類的實現,其操作類似於>=、==,非常簡單,我們看段程序,代碼如下:  

 1 import java.util.Date;
 2 
 3 public class Client18 {
 4     public static void main(String[] args) {
 5         // String對象是否是Object的實例 true
 6         boolean b1 = "String" instanceof Object;
 7         // String對象是否是String的實例 true
 8         boolean b2 = new String() instanceof String;
 9         // Object對象是否是String的實例 false
10         boolean b3 = new Object() instanceof String;
11         // 拆箱類型是否是裝箱類型的實例 編譯不通過
12         boolean b4 = 'A' instanceof Character;
13         // 空對象是否是String的實例 false
14         boolean b5 = null instanceof String;
15         // 轉換後的空對象是否是String的實例 false
16         boolean b6 = (String) null instanceof String;
17         // Date是否是String的實例 編譯不通過
18         boolean b7 = new Date() instanceof String;
19         // 在泛型類型中判斷String對象是否是Date的實例 false
20         boolean b8 = new GenericClass<String>().isDateInstance("");
21 
22     }
23 }
24 
25 class GenericClass<T> {
26     // 判斷是否是Date類型
27     public boolean isDateInstance(T t) {
28         return t instanceof Date;
29     }
30 
31 }

就這麼一段程序,instanceof的應用場景基本都出現了,同時問題也產生了:這段程序中哪些語句編譯不通過,我們一個一個的解釋說:

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

  在防御式編程中經常會用斷言(Assertion)對參數和環境做出判斷,避免程序因不當的判斷或輸入錯誤而產生邏輯異常,斷言在很多語言中都存在,C、C++、Python都有不同的斷言表現形式.在Java中斷言使用的是assert關鍵字,其基本用法如下:

  assert<布爾表達式>

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

在布爾表達式為假時,跑出AssertionError錯誤,並附帶了錯誤信息。assert的語法比較簡單,有以下兩個特性:

  (1)、assert默認是不啟用的

      我們知道斷言是為調試程序服務的,目的是為了能夠迅速、方便地檢查到程序異常,但Java在默認條件下是不啟用的,要啟用就要在編譯、運行時加上相關的關鍵字,這就不多說,有需要的話可以參考一下Java規范。

  (2)、assert跑出的異常AssertionError是繼承自Error的

      斷言失敗後,JVM會拋出一個AssertionError的錯誤,它繼承自Error,注意,這是一個錯誤,不可恢復,也就是表明這是一個嚴重問題,開發者必須予以關注並解決之。

  assert雖然是做斷言的,但不能將其等價於if...else...這樣的條件判斷,它在以下兩種情況下不可使用:

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

    我們知道防御式編程最核心的一點就是:所有的外部因素(輸入參數、環境變量、上下文)都是"邪惡"的,都存在著企圖摧毀程序的罪惡本源,為了抵制它,我們要在程序處處檢驗。滿地設卡,不滿足條件,就不執行後續程序,以保護後續程序的正確性,處處設卡沒問題,但就是不能用斷言做輸入校驗,特別是公開方法。我們開看一個例子: 

 1 public class Client19 {
 2     public static void main(String[] args) {
 3         System.out.println(StringUtils.encode(null));;
 4     }
 5 }
 6 
 7 class StringUtils{
 8     public static String encode(String str){
 9         assert    str != null : "加密的字符串為null";
10         /*加密處理*/
11         return str;
12         
13     }
14 }

  encode方法對輸入參數做了不為空的假設,如果為空,則拋出AssertionError錯誤,但這段程序存在一個嚴重的問題,encode是一個public方法,這標志著它時對外公開的,任何一個類只要能傳遞一個String類型的參數(遵守契約)就可以調用,但是Client19類按照規定和契約調用encode方法,卻獲得了一個AssertionError錯誤信息,是誰破壞了契約協議?---是encode方法自己。

  (2)、在執行邏輯代碼的情況下

    assert的支持是可選的,在開發時可以讓他運行,但在生產環境中系統則不需要其運行了(以便提高性能),因此在assert的布爾表達式中不能執行邏輯代碼,否則會因為環境的不同而產生不同的邏輯,例如: 

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

這段代碼在assert啟用的環境下沒有任何問題,但是一但投入到生成環境,就不會啟用斷言了,而這個方法就徹底完蛋了,list的刪除動作永遠不會執行,所以就永遠不會報錯或異常了,因為根本就沒有執行嘛!

  以上兩種情況下不能使用斷言assert,那在什麼情況下能夠使用assert呢?一句話:按照正常的執行邏輯不可能到達的代碼區域可以防止assert。具體分為三種情況:

public void doSomething() {
        int i = 7;
        while (i > 7) {
            /* 業務處理 */
        }
        assert false : "到達這裡就表示錯誤";
    }

3.建立程序探針:我們可能會在一段程序中定義兩個變量,分別代兩個不同的業務含義,但是兩者有固定的關系,例如:var1=var2 * 2,那我們就可以在程序中到處設"樁"了,斷言這兩者的關系,如果不滿足即表明程序已經出現了異常,業務也就沒有必要運行下去了。

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

   我們經常在系統中定義一個常量接口(或常量類),以囊括系統中所涉及的常量,從而簡化代碼,方便開發,在很多的開源項目中已經采用了類似的方法,比如在struts2中,org.apache.struts2.StrutsConstants就是一個常量類,它定義Struts框架中與配置有關的常量,而org.apache.struts2.StrutsConstants則是一個常量接口,其中定義了OGNL訪問的關鍵字。

  關於常量接口(類)我們開看一個例子,首先定義一個常量類:

public class Constant {
    //定義人類壽命極限
    public static final int MAX_AGE=150;
}

這是一個非常簡單的常量類,定義了人類的最大年齡,我們引用這個常量,代碼如下: 

public class Client{
    public static void main(String[] args) {
        System.out.println("人類的壽命極限是:"+Constant.MAX_AGE);
    }
}

  運行結果easy,故省略。目前的代碼是寫在"智能型"IDE工具中完成的,下面暫時回溯到原始時代,也就是回歸到用記事本編寫代碼的年代,然後看看會發生什麼事情(為什麼要如此,下面會給出答案)

  修改常量Constant類,人類的壽命極限增加了,最大活到180,代碼如下:

public class Constant {
    //定義人類壽命極限
    public static final int MAX_AGE=180;
}

  然後重新編譯,javac Constant,編譯完成後執行:java Client,大家猜猜輸出的年齡是多少?

  輸出的結果是:"人類的壽命極限是150",竟然沒有改成180,太奇怪了,這是為何?

  原因是:對於final修飾的基本類型和String類型,編譯器會認為它是穩定態的(Immutable Status)所以在編譯時就直接把值編譯到字節碼中了,避免了在運行期引用(Run-time Reference),以提高代碼的執行效率。對於我們的例子來說,Client類在編譯時字節碼中就寫上了"150",這個常量,而不是一個地址引用,因此無論你後續怎麼修改常量類,只要不重新編譯Client類,輸出還是照舊。

  對於final修飾的類(即非基本類型),編譯器會認為它不是穩定態的(Mutable Status),編譯時建立的則是引用關系(該類型也叫作Soft Final)。如果Client類引入的常量是一個類或實例,及時不重新編譯也會輸出最新值。

  千萬不可小看了這點知識,細坑也能絆倒大象,比如在一個web項目中,開發人員修改了一個final類型的值(基本類型)考慮到重新發布的風險較大,或者是審批流程過於繁瑣,反正是為了偷懶,於是直接采用替換class類文件的方式發布,替換完畢後應用服務器自動重啟,然後簡單測試一下,一切Ok,可運行幾天後發現業務數據對不上,有的類(引用關系的類)使用了舊值,有的類(繼承關系的類)使用的是新值,而且毫無頭緒,讓人一籌莫展,其實問題的根源就在於此。

  還有個小問題沒有說明,我們的例子為什麼不在IDE工具(比如Eclipse)中運行呢?那是因為在IDE中設置了自動編譯不能重現此問題,若修改了Constant類,IDE工具會自動編譯所有的引用類,"智能"化屏蔽了該問題,但潛在的風險其實仍然存在,我記得Eclipse應該有個設置自動編譯的入口,有興趣大家可以自己嘗試一下。

  注意:發布應用系統時禁止使用類文件替換方式,整體WAR包發布才是萬全之策。但我覺得應特殊情況特殊對待,並不可以偏概全,大家以為呢?

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved