閱讀目錄javascript
包名全小寫,類名首字母全大寫,常量所有大寫並用下劃線分隔,變量採用駝峯命名法(Camel Case)命名等,這些都是最基本的Java編碼規範,是每一個javaer都應熟知的規則,可是在變量的聲明中要注意不要引入容易混淆的字母。嘗試閱讀以下代碼,思考打印結果的i是多少:html
1 public class Demo{ 2 public static void main(String[] args) { 3 test01(); 4 } 5 6 public static void test01(){ 7 long i=1l; 8 System.out.println("i的兩倍是:"+(i+i)); 9 } 10 }
確定會有人說:這麼簡單的例子還能出錯?運行結果確定是22!實踐是檢驗真理的惟一標準,將其Run一下看看,或許你會很奇怪,結果是2,而不是22.難道是編譯器出問題了,少了個"2"?前端
由於賦給變量i的值就是數字"1",只是後面加了長整型變量的標示字母"l"而已。別說是我挖坑讓你跳,若是有相似程序出如今項目中,當你試圖經過閱讀代碼來理解做者的思想時,此情景就可能會出現。因此爲了讓你的程序更容易理解,字母"l"(包括大寫字母"O")儘可能不要和數字混用,以避免使讀者的理解和程序意圖產生誤差。若是字母和數字混合使用,字母"l"務必大寫,字母"O"則增長註釋。java
注意:字母"l"做爲長整型標誌時務必大寫。git
常量蛻變成變量?你胡扯吧,加了final和static的常量怎麼可能會變呢?不可能爲此賦值的呀。真的不可能嗎?看看以下代碼:程序員
1 import java.util.Random; 2 3 public class Demo01 { 4 public static void main(String[] args) { 5 test02(); 6 } 7 8 public static void test02() { 9 System.out.println("常量會變哦:" + Constant.RAND_CONST); 10 } 11 } 12 13 interface Constant { 14 public static final int RAND_CONST = new Random().nextInt(); 15 }
RAND_CONST是常量嗎?它的值會變嗎?絕對會變!這種常量的定義方式是絕對不可取的,常量就是常量,在編譯期就必須肯定其值,不該該在運行期更改,不然程序的可讀性會很是差,甚至連做者本身都不能肯定在運行期發生了何種神奇的事情。web
甭想着使用常量會變的這個功能來實現序列號算法、隨機種子生成,除非這真的是項目中的惟一方案,不然就放棄吧,常量仍是當常量使用。面試
注意:務必讓常量的值在運行期保持不變。算法
三元操做符是if-else的簡化寫法,在項目中使用它的地方不少,也很是好用,可是好用又簡單的東西並不表示就能夠隨意使用,看看以下代碼:數據庫
1 public static void test03() { 2 int i = 80; 3 String str = String.valueOf(i < 100 ? 90 : 100); 4 String str1 = String.valueOf(i < 100 ? 90 : 100.0); 5 System.out.println("二者是否相等:" + str.equals(str1)); 6 }
分析一下這段程序,i是80,小於100,二者的返回值確定都是90,再轉成String類型,其值也絕對相等,毋庸置疑的。嗯,分析的有點道理,可是變量str中的三元操做符的第二個操做數是100,而str1中的第二個操做數是100.0,難道木有影響嗎?不可能有影響吧,三元操做符的條件都爲真了,只返回第一個值嘛,於第二個值有毛線關係,貌似有道理。
運行以後,結果倒是:"二者是否相等:false",不相等,why?
問題就出在了100和100.0這兩個數字上,在變量str中,三元操做符的第一個操做數90和第二個操做數100都是int類型,類型相同,返回的結果也是int類型的90,而變量str1中的第一個操做數(90)是int類型,第二個操做數100.0是浮點數,也就是兩個操做數的類型不一致,可三元操做符必需要返回一個數據,並且類型要肯定,不可能條件爲真時返回int類型,條件爲假時返回float類型,編譯器是不容許如此的,因此它會進行類型轉換int類型轉換爲浮點數90.0,也就是三元操做符的返回值是浮點數90.0,那麼固然和整型的90不相等了。這裏爲何是整型轉成浮點型,而不是浮點型轉成整型呢?這就涉及三元操做符類型的轉換規則:
知道什麼緣由了,相應的解決辦法也就有了:保證三元操做符中的兩個操做數類型一致,避免此錯誤的發生。
在項目和系統開發中,爲了提升方法的靈活度和可複用性,咱們常常要傳遞不肯定數量的參數到方法中,在JAVA5以前經常使用的設計技巧就是把形參定義成Collection類型或其子類類型,或者數組類型,這種方法的缺點就是須要對空參數進行判斷和篩選,好比實參爲null值和長度爲0的Collection或數組。而Java5引入了變長參數(varags)就是爲了更好地挺好方法的複用性,讓方法的調用者能夠"爲所欲爲"地傳遞實參數量,固然變長參數也是要遵循必定規則的,好比變長參數必須是方法中的最後一個參數;一個方法不能定義多個變長參數等,這些基本規則須要牢記,可是即便記住了這些規則,仍然有可能出現錯誤,看以下代碼:
1 public class Client { 2 public static void main(String[] args) { 3 Client client = new Client(); 4 // 499元的貨物 打75折 5 client.calPrice(499, 75); 6 } 7 8 // 簡單折扣計算 9 public void calPrice(int price, int discount) { 10 float knockdownPrice = price * discount / 100.0F; 11 System.out.println("簡單折扣後的價格是:" + formatCurrency(knockdownPrice)); 12 } 13 14 // 複雜多折扣計算 15 public void calPrice(int price, int... discounts) { 16 float knockdownPrice = price; 17 for (int discount : discounts) { 18 knockdownPrice = knockdownPrice * discount / 100; 19 } 20 System.out.println("複雜折扣後的價格是:" + formatCurrency(knockdownPrice)); 21 } 22 23 public String formatCurrency(float price) { 24 return NumberFormat.getCurrencyInstance().format(price); 25 } 26 }
這是一個計算商品折扣的模擬類,帶有兩個參數的calPrice方法(該方法的業務邏輯是:提供商品的原價和折扣率,便可得到商品的折扣價)是一個簡單的折扣計算方法,該方法在實際項目中常常會用到,這是單一的打折方法。而帶有變長參數的calPrice方法是叫較複雜的折扣計算方式,多種折扣的疊加運算(模擬類是比較簡單的實現)在實際中也常常見到,好比在大甩賣期間對VIP會員再度進行打折;或者當天是你的生日,再給你打個9折,也就是俗話中的折上折。
業務邏輯清楚了,咱們來仔細看看這兩個方法,它們是重載嗎?固然是了,重載的定義是:"方法名相同,參數類型或數量不一樣",很明顯這兩個方法是重載。可是這個重載有點特殊,calPrice(int price ,int... discounts)的參數範疇覆蓋了calPrice(int price,int discount)的參數範疇。那問題就出來了:對於calPrice(499,75)這樣的計算,到底該調用哪一個方法來處理呢?
咱們知道java編譯器是很聰明的,它在編譯時會根據方法簽名來肯定調用那個方法,好比:calPrice(499,75,95)這個調用,很明顯75和95會被轉成一個包含兩個元素的數組,並傳遞到calPrice(int price,int...discounts)中,由於只有這一個方法符合這個實參類型,這很容易理解。可是咱們如今面對的是calPrice(499,75)調用,這個75既能夠被編譯成int類型的75,也能夠被編譯成int數組{75},即只包含一個元素的數組。那到底該調用哪個方法呢?運行結果是:"簡單折扣後的價格是:374.25"。看來調用了第一個方法,爲何會調用第一個方法,而不是第二個變長方法呢?由於java在編譯時,首先會根據實參的數量和類型(這裏2個實參,都爲int類型,注意沒有轉成int數組)來進行處理,也就是找到calPrice(int price,int discount)方法,並且確認他是否符合方法簽名條件。如今的問題是編譯器爲何會首先根據兩個int類型的實參而不是一個int類型,一個int數組類型的實參來查找方法呢?
由於int是一個原生數據類型,而數組自己是一個對象,編譯器想要"偷懶",因而它會從最簡單的開始"猜測",只要符合編譯條件的便可經過,因而就出現了此問題。
問題闡述清楚了,爲了讓咱們的程序能被"人類"看懂,仍是慎重考慮變長參數的方法重載吧,不然讓人傷腦筋不說,說不定哪天就陷入這類小陷阱裏了。
上一建議講解了變長參數的重載問題,本建議會繼續討論變長參數的重載問題,上一建議的例子是變長參數的範圍覆蓋了非變長參數的範圍,此次討論兩個都是變長參數的方法提及,代碼以下:
1 public class Client5 { 2 3 public void methodA(String str, Integer... is) { 4 5 } 6 7 public void methodA(String str, String... strs) { 8 9 } 10 11 public static void main(String[] args) { 12 Client5 client5 = new Client5(); 13 client5.methodA("china", 0); 14 client5.methodA("china", "people"); 15 client5.methodA("china"); 16 client5.methodA("china", null); 17 } 18 }
兩個methodA都進行了重載,如今的問題是:上面的client5.methodA("china");client5.methodA("china", null);編譯不經過,提示相同:方法模糊不清,編譯器不知道調用哪個方法,但這兩處代碼反應的味道是不一樣的。
對於methodA("china")方法,根據實參"china"(String類型),兩個方法都符合形參格式,編譯器不知道調用那個方法,因而報錯。咱們思考一下此問題:Client5這個類是一個複雜的商業邏輯,提供了兩個重載方法,從其它模塊調用(系統內本地調用系統或系統外遠程系統調用)時,調用者根據變長參數的規範調用,傳入變長參數的參數數量能夠是N個(N>=0),那固然能夠寫成client5.methodA("china")方法啊!徹底符合規範,可是這個卻讓編譯器和調用者鬱悶,程序符合規則卻不能運行,如此問題,誰之責任呢?是Client5類的設計者,他違反了KISS原則(Keep it Smile,Stupid,即懶人原則),按照此設計的方法應該很容一調用,但是如今遵循規範卻編譯不經過,這對設計者和開發者而言都是應該禁止出現的。
對於Client5.methodA("China",null),直接量null是沒喲類型的,雖然兩個methodA方法都符合調用要求,但不知道調用哪個,因而報錯了。仔細分析一下,除了不符合上面的懶人原則以外,還有一個很是很差的編碼習慣,即調用者隱藏了實參類型,這是很是危險的,不只僅調用者須要"猜想調用那個方法",並且被調用者也可能產生內部邏輯混亂的狀況。對於本例來講應該如此修改:
1 public static void main(String[] args) { 2 Client5 client5 = new Client5(); 3 String strs[] = null; 4 client5.methodA("china", strs); 5 }
也就是說讓編譯器知道這個null值是String類型的,編譯便可順利經過,也就減小了錯誤的發生。
在JAVA中,子類覆寫父類的中的方法很常見,這樣作既能夠修正bug,也能夠提供擴展的業務功能支持,同時還符合開閉原則(Open-Closed Principle)。
符合開閉原則(Open-Closed Principle)的主要特徵:
1.對於擴展是開放的(Open for extension)。這意味着模塊的行爲是能夠擴展的。當應用的需求改變時,咱們能夠對模塊進行擴展,使其具備知足那些改變的新行爲。也就是說,咱們能夠改變模塊的功能。
2.對於修改是關閉的(Closed for modification)。對模塊行爲進行擴展時,沒必要改動模塊的源代碼或者二進制代碼。模塊的二進制可執行版本,不管是可連接的庫、DLL或者.EXE文件,都無需改動。
下面咱們看一下覆寫必須知足的條件:
看下面這段代碼:
1 public class Client6 { 2 public static void main(String[] args) { 3 // 向上轉型 4 Base base = new Sub(); 5 base.fun(100, 50); 6 // 不轉型 7 Sub sub = new Sub(); 8 sub.fun(100, 50); 9 } 10 } 11 12 // 基類 13 class Base { 14 void fun(int price, int... discounts) { 15 System.out.println("Base......fun"); 16 } 17 } 18 19 // 子類,覆寫父類方法 20 class Sub extends Base { 21 @Override 22 void fun(int price, int[] discounts) { 23 System.out.println("Sub......fun"); 24 } 25 }
該程序中sub.fun(100, 50)報錯,提示找不到fun(int,int)方法。這太奇怪了:子類繼承了父類的全部屬性和方法,甭管是私有的仍是公開的訪問權限,一樣的參數,一樣的方法名,經過父類調用沒有任何問題,經過子類調用,卻編譯不過,爲啥?難到是沒繼承下來?或者子類縮小了父類方法的前置條件?若是是這樣,就不該該覆寫,@Override就應該報錯呀。
事實上,base對象是把子類對象作了向上轉型,形參列表由父類決定,因爲是變長參數,在編譯時,base.fun(100, 50);中的50這個實參會被編譯器"猜想"而編譯成"{50}"數組,再由子類Sub執行。咱們再來看看直接調用子類的狀況,這時編譯器並不會把"50"座類型轉換由於數組自己也是一個對象,編譯器尚未聰明到要在兩個沒有繼承關係的類之間轉換,要知道JAVA是要求嚴格的類型匹配的,類型不匹配編譯器天然就會拒絕執行,並給予錯誤提示。
這是個特例,覆寫的方法參數列表居然與父類不相同,這違背了覆寫的定義,而且會引起莫名其妙的錯誤。因此讀者在對變長參數進行覆寫時,若是要使用次相似的方法,請仔細想一想是否是要必定如此。
注意:覆寫的方法參數與父類相同,不只僅是類型、數量,還包括顯示形式.
記得大學剛開始學C語言時,老師就說:自增有兩種形式,分別是i++和++i,i++表示的先賦值後加1,++i是先加1後賦值,這樣理解了不少年也木有問題,直到遇到以下代碼,我才懷疑個人理解是否是錯了:
1 public class Client7 { 2 public static void main(String[] args) { 3 int count=0; 4 for(int i=0; i<10;i++){ 5 count=count++; 6 } 7 System.out.println("count = "+count); 8 } 9 }
這個程序輸出的count等於幾?是count自加10次嗎?答案等於10?能夠確定的說,這個運行結果是count=0。爲何呢?
count++是一個表達式,是由返回值的,它的返回值就是count自加前的值,Java對自加是這樣處理的:首先把count的值(注意是值,不是引用)拷貝到一個臨時變量區,而後對count變量+1,最後返回臨時變量區的值。程序第一次循環處理步驟以下:
"count=count++"這條語句能夠按照以下代碼理解:
1 public static int mockAdd(int count) { 2 // 先保存初始值 3 int temp = count; 4 // 作自增操做 5 count = count + 1; 6 // 返回原始值 7 return temp; 8 }
因而第一次循環後count的值爲0,其它9次循環也是同樣的,最終你會發現count的值始終沒有改變,仍然保持着最初的狀態.
此例中代碼做者的本意是但願count自增,因此想固然的賦值給自身就能夠了,未曾想到調到Java自增的陷阱中了,解決辦法很簡單,把"count=count++"改成"count++"便可。該問題在不一樣的語言環境中有着不一樣的實現:C++中"count=count++"與"count++"是等效的,而在PHP中保持着與JAVA相同的處理方式。每種語言對自增的實現方式各不相同。
1 public class Client8 { 2 public static void main(String[] args) { 3 // 數據定義初始化 4 int fee = 200; 5 // 其它業務處理 6 saveDefault: save(fee); 7 } 8 9 static void saveDefault() { 10 System.out.println("saveDefault...."); 11 } 12 13 static void save(int fee) { 14 System.out.println("save...."); 15 } 16 }
這段代碼分析一下,輸出結果,以及語法含義:
從Java5開始引入了靜態導入語法(import static),其目的是爲了減小字符的輸入量,提升代碼的可閱讀性,以便更好地理解程序。咱們先倆看一個不用靜態導入的例子,也就是通常導入:
1 public class Client9 { 2 // 計算圓面積 3 public static double claCircleArea(double r) { 4 return Math.PI * r * r; 5 } 6 7 // 計算球面積 8 public static double claBallArea(double r) { 9 return 4 * Math.PI * r * r; 10 } 11 }
這是很簡單的兩個方法,咱們再這兩個計算面積的方法中都引入了java.lang.Math類(該類是默認導入的)中的PI(圓周率)常量,而Math這個類寫在這裏有點多餘,特別是若是Client9類中的方法比較多時。若是每次輸入都須要敲入Math這個類,繁瑣且多餘,靜態導入能夠解決此問題,使用靜態導入後的程序以下:
1 import static java.lang.Math.PI; 2 3 public class Client9 { 4 // 計算圓面積 5 public static double claCircleArea(double r) { 6 return PI * r * r; 7 } 8 9 // 計算球面積 10 public static double claBallArea(double r) { 11 return 4 * PI * r * r; 12 } 13 }
靜態導入的做用是把Math類中的Pi常量引入到本類中,這會是程序更簡單,更容易閱讀,只要看到PI就知道這是圓周率,不用每次都把類名寫全了。可是,濫用靜態導入會使程序更難閱讀,更難維護,靜態導入後,代碼中就不須要再寫類名了,但咱們知道類是"一類事物的描述",缺乏了類名的修飾,靜態屬性和靜態方法的表象意義能夠被無限放大,這會讓閱讀者很難弄清楚其屬性或者方法表明何意,繩子哪一類的屬性(方法)都要思考一番(固然IDE的友好提示功能另說),把一個類的靜態導入元素都引入進來了,那簡直就是噩夢。咱們來看下面的例子:
1 import static java.lang.Math.*; 2 import static java.lang.Double.*; 3 import static java.lang.Integer.*; 4 import static java.text.NumberFormat.*; 5 6 import java.text.NumberFormat; 7 8 public class Client9 { 9 10 public static void formatMessage(String s) { 11 System.out.println("圓面積是: " + s); 12 } 13 14 public static void main(String[] args) { 15 double s = PI * parseDouble(args[0]); 16 NumberFormat nf = getInstance(); 17 nf.setMaximumFractionDigits(parseInt(args[1])); 18 formatMessage(nf.format(s)); 19 20 } 21 }
就這麼一段程序,看着就讓人惱火,常量PI,這知道是圓周率;parseDouble方法多是Double類的一個轉換方法,這看名稱能夠猜的到。那緊接着getInstance()方法是哪一個類的?是Client9本地類?不對呀,本地沒有這個方法,哦,原來是NumberFormat類的方法,這個和formatMessage本地方法沒有任何區別了---這代碼太難閱讀了,確定有人罵娘。
因此,對於靜態導入,必定要追尋兩個原則:
何爲具備明確、清晰表象意義的工具類,咱們看看Junit中使用靜態導入的例子:
1 import static org.junit.Assert.*; 2 class DaoTest{ 3 @Test 4 public void testInsert(){ 5 //斷言 6 assertEquals("foo","foo"); 7 assertFalse(Boolean.FALSE); 8 } 9 }
咱們從程序中很容易判斷出assertEquals方法是用來斷言兩個值是否相等的,assertFalse方法則是斷言表達式爲假,如此確實減小了代碼量,並且代碼的可讀性也提升了,這也是靜態導入用到正確的地方帶來的好處。
若是在一個類中的方法及屬性與靜態導入的方法及屬性相同會出現什麼問題呢?看下面的代碼
1 import static java.lang.Math.PI; 2 import static java.lang.Math.abs; 3 4 public class Client10 { 5 // 常量名於靜態導入的PI相同 6 public final static String PI = "祖沖之"; 7 //方法名於靜態導入的方法相同 8 public static int abs(int abs) { 9 return 0; 10 } 11 12 public static void main(String[] args) { 13 System.out.println("PI = "+PI); 14 System.out.println("abs(-100) = "+abs(-100)); 15 } 16 }
以上代碼中定義了一個String類型的常量PI,又定義了一個abs方法,與靜態導入的相同。首先說好消息,代碼沒有報錯,接下來是壞消息:咱們不知道那個屬性和方法別調用了,由於常量名和方法名相同,到底調用了那一個方法呢?運行以後結果爲:
PI = "祖沖之",abs(-100) = 0;
很明顯是本地的方法被調用了,爲什麼不調用Math類中的屬性和方法呢?那是由於編譯器有一個"最短路徑"原則:若是可以在本類中查找到相關的變量、常量、方法、就不會去其它包或父類、接口中查找,以確保本類中的屬性、方法優先。
所以,若是要變動一個被靜態導入的方法,最好的辦法是在原始類中重構,而不是在本類中覆蓋.
咱們編寫一個實現了Serializable接口(序列化標誌接口)的類,Eclipse立刻就會給一個黃色警告:須要添加一個Serial Version ID。爲何要增長?他是怎麼計算出來的?有什麼用?下面就來解釋該問題。
類實現Serializable接口的目的是爲了可持久化,好比網絡傳輸或本地存儲,爲系統的分佈和異構部署提供先決條件支持。若沒有序列化,如今咱們熟悉的遠程調用、對象數據庫都不可能存在,咱們來看一個簡單的序列化類:
1 import java.io.Serializable; 2 public class Person implements Serializable { 3 private String name; 4 5 public String getName() { 6 return name; 7 } 8 9 public void setName(String name) { 10 this.name = name; 11 } 12 13 }
這是一個簡單的JavaBean,實現了Serializable接口,能夠在網絡上傳輸,也能夠在本地存儲而後讀取。這裏咱們以java消息服務(Java Message Service)方式傳遞對象(即經過網絡傳遞一個對象),定義在消息隊列中的數據類型爲ObjectMessage,首先定義一個消息的生產者(Producer),代碼以下:
1 public class Producer { 2 public static void main(String[] args) { 3 Person p = new Person(); 4 p.setName("混世魔王"); 5 // 序列化,保存到磁盤上 6 SerializationUtils.writeObject(p); 7 } 8 }
這裏引入了一個工具類SerializationUtils,其做用是對一個類進行序列化和反序列化,並存儲到硬盤上(模擬網絡傳輸),其代碼以下:
1 import java.io.FileInputStream; 2 import java.io.FileNotFoundException; 3 import java.io.FileOutputStream; 4 import java.io.IOException; 5 import java.io.ObjectInputStream; 6 import java.io.ObjectOutputStream; 7 import java.io.Serializable; 8 9 public class SerializationUtils { 10 private static String FILE_NAME = "c:/obj.bin"; 11 //序列化 12 public static void writeObject(Serializable s) { 13 try { 14 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_NAME)); 15 oos.writeObject(s); 16 oos.close(); 17 } catch (FileNotFoundException e) { 18 e.printStackTrace(); 19 } catch (IOException e) { 20 e.printStackTrace(); 21 } 22 } 23 //反序列化 24 public static Object readObject() { 25 Object obj = null; 26 try { 27 ObjectInputStream input = new ObjectInputStream(new FileInputStream(FILE_NAME)); 28 obj=input.readObject(); 29 input.close(); 30 } catch (FileNotFoundException e) { 31 e.printStackTrace(); 32 } catch (IOException e) { 33 e.printStackTrace(); 34 } catch (ClassNotFoundException e) { 35 e.printStackTrace(); 36 } 37 return obj; 38 } 39 }
經過對象序列化過程,把一個內存塊轉化爲可傳輸的數據流,而後經過網絡發送到消息消費者(Customer)哪裏,進行反序列化,生成實驗對象,代碼以下:
1 public class Customer { 2 public static void main(String[] args) { 3 //反序列化 4 Person p=(Person) SerializationUtils.readObject(); 5 System.out.println(p.getName()); 6 } 7 }
這是一個反序列化的過程,也就是對象數據流轉換爲一個實例的過程,其運行後的輸出結果爲「混世魔王」。這太easy了,是的,這就是序列化和反序列化的典型Demo。但此處藏着一個問題:若是消息的生產者和消息的消費者(Person類)有差別,會出現何種神奇事件呢?好比:消息生產者中的Person類添加一個年齡屬性,而消費者沒有增長該屬性。爲啥沒有增長?由於這個是分佈式部署的應用,你甚至不知道這個應用部署在何處,特別是經過廣播方式發消息的狀況,漏掉一兩個訂閱者也是很正常的。
這中序列化和反序列化的類在不一致的狀況下,反序列化時會報一個InalidClassException異常,緣由是序列化和反序列化所對應的類版本發生了變化,JVM不能把數據流轉換爲實例對象。刨根問底:JVM是根據什麼來判斷一個類的版本呢?
好問題,經過SerializableUID,也叫作流標識符(Stream Unique Identifier),即類的版本定義的,它能夠顯示聲明也能夠隱式聲明。顯示聲明格式以下:
private static final long serialVersionUID = 1867341609628930239L;
而隱式聲明則是我不聲明,你編譯器在編譯的時候幫我生成。生成的依據是經過包名、類名、繼承關係、非私有的方法和屬性,以及參數、返回值等諸多因子算出來的,極度複雜,基本上計算出來的這個值是惟一的。
serialVersionUID如何生成已經說明了,咱們再來看看serialVersionUID的做用。JVM在反序列化時,會比較數據流中的serialVersionUID與類的serialVersionUID是否相同,若是相同,則認爲類沒有改變,能夠把數據load爲實例相同;若是不相同,對不起,我JVM不幹了,拋個異常InviladClassException給你瞧瞧。這是一個很是好的校驗機制,能夠保證一個對象即便在網絡或磁盤中「滾過」一次,仍能作到「出淤泥而不染」,完美的實現了類的一致性。
可是,有時候咱們須要一點特例場景,例如個人類改變不大,JVM是否能夠把我之前的對象反序列化回來?就是依據顯示聲明的serialVersionUID,向JVM撒謊說"個人類版本沒有變化",如此我買你編寫的類就實現了向上兼容,咱們修改Person類,裏面添加private static final long serialVersionUID = 1867341609628930239L;
剛開始生產者和消費者持有的Person類一致,都是V1.0,某天生產者的Person類變動了,增長了一個「年齡」屬性,升級爲V2.0,因爲種種緣由(好比程序員疏忽,升級時間窗口不一樣等)消費端的Person類仍是V1.0版本,添加的代碼爲 priavte int age;以及對應的setter和getter方法。
此時雖然生產這和消費者對應的類版本不一樣,可是顯示聲明的serialVersionUID相同,序列化也是能夠運行的,所帶來的業務問題就是消費端不能讀取到新增的業務屬性(age屬性而已)。經過此例,咱們反序列化也實現了版本向上兼容的功能,使用V1.0版本的應用訪問了一個V2.0的對象,這無疑提升了代碼的健壯性。咱們在編寫序列化類代碼時隨手添加一個serialVersionUID字段,也不會帶來太多的工做量,但它卻能夠在關鍵時候發揮異乎尋常的做用。
顯示聲明serialVersionUID能夠避免對象的不一致,但儘可能不要以這種方式向JVM撒謊。
咱們知道帶有final標識的屬性是不變量,也就是隻能賦值一次,不能重複賦值,可是在序列化類中就有點複雜了,好比這個類:
1 public class Person implements Serializable { 2 private static final long serialVersionUID = 1867341609628930239L; 3 public final String perName="程咬金"; 4 }
這個Peson類(此時V1.0版本)被序列化,而後存儲在磁盤上,在反序列化時perName屬性會從新計算其值(這與static變量不一樣,static變量壓根就沒有保存到數據流中)好比perName屬性修改爲了"秦叔寶"(版本升級爲V2.0),那麼反序列化的perName值就是"秦叔寶"。保持新舊對象的final變量相同,有利於代碼業務邏輯統一,這是序列化的基本原則之一,也就是說,若是final屬性是一個直接量,在反序列化時就會從新計算。對於基本原則很少說,如今說一下final變量的另外一種賦值方式:經過構造函數賦值。代碼以下:
public class Person implements Serializable { private static final long serialVersionUID = 1867341609628930239L; public final String perName; public Person() { perName = "程咬金"; } }
這也是咱們經常使用的一種賦值方式,能夠把Person類定義爲版本V1.0,而後進行序列化,看看序列化後有什麼問題,序列化代碼以下:
public class Serialize { public static void main(String[] args) { //序列化以持久保持 SerializationUtils.writeObject(new Person()); } }
Person的實習對象保存到了磁盤上,它時一個貧血對象(承載業務屬性定義,但不包含其行爲定義),咱們作一個簡單的模擬,修改一下PerName值表明變動,要注意的是serialVersionUID不變,修改後的代碼以下:
public class Person implements Serializable { private static final long serialVersionUID = 1867341609628930239L; public final String perName; public Person() { perName = "秦叔寶"; } }
此時Person類的版本時V2.0但serialVersionUID沒有改變,仍然能夠反序列化,代碼以下:
public class Deserialize { public static void main(String[] args) { Person p = (Person) SerializationUtils.readObject(); System.out.println(p.perName); } }
如今問題出來了,打印出來的結果是"程咬金" 仍是"秦叔寶"?答案是:"程咬金"。final類型的變量不是會從新計算嘛,打印出來的應該是秦叔寶纔對呀。爲何會是程咬金?這是由於這裏觸及到了反序列化的兩一個原則:反序列化時構造函數不會執行.
反序列化的執行過程是這樣的:JVM從數據流中獲取一個Object對象,而後根據數據流中的類文件描述信息(在序列化時,保存到磁盤的對象文件中包含了類描述信息,注意是描述信息,不是類)查看,發現是final變量,須要從新計算,因而引用Person類中的perName值,而此時JVM又發現perName竟沒有賦值,不能引用,因而它很聰明的再也不初始化,保持原值狀態,因此結果就是"程咬金"了。
注意:在序列化類中不使用構造函數爲final變量賦值.
爲final變量賦值還有另一種方式:經過方法賦值,及直接在聲明時經過方法的返回值賦值,仍是以Person類爲例來講明,代碼以下:
public class Person implements Serializable { private static final long serialVersionUID = 1867341609628930239L; //經過方法返回值爲final變量賦值 public final String pName = initName(); public String initName() { return "程咬金"; } }
pName屬性是經過initName方法的返回值賦值的,這在複雜的類中常常用到,這比使用構造函數賦值更簡潔,易修改,那麼如此用法在序列化時會不會有問題呢?咱們一塊兒看看。Person類寫好了(定義爲V1.0版本),先把它序列化,存儲到本地文件,其代碼與以前相同,不在贅述。如今Person類的代碼須要修改,initName的返回值改成"秦叔寶".那麼咱們以前存儲在磁盤上的的實例加載上來,pName的會是什麼呢?
如今,Person類的代碼須要修改,initName的返回值也改變了,代碼以下:
public class Person implements Serializable { private static final long serialVersionUID = 1867341609628930239L; //經過方法返回值爲final變量賦值 public final String pName = initName(); public String initName() { return "秦叔寶"; } }
上段代碼僅僅修改了initName的返回值(Person類爲V2.0版本)也就是經過new生成的對象的final變量的值都是"秦叔寶",那麼咱們把以前存儲在磁盤上的實例加載上來,pName的值會是什麼呢?
結果是"程咬金",很詫異,上一建議說過final變量會被從新賦值,可是這個例子又沒有從新賦值,爲何?
上個建議說的從新賦值,其中的"值"指的是簡單對象。簡單對象包括:8個基本類型,以及數組、字符串(字符串狀況複雜,不經過new關鍵字生成的String對象的狀況下,final變量的賦值與基本類型相同),可是不能方法賦值。
其中的原理是這樣的,保存到磁盤上(或網絡傳輸)的對象文件包括兩部分:
(1).類描述信息:包括類路徑、繼承關係、訪問權限、變量描述、變量訪問權限、方法簽名、返回值、以及變量的關聯類信息。要注意一點是,它並非class文件的翻版,它不記錄方法、構造函數、static變量等的具體實現。之因此類描述會被保存,很簡單,是由於能去也能回嘛,這保證反序列化的健壯運行。
(2).非瞬態(transient關鍵字)和非靜態(static關鍵字)的實體變量值
注意,這裏的值若是是一個基本類型,好說,就是一個簡單值保存下來;若是是複雜對象,也簡單,連該對象和關聯類信息一塊兒保存,而且持續遞歸下去(關聯類也必須實現Serializable接口,不然會出現序列化異常),也就是遞歸到最後,仍是基本數據類型的保存。
正是由於這兩個緣由,一個持久化的對象文件會比一個class類文件大不少,有興趣的讀者能夠本身測試一下,體積確實膨脹了很多。
總結一下:反序列化時final變量在如下狀況下不會被從新賦值:
部分屬性持久化問題看似很簡單,只要把不須要持久化的屬性加上瞬態關鍵字(transient關鍵字)便可。這是一種解決方案,但有時候行不通。例如一個計稅系統和一個HR系統,經過RMI(Remote Method Invocation,遠程方法調用)對接,計稅系統須要從HR系統得到人員的姓名和基本工資,以做爲納稅的依據,而HR系統的工資分爲兩部分:基本工資和績效工資,基本工資沒什麼祕密,績效工資是保密的,不能泄露到外系統,這明顯是連個相互關聯的類,先看看薪水類Salary的代碼:
1 public class Salary implements Serializable { 2 private static final long serialVersionUID = 2706085398747859680L; 3 // 基本工資 4 private int basePay; 5 // 績效工資 6 private int bonus; 7 8 public Salary(int _basepay, int _bonus) { 9 this.basePay = _basepay; 10 this.bonus = _bonus; 11 } 12 //Setter和Getter方法略 13 14 }
Person類和Salary類是關聯關係,代碼以下:
1 public class Person implements Serializable { 2 3 private static final long serialVersionUID = 9146176880143026279L; 4 5 private String name; 6 7 private Salary salary; 8 9 public Person(String _name, Salary _salary) { 10 this.name = _name; 11 this.salary = _salary; 12 } 13 14 //Setter和Getter方法略 15 16 }
這是兩個簡單的JavaBean,都實現了Serializable接口,具有了序列化的條件。首先計稅系統請求HR系統對一個Person對象進行序列化,把人員信息和工資信息傳遞到計稅系統中,代碼以下:
1 public class Serialize { 2 public static void main(String[] args) { 3 // 基本工資1000元,績效工資2500元 4 Salary salary = new Salary(1000, 2500); 5 // 記錄人員信息 6 Person person = new Person("張三", salary); 7 // HR系統持久化,並傳遞到計稅系統 8 SerializationUtils.writeObject(person); 9 } 10 }
在經過網絡傳輸到計稅系統後,進行反序列化,代碼以下:
1 public class Deserialize { 2 public static void main(String[] args) { 3 Person p = (Person) SerializationUtils.readObject(); 4 StringBuffer buf = new StringBuffer(); 5 buf.append("姓名: "+p.getName()); 6 buf.append("\t基本工資: "+p.getSalary().getBasePay()); 7 buf.append("\t績效工資: "+p.getSalary().getBonus()); 8 System.out.println(buf); 9 } 10 }
打印出的結果爲:姓名: 張三 基本工資: 1000 績效工資: 2500
可是這不符合需求,由於計稅系統只能從HR系統中獲取人員姓名和基本工資,而績效工資是不能得到的,這是個保密數據,不容許發生泄漏。怎麼解決這個問題呢?你可能會想到如下四種方案:
下面展現一個優秀的方案,其中實現了Serializable接口的類能夠實現兩個私有方法:writeObject和readObject,以影響和控制序列化和反序列化的過程。咱們把Person類稍做修改,看看如何控制序列化和反序列化,代碼以下:
1 public class Person implements Serializable { 2 3 private static final long serialVersionUID = 9146176880143026279L; 4 5 private String name; 6 7 private transient Salary salary; 8 9 public Person(String _name, Salary _salary) { 10 this.name = _name; 11 this.salary = _salary; 12 } 13 //序列化委託方法 14 private void writeObject(ObjectOutputStream oos) throws IOException { 15 oos.defaultWriteObject(); 16 oos.writeInt(salary.getBasePay()); 17 } 18 //反序列化委託方法 19 private void readObject(ObjectInputStream input)throws ClassNotFoundException, IOException { 20 input.defaultReadObject(); 21 salary = new Salary(input.readInt(), 0); 22 } 23 }
其它代碼不作任何改動,運行以後結果爲:姓名: 張三 基本工資: 1000 績效工資: 0
在Person類中增長了writeObject和readObject兩個方法,而且訪問權限都是私有級別,爲何會改變程序的運行結果呢?其實這裏用了序列化的獨有機制:序列化回調。Java調用ObjectOutputStream類把一個對象轉換成數據流時,會經過反射(Refection)檢查被序列化的類是否有writeObject方法,而且檢查其是否符合私有,無返回值的特性,如有,則會委託該方法進行對象序列化,若沒有,則由ObjectOutputStream按照默認規則繼續序列化。一樣,在從流數據恢復成實例對象時,也會檢查是否有一個私有的readObject方法,若是有,則會經過該方法讀取屬性值,此處有幾個關鍵點須要說明:
分別是寫入和讀出相應的值,相似一個隊列,先進先出,若是此處有複雜的數據邏輯,建議按封裝Collection對象處理。你們可能注意到上面的方式也是Person失去了分佈式部署的能了,確實是,可是HR系統的難點和重點是薪水的計算,特別是績效工資,它所依賴的參數很複雜(僅從數量上說就有上百甚至上千種),計算公式也不簡單(通常是引入腳本語言,個性化公式定製)而相對來講Person類基本上都是靜態屬性,計算的可能性不大,因此即便爲性能考慮,Person類爲分佈式部署的意義也不大。
咱們常常會寫一些轉換類,好比貨幣轉換,日期轉換,編碼轉換等,在金融領域裏用到的最多的要數中文數字轉換了,好比把"1"轉換爲"壹" ,不過開源工具是不會提供此工具類的,由於它太貼近中國文化了,須要本身編寫:
1 public class Client15 { 2 public static void main(String[] args) { 3 System.out.println(toChineseNuberCase(0)); 4 } 5 6 public static String toChineseNuberCase(int n) { 7 String chineseNumber = ""; 8 switch (n) { 9 case 0: 10 chineseNumber = "零"; 11 case 1: 12 chineseNumber = "壹"; 13 case 2: 14 chineseNumber = "貳"; 15 case 3: 16 chineseNumber = "叄"; 17 case 4: 18 chineseNumber = "肆"; 19 case 5: 20 chineseNumber = "伍"; 21 case 6: 22 chineseNumber = "陸"; 23 case 7: 24 chineseNumber = "柒"; 25 case 8: 26 chineseNumber = "捌"; 27 case 9: 28 chineseNumber = "玖"; 29 } 30 return chineseNumber; 31 } 32 }
這是一個簡單的代碼,但運行結果倒是"玖",這個很簡單,可能你們在剛接觸語法時都學過,但雖簡單,若是程序員漏寫了,簡單的問題會形成很大的後果,甚至經濟上的損失。因此在用switch語句上記得加上break,養成良好的習慣。對於此類問題,除了日常當心以外,能夠使用單元測試來避免,但你們都曉得,項目緊的時候,可能但單元測試都覆蓋不了。因此對於此類問題,一個最簡單的辦法就是:修改IDE的警告級別,例如在Eclipse中,能夠依次點擊PerFormaces-->Java-->Compiler-->Errors/Warings-->Potential Programming problems,而後修改'switch' case fall-through爲Errors級別,若是你膽敢不在case語句中加入break,那Eclipse直接就報個紅叉給你看,這樣能夠避免該問題的發生了。但仍是囉嗦一句,養成良好習慣更重要!
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解釋腳本語言,能夠直接經過該工具運行腳本。想一想看。這是多麼大的誘惑力呀!並且這個工具是能夠跨操做系統的,腳本移植就更容易了。
動態編譯一直是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等無縫的腳本語言。另外,咱們在使用動態編譯時,須要注意如下幾點:
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的應用場景基本都出現了,同時問題也產生了:這段程序中哪些語句編譯不經過,咱們一個一個的解釋說:
"String" instanceof Object:返回值是true,這很正常,"String"是一個字符串,字符串又繼承了Object,那固然返回true了。
new String() instanceof String:返回值是true,沒有任何問題,一個類的對象固然是它的實例了。
new Object() instanceof String:返回值爲false,Object是父類,其對象固然不是String類的實例了。要注意的是,這句話其實徹底能夠編譯經過,只要instanceof關鍵字的左右兩個操做數有繼承或實現關係,就能夠編譯經過。
'A' instanceof Character:這句話編譯不經過,爲何呢?由於'A'是一個char類型,也就是一個基本類型,不是一個對象,instanceof只能用於對象的判斷,不能用於基本類型的判斷。
(String) null instanceof String:返回值爲false,不要看這裏有個強制類型轉換就認爲結果是true,不是的,null是一個萬用類型,也就是說它能夠沒類型,即便作類型轉換仍是個null。
new Date() instanceof String:編譯不經過,由於Date類和String沒有繼承或實現關係,因此在編譯時就直接報錯了,instanceof操做符的左右操做數必須有繼承或實現關係,不然編譯會失敗。
new GenericClass<String>().isDateInstance(""):編譯不經過,非也,編譯經過了,返回值爲false,T是個String類型,於Date之間沒有繼承或實現關係,爲何"t instanceof Date"會編譯經過呢?那是由於Java的泛型是爲編碼服務的,在編譯成字節碼時,T已是Object類型了傳遞的實參是String類型,也就是說T的表面類型是Object,實際類型是String,那麼"t instanceof Date"等價於"Object instanceof Date"了,因此返回false就很正常了。
在防護式編程中常常會用斷言(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,那咱們就能夠在程序中處處設"樁"了,斷言這二者的關係,若是不知足即代表程序已經出現了異常,業務也就沒有必要運行下去了。
咱們常常在系統中定義一個常量接口(或常量類),以囊括系統中所涉及的常量,從而簡化代碼,方便開發,在不少的開源項目中已經採用了相似的方法,好比在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應該有個設置自動編譯的入口,有興趣你們能夠本身嘗試一下。
判斷一個數是奇數仍是偶數是小學裏的基本知識,可以被2整除的整數是偶數,不能被2整除的數是奇數,這規則簡單明瞭,還有什麼可考慮的?好,咱們來看一個例子,代碼以下:
1 import java.util.Scanner; 2 3 public class Client21 { 4 public static void main(String[] args) { 5 // 接收鍵盤輸入參數 6 Scanner input = new Scanner(System.in); 7 System.out.println("輸入多個數字判斷奇偶:"); 8 while (input.hasNextInt()) { 9 int i = input.nextInt(); 10 String str = i + "-->" + (i % 2 == 1 ? "奇數" : "偶數"); 11 System.out.println(str); 12 13 } 14 } 15 }
輸入多個數字,而後判斷每一個數字的奇偶性,不能被2整除的就是奇數,其它的都是偶數,徹底是根據奇偶數的定義編寫的程序,咱們開看看打印的結果:
輸入多個數字判斷奇偶:1 2 0 -1 -2 1-->奇數 2-->偶數 0-->偶數 -1-->偶數 -2-->偶數
前三個還很靠譜,第四個參數-1怎麼多是偶數呢,這Java也太差勁了吧。如此簡單的計算也會出錯!別忙着下結論,咱們先來了解一下Java中的取餘(%標識符)算法,模擬代碼以下:
// 模擬取餘計算,dividend被除數,divisor除數 public static int remainder(int dividend, int divisor) { return dividend - dividend / divisor * divisor; }
看到這段程序,你們都會心的笑了,原來Java這麼處理取餘計算的呀,根據上面的模擬取餘可知,當輸入-1的時候,運算結果爲-1,固然不等於1了,因此它就被斷定爲偶數了,也就是咱們的判斷失誤了。問題明白了,修正也很簡單,改成判斷是不是偶數便可。代碼以下: i % 2 == 0 ? "偶數" : "奇數";
注意:對於基礎知識,咱們應該"知其然,並知其因此然"。
在平常生活中,最容易接觸到的小數就是貨幣,好比,你付給售貨員10元錢購買一個9.6元的零食,售貨員應該找你0.4元,也就是4毛錢纔對,咱們來看下面的程序:
public class Client22 { public static void main(String[] args) { System.out.println(10.00-9.60); } }
咱們的指望結果是0.4,也應該是這個數字,可是打印出來的倒是:0.40000000000000036,這是爲何呢?
這是由於在計算機中浮點數有可能(注意是有可能)是不許確的,它只能無限接近準確值,而不能徹底精確。爲何會如此呢?這是由浮點數的存儲規則所決定的,咱們先來看看0.4這個十進制小數如何轉換成二進制小數,使用"乘2取整,順序排列"法(不懂,這就沒招了,這太基礎了),咱們發現0.4不能使用二進制準確的表示,在二進制數世界裏它是一個無限循環的小數,也就是說,"展現" 都不能 "展現",更別說在內存中存儲了(浮點數的存儲包括三部分:符號位、指數位、尾數,具體再也不介紹),能夠這樣理解,在十進制的世界裏沒有辦法惟一準確表示1/3,那麼在二進制的世界裏固然也沒法準確表示1/5(若是二進制也有分數的話卻是能夠表示),在二進制的世界裏1/5是一個無限循環的小數。
你們可能要說了,那我對結果取整不就對了嗎?代碼以下
public class Client22 { public static void main(String[] args) { NumberFormat f = new DecimalFormat("#.##"); System.out.println(f.format(10.00-9.60)); } }
打印出的結果是0.4,看似解決了。可是隱藏了一個很深的問題。咱們來思考一下金融行業的計算方法,會計系統通常記錄小數點後的4爲小數,可是在彙總、展示、報表中、則只記錄小數點後的2位小數,若是使用浮點數來計算貨幣,想一想看,在大批量加減乘除後結果會有很大的差距(其中還涉及到四捨五入的問題)!會計系統要求的就是準確,可是由於計算機的緣故不許確了,那真是罪過,要解決此問題有兩種方法:
(1)、使用BigDecimal
BigDecimal是專門爲彌補浮點數沒法精確計算的缺憾而設計的類,而且它自己也提供了加減乘除的經常使用數學算法。特別是與數據庫Decimal類型的字段映射時,BigDecimal是最優的解決方案。
(2)、使用整型
把參與運算的值擴大100倍,並轉爲整型,而後在展示時再縮小100倍,這樣處理的好處是計算簡單,準確,通常在非金融行業(如零售行業)應用較多。此方法還會用於某些零售POS機,他們輸入和輸出的所有是整數,那運算就更簡單了.
咱們作一個小學生的題目,光速每秒30萬千米,根據光線的旅行時間,計算月球和地球,太陽和地球之間的距離。代碼以下:
1 public class Client23 { 2 // 光速是30萬千米/秒,常量 3 public static final int LIGHT_SPEED = 30 * 10000 * 1000; 4 5 public static void main(String[] args) { 6 System.out.println("題目1:月球照射到地球須要一秒,計算月亮和地球的距離。"); 7 long dis1 = LIGHT_SPEED * 1; 8 System.out.println("月球與地球的距離是:" + dis1 + " 米 "); 9 System.out.println("-------------------------------"); 10 System.out.println("題目2:太陽光照射到地球須要8分鐘,計算太陽到地球的距離."); 11 // 可能要超出整數範圍,使用long型 12 long dis2 = LIGHT_SPEED * 60 * 8; 13 System.out.println("太陽與地球之間的距離是:" + dis2 + " 米"); 14 } 15 }
估計有人鄙視了,這種小學生的乘法有神麼可作的,不錯,就是一個乘法運算,咱們運行以後的結果以下:
題目1:月球照射到地球須要一秒,計算月亮和地球的距離。
月球與地球的距離是:300000000 米
-------------------------------
題目2:太陽光照射到地球須要8分鐘,計算太陽到地球的距離.
太陽與地球之間的距離是:-2028888064 米
太陽和地球的距離居然是負的,詭異。dis2不是已經考慮到int類型可能越界的問題,並使用了long型嗎,怎麼還會出現負值呢?
那是由於Java是先運算而後進行類型轉換的,具體的說就是由於dis2的三個運算參數都是int型,三者相乘的結果雖然也是int型,可是已經超過了int的最大值,因此其值就是負值了(爲何是負值,由於過界了就會重頭開始),再轉換爲long型,結果仍是負值。
問題知道了,解決起來也很簡單,只要加個小小的L便可,代碼以下:
long dis2 = LIGHT_SPEED * 60L * 8;
60L是一個長整型,乘出來的結果也是一個長整型的(此乃Java的基本轉換規則,向數據範圍大的方向轉換,也就是加寬類型),在尚未超過int類型的範圍時就已經轉換爲long型了,完全解決了越界問題。在實際開發中,更通用的作法是主動聲明類型轉化(注意,不是強制類型轉換)代碼以下:
long dis2 = 1L * LIGHT_SPEED * 60L * 8
既然指望的結果是long型,那就讓第一個參與的參數也是Long(1L)吧,也就說明"嗨"我已是長整型了,大家都跟着我一塊轉爲長整型吧。
注意:基本類型轉換時,使用主動聲明方式減小沒必要要的Bug.
某商家生產的電子產品很是暢銷,須要提早30天預訂才能搶到手,同時還規定了一個會員可擁有的最多產品數量,目的是爲了防止囤積壓貨肆意加價。會員的預訂過程是這樣的:先登陸官方網站,選擇產品型號,而後設置須要預訂的數量,提交,符合規則即提示下單成功,不符合規則提示下單失敗,後臺的處理模擬以下:
1 import java.util.Scanner; 2 3 public class Client24 { 4 // 一個會員擁有產品的最多數量 5 public final static int LIMIT = 2000; 6 7 public static void main(String[] args) { 8 // 會員當前用有的產品數量 9 int cur = 1000; 10 Scanner input = new Scanner(System.in); 11 System.out.println("請輸入須要預約的數量:"); 12 while (input.hasNextInt()) { 13 int order = input.nextInt(); 14 if (order > 0 && order + cur <= LIMIT) { 15 System.out.println("你已經成功預約:" + order + " 個產品"); 16 } else { 17 System.out.println("超過限額,預約失敗!"); 18 } 19 } 20 21 } 22 }
這是一個簡單的訂單處理程序,其中cur表明的是會員當前擁有的產品數量,LIMIT是一個會員最多擁有的產品數量(現實中,這兩個參數固然是從數據庫中得到的,不過這裏是一個模擬程序),若是當前預訂數量與擁有數量之和超過了最大數量,則預訂失敗,不然下單成功。業務邏輯很簡單,同時在web界面上對訂單數量作了嚴格的校驗,好比不能是負值、不能超過最大數量等,可是人算不如天算,運行不到兩小時數據庫中就出現了異常數據:某會員擁有的產品數量與預約數量之和遠遠大於限額。怎麼會這樣呢?程序邏輯上不可能有問題呀,這如何產生的呢?咱們來模擬一下,第一次輸入:
請輸入須要預約的數量:800 你已經成功預約800個產品
這徹底知足條件,沒有任何問題,繼續輸入:
請輸入須要預約的數量:2147483647 你已經成功預約2147483647個產品
看到沒有,這個數字已經遠遠超過了2000的限額,可是居然預約成功了,真實神奇!
看着2147483647這個數字很眼熟?那就對了,這個數字就是int類型的最大值,沒錯,有人輸入了一個最大值,使校驗條件失敗了,Why?咱們來看程序,order的值是2147483647那再加上1000就超出int的範圍了,其結果是-2147482649,那固然是小於正數2000了!一句歸其緣由:數字越界使校驗條件失效。
在單元測試中,有一項測試叫作邊界測試(也叫臨界測試),若是一個方法接收的是int類型的參數,那麼如下三個值是必須測試的:0、正最大、負最小,其中正最大、負最小是邊界值,若是這三個值都沒有問題,方法纔是比較安全可靠的。咱們的例子就是由於缺乏邊界測試,導致生產系統產生了嚴重的誤差。
也許你要疑惑了,Web界面已經作了嚴格的校驗,爲何還能輸入2147483647 這麼大的數字呢?是否說明Web校驗不嚴格?錯了,不是這樣的,Web校驗都是在頁面上經過JavaScript實現的,只能限制普通用戶(這裏的普通用戶是指不懂html,不懂HTTP,不懂Java的簡單使用者),而對於高手,這些校驗基本上就是擺設,HTTP是明文傳輸的,將其攔截幾回,分析一下數據結構,而後寫一個模擬器,一切前端校驗就成了浮雲!想日後臺提交個什麼數據還不是信手拈來!
本建議仍是來重溫一個小學數學問題:四捨五入。四捨五入是一種近似精確的計算方法,在Java5以前,咱們通常是經過Math.round來得到指定精度的整數或小數的,這種方法使用很是普遍,代碼以下:
public class Client25 { public static void main(String[] args) { System.out.println("10.5近似值: "+Math.round(10.5)); System.out.println("-10.5近似值: "+Math.round(-10.5)); } }
輸出結果爲:10.5近似值: 11 -10.5近似值: -10
這是四捨五入的經典案例,也是初級面試官很樂意選擇的考題,絕對值相同的兩個數字,近似值爲何就不一樣了呢?這是由Math.round採用的舍入規則決定的(採用的是正無窮方向舍入規則),咱們知道四捨五入是有偏差的:其偏差值是舍入的一半。咱們以舍入運用最頻繁的銀行利息計算爲例來闡述此問題。
咱們知道銀行的盈利渠道主要是利息差,從儲戶手裏收攏資金,而後房貸出去,期間的利息差額即是所得到利潤,對一個銀行來講,對付給儲戶的利息計算很是頻繁,人民銀行規定每一個季度末月的20日爲銀行結息日,一年有4次的結息日。
場景介紹完畢,咱們回頭來看看四捨五入,小於5的數字被捨去,大於5的數字進位後捨去,因爲單位上的數字都是天然計算出來的,按照利率計算可知,被捨去的數字都分佈在0~9之間,下面以10筆存款利息計算做爲模型,以銀行家的身份來思考這個算法:
四舍:捨棄的數值是:0.000、0.00一、0.00二、0.00三、0.004由於是捨棄的,對於銀行家來講就不須要付款給儲戶了,那每舍一個數字就會賺取相應的金額:0.000、0.00一、0.00二、0.00三、0.004.
五入:進位的數值是:0.00五、0.00六、0.00七、0.00八、0.009,由於是進位,對銀行家來講,每進一位就會多付款給儲戶,也就是虧損了,那虧損部分就是其對應的10進制補數:0.00五、.000四、0.00三、0.00二、0.001.
由於捨棄和進位的數字是均勻分佈在0~9之間,對於銀行家來講,沒10筆存款的利息因採用四捨五入而得到的盈利是:
0.000 + 0.001 + 0.002 + 0.003 + 0.004 - 0.005 - 0.004 - 0.003 - 0.002 - 0.001 = - 0.005;
也就是說,每10筆利息計算中就損失0.005元,即每筆利息計算就損失0.0005元,這對一家有5千萬儲戶的銀行家來講(對國內銀行來講,5千萬是個小數字),每一年僅僅由於四捨五入的偏差而損失的金額是:
銀行帳戶數量(5千萬)*4(一年計算四次利息)*0.0005(每筆利息損失的金額)
5000*10000*0.0005*4=100000.0;即,每一年由於一個算法偏差就損失了10萬元,事實上以上的假設條件都是很是保守的,實際狀況可能損失的更多。那各位可能要說了,銀行還要放貸呀,放出去這筆計算偏差不就抵消了嗎?不會抵消,銀行的貸款數量是很是有限的其數量級根本沒法和存款相比。
這個算法偏差是由美國銀行家發現的(那但是私人銀行,錢是本身的,白白損失了可不行),而且對此提出了一個修正算法,叫作銀行家舍入(Banker's Round)的近似算法,其規則以下:
以上規則彙總成一句話:四捨六入五考慮,五後非零就進一,五後爲零看奇偶,五前爲偶應捨去,五前爲奇要進一。咱們舉例說明,取2位精度;
round(10.5551) = 10.56 round(10.555) = 10.56 round(10.545) = 10.54
要在Java5以上的版本中使用銀行家的舍入法則很是簡單,直接使用RoundingMode類提供的Round模式便可,示例代碼以下:
1 import java.math.BigDecimal; 2 import java.math.RoundingMode; 3 4 public class Client25 { 5 public static void main(String[] args) { 6 // 存款 7 BigDecimal d = new BigDecimal(888888); 8 // 月利率,乘3計算季利率 9 BigDecimal r = new BigDecimal(0.001875*3); 10 //計算利息 11 BigDecimal i =d.multiply(r).setScale(2,RoundingMode.HALF_EVEN); 12 System.out.println("季利息是:"+i); 13 14 } 15 }
在上面的例子中,咱們使用了BigDecimal類,而且採用了setScale方法設置了精度,同時傳遞了一個RoundingMode.HALF_EVEN參數表示使用銀行家法則進行近似計算,BigDecimal和RoundingMode是一個絕配,想要採用什麼方式使用RoundingMode設置便可。目前Java支持如下七種舍入方式:
在普通的項目中舍入模式不會有太多影響,能夠直接使用Math.round方法,但在大量與貨幣數字交互的項目中,必定要選擇好近似的計算模式,儘可能減小因算法不一樣而形成的損失。
注意:根據不一樣的場景,慎重選擇不一樣的舍入模式,以提升項目的精準度,減小算法損失。
附錄:此處說的這些常量所有來自java的RoundingMode類,故而貼出此類的源碼供你們參考。
1 package java.math; 2 /** 3 * Specifies a <i>rounding behavior</i> for numerical operations 4 * capable of discarding precision. Each rounding mode indicates how 5 * the least significant returned digit of a rounded result is to be 6 * calculated. If fewer digits are returned than the digits needed to 7 * represent the exact numerical result, the discarded digits will be 8 * referred to as the <i>discarded fraction</i> regardless the digits' 9 * contribution to the value of the number. In other words, 10 * considered as a numerical value, the discarded fraction could have 11 * an absolute value greater than one. 12 * 13 * <p>Each rounding mode description includes a table listing how 14 * different two-digit decimal values would round to a one digit 15 * decimal value under the rounding mode in question. The result 16 * column in the tables could be gotten by creating a 17 * {@code BigDecimal} number with the specified value, forming a 18 * {@link MathContext} object with the proper settings 19 * ({@code precision} set to {@code 1}, and the 20 * {@code roundingMode} set to the rounding mode in question), and 21 * calling {@link BigDecimal#round round} on this number with the 22 * proper {@code MathContext}. A summary table showing the results 23 * of these rounding operations for all rounding modes appears below. 24 * 25 *<p> 26 *<table border> 27 * <caption><b>Summary of Rounding Operations Under Different Rounding Modes</b></caption> 28 * <tr><th></th><th colspan=8>Result of rounding input to one digit with the given 29 * rounding mode</th> 30 * <tr valign=top> 31 * <th>Input Number</th> <th>{@code UP}</th> 32 * <th>{@code DOWN}</th> 33 * <th>{@code CEILING}</th> 34 * <th>{@code FLOOR}</th> 35 * <th>{@code HALF_UP}</th> 36 * <th>{@code HALF_DOWN}</th> 37 * <th>{@code HALF_EVEN}</th> 38 * <th>{@code UNNECESSARY}</th> 39 * 40 * <tr align=right><td>5.5</td> <td>6</td> <td>5</td> <td>6</td> <td>5</td> <td>6</td> <td>5</td> <td>6</td> <td>throw {@code ArithmeticException}</td> 41 * <tr align=right><td>2.5</td> <td>3</td> <td>2</td> <td>3</td> <td>2</td> <td>3</td> <td>2</td> <td>2</td> <td>throw {@code ArithmeticException}</td> 42 * <tr align=right><td>1.6</td> <td>2</td> <td>1</td> <td>2</td> <td>1</td> <td>2</td> <td>2</td> <td>2</td> <td>throw {@code ArithmeticException}</td> 43 * <tr align=right><td>1.1</td> <td>2</td> <td>1</td> <td>2</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>throw {@code ArithmeticException}</td> 44 * <tr align=right><td>1.0</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> 45 * <tr align=right><td>-1.0</td> <td>-1</td> <td>-1</td> <td>-1</td> <td>-1</td> <td>-1</td> <td>-1</td> <td>-1</td> <td>-1</td> 46 * <tr align=right><td>-1.1</td> <td>-2</td> <td>-1</td> <td>-1</td> <td>-2</td> <td>-1</td> <td>-1</td> <td>-1</td> <td>throw {@code ArithmeticException}</td> 47 * <tr align=right><td>-1.6</td> <td>-2</td> <td>-1</td> <td>-1</td> <td>-2</td> <td>-2</td> <td>-2</td> <td>-2</td> <td>throw {@code ArithmeticException}</td> 48 * <tr align=right><td>-2.5</td> <td>-3</td> <td>-2</td> <td>-2</td> <td>-3</td> <td>-3</td> <td>-2</td> <td>-2</td> <td>throw {@code ArithmeticException}</td> 49 * <tr align=right><td>-5.5</td> <td>-6</td> <td>-5</td> <td>-5</td> <td>-6</td> <td>-6</td> <td>-5</td> <td>-6</td> <td>throw {@code ArithmeticException}</td> 50 *</table> 51 * 52 * 53 * <p>This {@code enum} is intended to replace the integer-based 54 * enumeration of rounding mode constants in {@link BigDecimal} 55 * ({@link BigDecimal#ROUND_UP}, {@link BigDecimal#ROUND_DOWN}, 56 * etc. ). 57 * 58 * @see BigDecimal 59 * @see MathContext 60 * @author Josh Bloch 61 * @author Mike Cowlishaw 62 * @author Joseph D. Darcy 63 * @since 1.5 64 */ 65 public enum RoundingMode { 66 67 /** 68 * Rounding mode to round away from zero. Always increments the 69 * digit prior to a non-zero discarded fraction. Note that this 70 * rounding mode never decreases the magnitude of the calculated 71 * value. 72 * 73 *<p>Example: 74 *<table border> 75 *<tr valign=top><th>Input Number</th> 76 * <th>Input rounded to one digit<br> with {@code UP} rounding 77 *<tr align=right><td>5.5</td> <td>6</td> 78 *<tr align=right><td>2.5</td> <td>3</td> 79 *<tr align=right><td>1.6</td> <td>2</td> 80 *<tr align=right><td>1.1</td> <td>2</td> 81 *<tr align=right><td>1.0</td> <td>1</td> 82 *<tr align=right><td>-1.0</td> <td>-1</td> 83 *<tr align=right><td>-1.1</td> <td>-2</td> 84 *<tr align=right><td>-1.6</td> <td>-2</td> 85 *<tr align=right><td>-2.5</td> <td>-3</td> 86 *<tr align=right><td>-5.5</td> <td>-6</td> 87 *</table> 88 */ 89 UP(BigDecimal.ROUND_UP), 90 91 /** 92 * Rounding mode to round towards zero. Never increments the digit 93 * prior to a discarded fraction (i.e., truncates). Note that this 94 * rounding mode never increases the magnitude of the calculated value. 95 * 96 *<p>Example: 97 *<table border> 98 *<tr valign=top><th>Input Number</th> 99 * <th>Input rounded to one digit<br> with {@code DOWN} rounding 100 *<tr align=right><td>5.5</td> <td>5</td> 101 *<tr align=right><td>2.5</td> <td>2</td> 102 *<tr align=right><td>1.6</td> <td>1</td> 103 *<tr align=right><td>1.1</td> <td>1</td> 104 *<tr align=right><td>1.0</td> <td>1</td> 105 *<tr align=right><td>-1.0</td> <td>-1</td> 106 *<tr align=right><td>-1.1</td> <td>-1</td> 107 *<tr align=right><td>-1.6</td> <td>-1</td> 108 *<tr align=right><td>-2.5</td> <td>-2</td> 109 *<tr align=right><td>-5.5</td> <td>-5</td> 110 *</table> 111 */ 112 DOWN(BigDecimal.ROUND_DOWN), 113 114 /** 115 * Rounding mode to round towards positive infinity. If the 116 * result is positive, behaves as for {@code RoundingMode.UP}; 117 * if negative, behaves as for {@code RoundingMode.DOWN}. Note 118 * that this rounding mode never decreases the calculated value. 119 * 120 *<p>Example: 121 *<table border> 122 *<tr valign=top><th>Input Number</th> 123 * <th>Input rounded to one digit<br> with {@code CEILING} rounding 124 *<tr align=right><td>5.5</td> <td>6</td> 125 *<tr align=right><td>2.5</td> <td>3</td> 126 *<tr align=right><td>1.6</td> <td>2</td> 127 *<tr align=right><td>1.1</td> <td>2</td> 128 *<tr align=right><td>1.0</td> <td>1</td> 129 *<tr align=right><td>-1.0</td> <td>-1</td> 130 *<tr align=right><td>-1.1</td> <td>-1</td> 131 *<tr align=right><td>-1.6</td> <td>-1</td> 132 *<tr align=right><td>-2.5</td> <td>-2</td> 133 *<tr align=right><td>-5.5</td> <td>-5</td> 134 *</table> 135 */ 136 CEILING(BigDecimal.ROUND_CEILING), 137 138 /** 139 * Rounding mode to round towards negative infinity. If the 140 * result is positive, behave as for {@code RoundingMode.DOWN}; 141 * if negative, behave as for {@code RoundingMode.UP}. Note that 142 * this rounding mode never increases the calculated value. 143 * 144 *<p>Example: 145 *<table border> 146 *<tr valign=top><th>Input Number</th> 147 * <th>Input rounded to one digit<br> with {@code FLOOR} rounding 148 *<tr align=right><td>5.5</td> <td>5</td> 149 *<tr align=right><td>2.5</td> <td>2</td> 150 *<tr align=right><td>1.6</td> <td>1</td> 151 *<tr align=right><td>1.1</td> <td>1</td> 152 *<tr align=right><td>1.0</td> <td>1</td> 153 *<tr align=right><td>-1.0</td> <td>-1</td> 154 *<tr align=right><td>-1.1</td> <td>-2</td> 155 *<tr align=right><td>-1.6</td> <td>-2</td> 156 *<tr align=right><td>-2.5</td> <td>-3</td> 157 *<tr align=right><td>-5.5</td> <td>-6</td> 158 *</table> 159 */ 160 FLOOR(BigDecimal.ROUND_FLOOR), 161 162 /** 163 * Rounding mode to round towards {@literal "nearest neighbor"} 164 * unless both neighbors are equidistant, in which case round up. 165 * Behaves as for {@code RoundingMode.UP} if the discarded 166 * fraction is ≥ 0.5; otherwise, behaves as for 167 * {@code RoundingMode.DOWN}. Note that this is the rounding 168 * mode commonly taught at school. 169 * 170 *<p>Example: 171 *<table border> 172 *<tr valign=top><th>Input Number</th> 173 * <th>Input rounded to one digit<br> with {@code HALF_UP} rounding 174 *<tr align=right><td>5.5</td> <td>6</td> 175 *<tr align=right><td>2.5</td> <td>3</td> 176 *<tr align=right><td>1.6</td> <td>2</td> 177 *<tr align=right><td>1.1</td> <td>1</td> 178 *<tr align=right><td>1.0</td> <td>1</td> 179 *<tr align=right><td>-1.0</td> <td>-1</td> 180 *<tr align=right><td>-1.1</td> <td>-1</td> 181 *<tr align=right><td>-1.6</td> <td>-2</td> 182 *<tr align=right><td>-2.5</td> <td>-3</td> 183 *<tr align=right><td>-5.5</td> <td>-6</td> 184 *</table> 185 */ 186 HALF_UP(BigDecimal.ROUND_HALF_UP), 187 188 /** 189 * Rounding mode to round towards {@literal "nearest neighbor"} 190 * unless both neighbors are equidistant, in which case round 191 * down. Behaves as for {@code RoundingMode.UP} if the discarded 192 * fraction is > 0.5; otherwise, behaves as for 193 * {@code RoundingMode.DOWN}. 194 * 195 *<p>Example: 196 *<table border> 197 *<tr valign=top><th>Input Number</th> 198 * <th>Input rounded to one digit<br> with {@code HALF_DOWN} rounding 199 *<tr align=right><td>5.5</td> <td>5</td> 200 *<tr align=right><td>2.5</td> <td>2</td> 201 *<tr align=right><td>1.6</td> <td>2</td> 202 *<tr align=right><td>1.1</td> <td>1</td> 203 *<tr align=right><td>1.0</td> <td>1</td> 204 *<tr align=right><td>-1.0</td> <td>-1</td> 205 *<tr align=right><td>-1.1</td> <td>-1</td> 206 *<tr align=right><td>-1.6</td> <td>-2</td> 207 *<tr align=right><td>-2.5</td> <td>-2</td> 208 *<tr align=right><td>-5.5</td> <td>-5</td> 209 *</table> 210 */ 211 HALF_DOWN(BigDecimal.ROUND_HALF_DOWN), 212 213 /** 214 * Rounding mode to round towards the {@literal "nearest neighbor"} 215 * unless both neighbors are equidistant, in which case, round 216 * towards the even neighbor. Behaves as for 217 * {@code RoundingMode.HALF_UP} if the digit to the left of the 218 * discarded fraction is odd; behaves as for 219 * {@code RoundingMode.HALF_DOWN} if it's even. Note that this 220 * is the rounding mode that statistically minimizes cumulative 221 * error when applied repeatedly over a sequence of calculations. 222 * It is sometimes known as {@literal "Banker's rounding,"} and is 223 * chiefly used in the USA. This rounding mode is analogous to 224 * the rounding policy used for {@code float} and {@code double} 225 * arithmetic in Java. 226 * 227 *<p>Example: 228 *<table border> 229 *<tr valign=top><th>Input Number</th> 230 * <th>Input rounded to one digit<br> with {@code HALF_EVEN} rounding 231 *<tr align=right><td>5.5</td> <td>6</td> 232 *<tr align=right><td>2.5</td> <td>2</td> 233 *<tr align=right><td>1.6</td> <td>2</td> 234 *<tr align=right><td>1.1</td> <td>1</td> 235 *<tr align=right><td>1.0</td> <td>1</td> 236 *<tr align=right><td>-1.0</td> <td>-1</td> 237 *<tr align=right><td>-1.1</td> <td>-1</td> 238 *<tr align=right><td>-1.6</td> <td>-2</td> 239 *<tr align=right><td>-2.5</td> <td>-2</td> 240 *<tr align=right><td>-5.5</td> <td>-6</td> 241 *</table> 242 */ 243 HALF_EVEN(BigDecimal.ROUND_HALF_EVEN), 244 245 /** 246 * Rounding mode to assert that the requested operation has an exact 247 * result, hence no rounding is necessary. If this rounding mode is 248 * specified on an operation that yields an inexact result, an 249 * {@code ArithmeticException} is thrown. 250 *<p>Example: 251 *<table border> 252 *<tr valign=top><th>Input Number</th> 253 * <th>Input rounded to one digit<br> with {@code UNNECESSARY} rounding 254 *<tr align=right><td>5.5</td> <td>throw {@code ArithmeticException}</td> 255 *<tr align=right><td>2.5</td> <td>throw {@code ArithmeticException}</td> 256 *<tr align=right><td>1.6</td> <td>throw {@code ArithmeticException}</td> 257 *<tr align=right><td>1.1</td> <td>throw {@code ArithmeticException}</td> 258 *<tr align=right><td>1.0</td> <td>1</td> 259 *<tr align=right><td>-1.0</td> <td>-1</td> 260 *<tr align=right><td>-1.1</td> <td>throw {@code ArithmeticException}</td> 261 *<tr align=right><td>-1.6</td> <td>throw {@code ArithmeticException}</td> 262 *<tr align=right><td>-2.5</td> <td>throw {@code ArithmeticException}</td> 263 *<tr align=right><td>-5.5</td> <td>throw {@code ArithmeticException}</td> 264 *</table> 265 */ 266 UNNECESSARY(BigDecimal.ROUND_UNNECESSARY); 267 268 // Corresponding BigDecimal rounding constant 269 final int oldMode; 270 271 /** 272 * Constructor 273 * 274 * @param oldMode The {@code BigDecimal} constant corresponding to 275 * this mode 276 */ 277 private RoundingMode(int oldMode) { 278 this.oldMode = oldMode; 279 } 280 281 /** 282 * Returns the {@code RoundingMode} object corresponding to a 283 * legacy integer rounding mode constant in {@link BigDecimal}. 284 * 285 * @param rm legacy integer rounding mode to convert 286 * @return {@code RoundingMode} corresponding to the given integer. 287 * @throws IllegalArgumentException integer is out of range 288 */ 289 public static RoundingMode valueOf(int rm) { 290 switch(rm) { 291 292 case BigDecimal.ROUND_UP: 293 return UP; 294 295 case BigDecimal.ROUND_DOWN: 296 return DOWN; 297 298 case BigDecimal.ROUND_CEILING: 299 return CEILING; 300 301 case BigDecimal.ROUND_FLOOR: 302 return FLOOR; 303 304 case BigDecimal.ROUND_HALF_UP: 305 return HALF_UP; 306 307 case BigDecimal.ROUND_HALF_DOWN: 308 return HALF_DOWN; 309 310 case BigDecimal.ROUND_HALF_EVEN: 311 return HALF_EVEN; 312 313 case BigDecimal.ROUND_UNNECESSARY: 314 return UNNECESSARY; 315 316 default: 317 throw new IllegalArgumentException("argument out of range"); 318 } 319 } 320 }
咱們知道Java引入包裝類型(Wrapper Types)是爲了解決基本類型的實例化問題,以便讓一個基本類型也能參與到面向對象的編程世界中。而在Java5中泛型更是對基本類型說了"不",若是把一個整型放入List中,就必須使用Integer包裝類型。咱們看一段代碼:
1 import java.util.ArrayList; 2 import java.util.List; 3 4 public class Client26 { 5 6 public static int testMethod(List<Integer> list) { 7 int count = 0; 8 for (int i : list) { 9 count += i; 10 } 11 return count; 12 } 13 14 public static void main(String[] args) { 15 List<Integer> list = new ArrayList<Integer>(); 16 list.add(1); 17 list.add(2); 18 list.add(null); 19 System.out.println(testMethod(list)); 20 } 21 }
testMethod接收一個元素是整型的List參數,計算全部元素之和,這在統計和項目中很常見,而後編寫一個測試testMethod,在main方法中把一、2和空值都放到List中,而後調用方法計算,如今思考一下會不會報錯。應該不會吧,基本類型和包裝類型都是能夠經過自動裝箱(Autoboxing)和自動拆箱(AutoUnboxing)自由轉換的,null應該能夠轉換爲0吧,真的是這樣嗎?運行以後的結果是: Exception in thread "main" java.lang.NullPointerException 運行失敗,報空指針異常,咱們稍稍思考一下很快就知道緣由了:在程序for循環中,隱含了一個拆箱過程,在此過程當中包裝類型轉換爲了基本類型。咱們知道拆箱過程是經過調用包裝對象的intValue方法來實現的,因爲包裝類型爲null,訪問其intValue方法報空指針異常就在所不免了。問題清楚了,修改也很簡單,加入null值檢查便可,代碼以下:
public static int testMethod(List<Integer> list) { int count = 0; for (Integer i : list) { count += (i != null) ? i : 0; } return count; }
上面以Integer和int爲例說明了拆箱問題,其它7個包裝對象的拆箱過程也存在着一樣的問題。包裝對象和拆箱對象能夠自由轉換,這不假,可是要剔除null值,null值並不能轉換爲基本類型。對於此問題,咱們謹記一點:包裝類型參與運算時,要作null值校驗。
基本類型是能夠比較大小的,其所對應的包裝類型都實現了Comparable接口,也說明了此問題,那咱們來比較一下兩個包裝類型的大小,代碼以下:
1 public class Client27 { 2 public static void main(String[] args) { 3 Integer i = new Integer(100); 4 Integer j = new Integer(100); 5 compare(i, j); 6 } 7 8 public static void compare(Integer i, Integer j) { 9 System.out.println(i == j); 10 System.out.println(i > j); 11 System.out.println(i < j); 12 13 } 14 }
代碼很簡單,產生了兩個Integer對象,而後比較兩個的大小關係,既然包裝類型和基本類型是能夠自由轉換的,那上面的代碼是否是就能夠打印出兩個相等的值呢?讓事實說話,運行結果以下:
false false false
居然是3個false,也就是說兩個值之間不相等,也沒大小關係,這個也太奇怪了吧。不奇怪,咱們來一 一解釋:
問題清楚了,修改老是比較容易的,直接使用Integer的實例compareTo方法便可,可是這類問題的產生更應該說是習慣問題,只要是兩個對象之間的比較就應該採用相應的方法,而不是經過Java的默認機制來處理,除非你肯定對此很是瞭解。
上一個建議咱們解釋了包裝對象的比較問題,本建議將繼續深刻討論相關問題,首先看看以下代碼:
1 import java.util.Scanner; 2 3 public class Client28 { 4 public static void main(String[] args) { 5 Scanner input = new Scanner(System.in); 6 while (input.hasNextInt()) { 7 int tempInt = input.nextInt(); 8 System.out.println("\n=====" + tempInt + " 的相等判斷====="); 9 // 兩個經過new產生的對象 10 Integer i = new Integer(tempInt); 11 Integer j = new Integer(tempInt); 12 System.out.println(" new 產生的對象:" + (i == j)); 13 // 基本類型轉換爲包裝類型後比較 14 i = tempInt; 15 j = tempInt; 16 System.out.println(" 基本類型轉換的對象:" + (i == j)); 17 // 經過靜態方法生成一個實例 18 i = Integer.valueOf(tempInt); 19 j = Integer.valueOf(tempInt); 20 System.out.println(" valueOf產生的對象:" + (i == j)); 21 } 22 } 23 }
輸入多個數字,而後按照3中不一樣的方式產生Integer對象,判斷其是否相等,注意這裏使用了"==",這說明判斷的不是同一個對象。咱們輸入三個數字12七、12八、555,結果以下:
127
=====127 的相等判斷=====
new 產生的對象:false
基本類型轉換的對象:true
valueOf產生的對象:true
128
=====128 的相等判斷=====
new 產生的對象:false
基本類型轉換的對象:false
valueOf產生的對象:false
555
=====555 的相等判斷=====
new 產生的對象:false
基本類型轉換的對象:false
valueOf產生的對象:false
很難以想象呀,數字127的比較結果居然和其它兩個數字不一樣,它的裝箱動做所產生的對象居然是同一個對象,valueOf產生的也是同一個對象,可是大於127的數字和128和555的比較過程當中產生的卻不是同一個對象,這是爲何?咱們來一個一個解釋。
(1)、new產生的Integer對象
new聲明的就是要生成一個新的對象,沒二話,這是兩個對象,地址確定不等,比較結果爲false。
(2)、裝箱生成的對象
對於這一點,首先要說明的是裝箱動做是經過valueOf方法實現的,也就是說後兩個算法相同的,那結果確定也是同樣的,如今問題是:valueOf是如何生成對象的呢?咱們來閱讀如下Integer.valueOf的源碼:
1 /** 2 * Returns an {@code Integer} instance representing the specified 3 * {@code int} value. If a new {@code Integer} instance is not 4 * required, this method should generally be used in preference to 5 * the constructor {@link #Integer(int)}, as this method is likely 6 * to yield significantly better space and time performance by 7 * caching frequently requested values. 8 * 9 * This method will always cache values in the range -128 to 127, 10 * inclusive, and may cache other values outside of this range. 11 * 12 * @param i an {@code int} value. 13 * @return an {@code Integer} instance representing {@code i}. 14 * @since 1.5 15 */ 16 public static Integer valueOf(int i) { 17 assert IntegerCache.high >= 127; 18 if (i >= IntegerCache.low && i <= IntegerCache.high) 19 return IntegerCache.cache[i + (-IntegerCache.low)]; 20 return new Integer(i); 21 }
這段代碼的意思已經很明瞭了,若是是-128到127之間的int類型轉換爲Integer對象,則直接從cache數組中得到,那cache數組裏是什麼東西,JDK7的源代碼以下:
1 /** 2 * Cache to support the object identity semantics of autoboxing for values between 3 * -128 and 127 (inclusive) as required by JLS. 4 * 5 * The cache is initialized on first usage. The size of the cache 6 * may be controlled by the -XX:AutoBoxCacheMax=<size> option. 7 * During VM initialization, java.lang.Integer.IntegerCache.high property 8 * may be set and saved in the private system properties in the 9 * sun.misc.VM class. 10 */ 11 12 private static class IntegerCache { 13 static final int low = -128; 14 static final int high; 15 static final Integer cache[]; 16 17 static { 18 // high value may be configured by property 19 int h = 127; 20 String integerCacheHighPropValue = 21 sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); 22 if (integerCacheHighPropValue != null) { 23 int i = parseInt(integerCacheHighPropValue); 24 i = Math.max(i, 127); 25 // Maximum array size is Integer.MAX_VALUE 26 h = Math.min(i, Integer.MAX_VALUE - (-low)); 27 } 28 high = h; 29 30 cache = new Integer[(high - low) + 1]; 31 int j = low; 32 for(int k = 0; k < cache.length; k++) 33 cache[k] = new Integer(j++); 34 } 35 36 private IntegerCache() {} 37 }
cache是IntegerCache內部類的一個靜態數組,容納的是-128到127之間的Integer對象。經過valueOf產生包裝對象時,若是int參數在-128到127之間,則直接從整型池中得到對象,不在該範圍內的int類型則經過new生成包裝對象。
明白了這一點,要理解上面的輸出結果就迎刃而解了,127的包裝對象是直接從整型池中得到的,無論你輸入多少次127這個數字,得到的對象都是同一個,那地址天然是相等的。而12八、555超出了整型池範圍,是經過new產生一個新的對象,地址不一樣,固然也就不相等了。
以上的理解也是整型池的原理,整型池的存在不只僅提升了系統性能,同時也節約了內存空間,這也是咱們使用整型池的緣由,也就是在聲明包裝對象的時候使用valueOf生成,而不是經過構造函數來生成的緣由。順便提醒你們,在判斷對象是否相等的時候,最好使用equals方法,避免使用"=="產生非預期效果。
注意:經過包裝類型的valueOf生成的包裝實例能夠顯著提升空間和時間性能。
包裝類型是一個類,它提供了諸如構造方法,類型轉換,比較等很是實用的功能,並且在Java5以後又實現了與基本類型的轉換,這使包裝類型如虎添翼,更是應用普遍了,在開發中包裝類型已經隨處可見,但不管是從安全性、性能方面來講,仍是從穩定性方面來講,基本類型都是首選方案。咱們看一段代碼:
1 public class Client29 { 2 public static void main(String[] args) { 3 Client29 c = new Client29(); 4 int i = 140; 5 // 分別傳遞int類型和Integer類型 6 c.testMethod(i); 7 c.testMethod(new Integer(i)); 8 } 9 10 public void testMethod(long a) { 11 System.out.println(" 基本類型的方法被調用"); 12 } 13 14 public void testMethod(Long a) { 15 System.out.println(" 包裝類型的方法被調用"); 16 } 17 }
在上面的程序中首先聲明瞭一個int變量i,而後加寬轉變成long型,再調用testMethod()方法,分別傳遞int和long的基本類型和包裝類型,諸位想一想該程序是否可以編譯?若是能編譯,輸出結果又是什麼呢?
首先,這段程序絕對是可以編譯的。不過,說不能編譯的同窗仍是動了一番腦筋的,你可能猜想如下這些地方不能編譯:
(1)、testMethod方法重載問題。定義的兩個testMethod()方法實現了重載,一個形參是基本類型,一個形參是包裝類型,這類重載很正常。雖然基本類型和包裝類型有自動裝箱、自動拆箱功能,但並不影響它們的重載,自動拆箱(裝箱)只有在賦值時纔會發生,和編譯重載沒有關係。
(2)、c.testMethod(i) 報錯。i 是int類型,傳遞到testMethod(long a)是沒有任何問題的,編譯器會自動把 i 的類型加寬,並將其轉變爲long型,這是基本類型的轉換法則,也沒有任何問題。
(3)、c.testMethod(new Integer(i))報錯。代碼中沒有testMethod(Integer i)方法,不可能接收一個Integer類型的參數,並且Integer和Long兩個包裝類型是兄弟關係,不是繼承關係,那就是說確定編譯失敗了?不,編譯時成功的,稍後再解釋爲何這裏編譯成功。
既然編譯經過了,咱們看一下輸出:
基本類型的方法被調用
基本類型的方法被調用
c.testMethod(i)的輸出是正常的,咱們已經解釋過了,那第二個輸出就讓人困惑了,爲何會調用testMethod(long a)方法呢?這是由於自動裝箱有一個重要原則:基本類型能夠先加寬,再轉變成寬類型的包裝類型,但不能直接轉變成寬類型的包裝類型。這句話比較拗口,簡單的說就是,int能夠加寬轉變成long,而後再轉變成Long對象,但不能直接轉變成包裝類型,注意這裏指的都是自動轉換,不是經過構造函數生成,爲了解釋這個原則,咱們再來看一個例子:
1 public class Client29 { 2 public static void main(String[] args) { 3 Client29 c = new Client29(); 4 int i = 140; 5 c.testMethod(i); 6 } 7 8 public void testMethod(Long a) { 9 System.out.println(" 包裝類型的方法被調用"); 10 } 11 }
這段程序的編譯是不經過的,由於i是一個int類型,不能自動轉變爲Long型,可是修改爲如下代碼就能夠經過了:
int i = 140; long a =(long)i; c.testMethod(a);
這就是int先加寬轉變成爲long型,而後自動轉換成Long型,規則說明了,咱們繼續來看testMethod(Integer.valueOf(i))是如何調用的,Integer.valueOf(i)返回的是一個Integer對象,這沒錯,可是Integer和int是能夠互相轉換的。沒有testMethod(Integer i)方法?不要緊,編譯器會嘗試轉換成int類型的實參調用,Ok,此次成功了,與testMethod(i)相同了,因而乎被加寬轉變成long型---結果也很明顯了。整個testMethod(Integer.valueOf(i))的執行過程是這樣的:
(1)、i 經過valueOf方法包裝成一個Integer對象
(2)、因爲沒有testMethod(Integer i)方法,編譯器會"聰明"的把Integer對象轉換成int。
(3)、int自動拓寬爲long,編譯結束
使用包裝類型確實有方便的方法,可是也引發一些沒必要要的困惑,好比咱們這個例子,若是testMethod()的兩個重載方法使用的是基本類型,並且實參也是基本類型,就不會產生以上問題,並且程序的可讀性更強。自動裝箱(拆箱)雖然很方便,但引發的問題也很是嚴重,咱們甚至都不知道執行的是哪一個方法。
注意:重申,基本類型優先考慮。
隨機數用的地方比較多,好比加密,混淆計算,咱們使用隨機數指望得到一個惟一的、不可仿造的數字,以免產生相同的業務數據形成混亂。在Java項目中一般是經過Math.random方法和Random類來得到隨機數的,咱們來看一段代碼:
1 import java.util.Random; 2 3 public class Client30 { 4 public static void main(String[] args) { 5 Random r = new Random(); 6 for(int i=1; i<=4; i++){ 7 System.out.println("第"+i+"次:"+r.nextInt()); 8 9 } 10 } 11 }
代碼很簡單,咱們通常都是這樣得到隨機數的,運行此程序可知,三次打印,的隨機數都不相同,即便屢次運行結果也不一樣,這也正是咱們想要隨機數的緣由,咱們再來看看下面的程序:
1 public class Client30 { 2 public static void main(String[] args) { 3 Random r = new Random(1000); 4 for(int i=1; i<=4; i++){ 5 System.out.println("第"+i+"次:"+r.nextInt()); 6 7 } 8 } 9 }
上面使用了Random的有參構造,運行結果以下:
第1次:-1244746321
第2次:1060493871
第3次:-1826063944
第4次:1976922248
計算機不一樣輸出的隨機數也不一樣,可是有一點是相同的:在同一臺機器上,甭管運行多少次,所打印的隨機數都是相同的,也就是說第一次運行,會打印出這幾個隨機數,第二次運行仍是打印出這三個隨機數,只要是在同一臺機器上,就永遠都會打印出相同的隨機數,彷佛隨機數不隨機了,問題何在?
那是由於產生的隨機數的種子被固定了,在Java中,隨機數的產生取決於種子,隨機數和種子之間的關係聽從如下兩個原則:
看完上面兩個規則,咱們再來看這個例子,會發現問題就出在有參構造上,Random類的默認種子(無參構造)是System.nonoTime()的返回值(JDK1.5版本之前默認種子是System.currentTimeMillis()的返回值),注意這個值是距離某一個固定時間點的納秒數,不一樣的操做系統和硬件有不一樣的固定時間點,也就是說不一樣的操做系統其納秒值是不一樣的,而同一個操做系統納秒值也會不一樣,隨機數天然也就不一樣了.(順便說下,System.nonoTime不能用於計算日期,那是由於"固定"的時間是不肯定的,納秒值甚至多是負值,這點與System.currentTiemMillis不一樣)。
new Random(1000)顯示的設置了隨機種子爲1000,運行屢次,雖然實例不一樣,但都會得到相同的四個隨機數,因此,除非必要,不然不要設置隨機種子。
順便提一下,在Java中有兩種方法能夠得到不一樣的隨機數:經過,java.util.Random類得到隨機數的原理和Math.random方法相同,Math.random方法也是經過生成一個Random類的實例,而後委託nextDouble()方法的,二者異曲同工,沒有差異。
看到這樣的標題,你們是否感到鬱悶呢?接口中有實現代碼嗎?這怎麼可能呢?確實,接口中能夠聲明常量,聲明抽象方法,能夠繼承父接口,但就是不能有具體實現,由於接口是一種契約(Contract),是一種框架性協議,這代表它的實現類都是同一種類型,或者具有類似特徵的一個集合體。對於通常程序,接口確實沒有任何實現,可是在那些特殊的程序中就例外了,閱讀以下代碼:
1 public class Client31 { 2 public static void main(String[] args) { 3 //調用接口的實現 4 B.s.doSomeThing(); 5 } 6 } 7 8 // 在接口中存在實現代碼 9 interface B { 10 public static final S s = new S() { 11 public void doSomeThing() { 12 System.out.println("我在接口中實現了"); 13 } 14 }; 15 } 16 17 // 被實現的接口 18 interface S { 19 public void doSomeThing(); 20 }
仔細看main方法,注意那個B接口。它調用了接口常量,在沒有實現任何顯示實現類的狀況下,它居然打印出告終果,那B接口中的s常量(接口是S)是在什麼地方被實現的呢?答案在B接口中。
在B接口中聲明瞭一個靜態常量s,其值是一個匿名內部類(Anonymous Inner Class)的實例對象,就是該匿名內部類(固然,也能夠不用匿名,直接在接口中是實現內部類也是容許的)實現了S接口。你看,在接口中也存在着實現代碼吧!
這確實很好,很強大,可是在通常的項目中,此類代碼是嚴禁出現的,緣由很簡單:這是一種很是很差的編碼習慣,接口是用來幹什麼的?接口是一個契約,不只僅約束着實現,同時也是一個保證,保證提供的服務(常量和方法)是穩定的、可靠的,若是把實現代碼寫到接口中,那接口就綁定了可能變化的因素,這會致使實現再也不穩定和可靠,是隨時均可能被拋棄、被更改、被重構的。因此,接口中雖然能夠有實現,但應避免使用。
注意:接口中不能出現實現代碼。
這個標題是否像上一個建議的標題同樣讓人鬱悶呢?什麼叫作變量必定要先聲明後賦值?Java中的變量不都是先聲明後使用的嗎?難道還能先使用後聲明?能不能暫且不說,咱們看一個例子,代碼以下:
1 public class Client32 { 2 public static int i = 1; 3 4 static { 5 i = 100; 6 } 7 public static void main(String[] args) { 8 System.out.println(i); 9 } 10 }
這段程序很簡單,輸出100嘛,對,確實是100,咱們稍稍修改一下,代碼以下:
1 public class Client32 { 2 static { 3 i = 100; 4 } 5 6 public static int i = 1; 7 8 public static void main(String[] args) { 9 System.out.println(i); 10 } 11 }
注意變量 i 的聲明和賦值調換了位置,如今的問題是:這段程序可否編譯?如過能夠編譯,輸出是多少?還要注意,這個變量i但是先使用(也就是賦值)後聲明的。
答案是:能夠編譯,沒有任何問題,輸出結果爲1。對,輸出是 1 不是100.僅僅調換了位置,輸出就變了,並且變量 i 仍是先使用後聲明的,難道顛倒了?
這要從靜態變量的誕生提及,靜態變量是類加載時被分配到數據區(Data Area)的,它在內存中只有一個拷貝,不會被分配屢次,其後的全部賦值操做都是值改變,地址則保持不變。咱們知道JVM初始化變量是先聲明空間,而後再賦值,也就是說:在JVM中是分開執行的,等價於:
int i ; //分配空間
i = 100; //賦值
靜態變量是在類初始化的時候首先被加載的,JVM會去查找類中全部的靜態聲明,而後分配空間,注意這時候只是完成了地址空間的分配,尚未賦值,以後JVM會根據類中靜態賦值(包括靜態類賦值和靜態塊賦值)的前後順序來執行。對於程序來講,就是先聲明瞭int類型的地址空間,並把地址傳遞給了i,而後按照類的前後順序執行賦值操做,首先執行靜態塊中i = 100,接着執行 i = 1,那最後的結果就是 i =1了。
哦,如此而已,若是有多個靜態塊對 i 繼續賦值呢?i 固然仍是等於1了,誰的位置最靠後誰有最終的決定權。
有些程序員喜歡把變量定義放到類最底部,若是這是實例變量還好說,沒有任何問題,但若是是靜態變量,並且還在靜態塊中賦值了,那這結果就和指望的不同了,因此遵循Java通用的開發規範"變量先聲明後賦值使用",是一個良好的編碼風格。
注意:再次重申變量要先聲明後使用,這不是一句廢話。
咱們知到在Java中能夠經過覆寫(Override)來加強或減弱父類的方法和行爲,但覆寫是針對非靜態方法(也叫作實例方法,只有生成實例才能調用的方法)的,不能針對靜態方法(static修飾的方法,也叫作類方法),爲何呢?咱們看一個例子,代碼以下:
1 public class Client33 { 2 public static void main(String[] args) { 3 Base base = new Sub(); 4 //調用非靜態方法 5 base.doAnything(); 6 //調用靜態方法 7 base.doSomething(); 8 } 9 } 10 11 class Base { 12 // 我是父類靜態方法 13 public static void doSomething() { 14 System.out.println("我是父類靜態方法"); 15 } 16 17 // 父類非靜態方法 18 public void doAnything() { 19 System.out.println("我是父類非靜態方法"); 20 } 21 } 22 23 class Sub extends Base { 24 // 子類同名、同參數的靜態方法 25 public static void doSomething() { 26 System.out.println("我是子類靜態方法"); 27 } 28 29 // 覆寫父類非靜態方法 30 @Override 31 public void doAnything() { 32 System.out.println("我是子類非靜態方法"); 33 } 34 }
注意看程序,子類的doAnything方法覆寫了父類方法,真沒有問題,那麼doSomething方法呢?它與父類的方法名相同,輸入、輸出也相同,按道理來講應該是覆寫,不過究竟是不是覆寫呢?咱們看看輸出結果: 我是子類非靜態方法 我是父類靜態方法
這個結果很讓人困惑,一樣是調用子類方法,一個執行了父類方法,二者的差異僅僅是有無static修飾,卻獲得不一樣的結果,緣由何在呢?
咱們知道一個實例對象有兩個類型:表面類型(Apparent Type)和實際類型(Actual Type),表面類型是聲明的類型,實際類型是對象產生時的類型,好比咱們例子,變量base的表面類型是Base,實際類型是Sub。對於非靜態方法,它是根據對象的實際類型來執行的,也就是執行了Sub類中的doAnything方法。而對於靜態方法來講就比較特殊了,首先靜態方法不依賴實例對象,它是經過類名來訪問的;其次,能夠經過對象訪問靜態方法,若是是經過對象訪問靜態方法,JVM則會經過對象的表面類型查找靜態方法的入口,繼而執行之。所以上面的程序打印出"我是父類非靜態方法",也就不足爲奇了。
在子類中構建與父類方法相同的方法名、輸入參數、輸出參數、訪問權限(權限能夠擴大),而且父類,子類都是靜態方法,此種行爲叫作隱藏(Hide),它與覆寫有兩點不一樣:
(1)、表現形式不一樣:隱藏用於靜態方法,覆寫用於非靜態方法,在代碼上的表現是@Override註解可用於覆寫,不可用於隱藏。
(2)、職責不一樣:隱藏的目的是爲了拋棄父類的靜態方法,重現子類方法,例如咱們的例子,Sub.doSomething的出現是爲了遮蓋父類的Base.doSomething方法,也就是i指望父類的靜態方法不要作破壞子類的業務行爲,而覆寫是將父類的的行爲加強或減弱,延續父類的職責。
解釋了這麼多,咱們回頭看看本建議的標題,靜態方法不能覆寫,能夠再續上一句話,雖然不能覆寫,但能夠隱藏。順便說一下,經過實例對象訪問靜態方法或靜態屬性不是好習慣,它給代碼帶來了"壞味道",建議你們閱之戒之。
咱們知道經過new關鍵字生成的對象必然會調用構造函數,構造函數的簡繁狀況會直接影響實例對象的建立是否繁瑣,在項目開發中,咱們通常都會制定構造函數儘可能簡單,儘量不拋異常,儘可能不作複雜運算等規範,那若是一個構造函數確實複雜了會怎麼樣?咱們開看一段代碼:
1 public class Client34 { 2 public static void main(String[] args) { 3 Server s= new SimpleServer(1000); 4 } 5 } 6 7 abstract class Server { 8 public final static int DEFAULT_PORT = 40000; 9 10 public Server() { 11 // 得到子類提供的端口號 12 int port = getPort(); 13 System.out.println("端口號:" + port); 14 /* 進行監聽動做 */ 15 } 16 17 // 由子類提供端口號,並做可用性檢查 18 protected abstract int getPort(); 19 } 20 21 class SimpleServer extends Server { 22 private int port = 100; 23 24 // 初始化傳遞一個端口號 25 public SimpleServer(int _port) { 26 port = _port; 27 } 28 29 // 檢查端口是否有效,無效則使用默認端口,這裏使用隨機數模擬 30 @Override 31 protected int getPort() { 32 return Math.random() > 0.5 ? port : DEFAULT_PORT; 33 } 34 35 }
該代碼是一個服務類的簡單模擬程序,Server類實現了服務器的建立邏輯,子類要在生成實例對象時傳遞一個端口號便可建立一個監聽端口的服務,該代碼的意圖以下:
貌似很合理,再仔細看看代碼,確實與咱們的意圖相吻合,那咱們嘗試屢次運行看看,輸出結果要麼是"端口號:40000",要麼是"端口號:0",永遠不會出現"端口號:100"或是"端口號:1000",這就奇怪了,40000還好說,那個0是怎麼冒出來的呢?怠慢什麼地方出現了問題呢?
要解釋這個問題,咱們首先要說說子類是如何實例化的。子類實例化時,會首先初始化父類(注意這裏是初始化,不是生成父類對象),也就是初始化父類的變量,調用父類的構造函數,而後纔會初始化子類的變量,調用子類的構造函數,最後生成一個實例對象。瞭解了相關知識,咱們再來看看上面的程序,其執行過程以下:
終於清楚了,在類初始化時getPort方法返回值尚未賦值,port只是得到了默認初始值(int類型的實例變量默認初始值是0),所以Server永遠監聽的是40000端口(0端口是沒有意義的)。這個問題的產生從淺處說是類元素初始順序致使的,從深處說是由於構造函數太複雜引發的。構造函數用做初始化變量,聲明實例的上下文,這都是簡單實現的,沒有任何問題,但咱們的例子卻實現了一個複雜的邏輯,而這放在構造函數裏就不合適了。
問題知道了,修改也很簡單,把父類的無參構造函數中的全部實現都移動到一個叫作start的方法中,將SimpleServer類初始化完畢,再調用其start方法便可實現服務器的啓動工做,簡潔而又直觀,這也是大部分JEE服務器的實現方式。
注意:構造函數簡化,再簡化,應該達到"一眼洞穿"的境界。
構造函數是一個類初始化必須執行的代碼,它決定着類初始化的效率,若是構造函數比較複雜,並且還關聯了其它類,則可能產生想不到的問題,咱們來看以下代碼:
1 public class Client35 { 2 public static void main(String[] args) { 3 Son son = new Son(); 4 son.doSomething(); 5 } 6 } 7 8 // 父類 9 class Father { 10 public Father() { 11 new Other(); 12 } 13 } 14 15 // 相關類 16 class Other { 17 public Other() { 18 new Son(); 19 } 20 } 21 22 // 子類 23 class Son extends Father { 24 public void doSomething() { 25 System.out.println("Hi, show me Something!"); 26 } 27 }
這段代碼並不複雜,只是在構造函數中初始化了其它類,想一想看這段代碼的運行結果是什麼?會打印出"Hi ,show me Something!"嗎?
答案是這段代碼不能運行,報StatckOverflowError異常,棧(Stack)內存溢出,這是由於聲明變量son時,調用了Son的無參構造函數,JVM又默認調用了父類的構造函數,接着Father又初始化了Other類,而Other類又調用了Son類,因而一個死循環就誕生了,知道內存被消耗完中止。
你們可能以爲這樣的場景不會出如今開發中,咱們來思考這樣的場景,Father是由框架提供的,Son類是咱們本身編寫的擴展代碼,而Other類則是框架要求的攔截類(Interceptor類或者Handle類或者Hook方法),再來看看問題,這種場景不可能出現嗎?
可能你們會以爲這樣的場景不會出現,這種問題只要系統一運行就會發現,不可能對項目產生影響。
那是由於咱們這裏展現的代碼比較簡單,很容易一眼洞穿,一個項目中的構造函數可不止一兩個,類之間的關係也不會這麼簡單,要想瞥一眼就能明白是否有缺陷這對全部人員來講都是不可能完成的任務,解決此類問題最好的辦法就是:不要在構造函數中聲明初始化其餘類,養成良好習慣。