程序猿二三事之Java基礎--Java SE 5增加的特性--語法篇(一)

為什么是Java SE 5?

目前已經到了JDK-8u74了,JDK7的主版本已經于2015年4月停止公開更新。

那為什么還要來說Java/JDK5呢?

Java SE在1.4(2002)趨于成熟,隨著越來越多應用于開發企業應用,許多框架誕生于這個時期或走向成熟。
Java SE 5.0的發布(2004)在語法層面增加了很多特性,讓開發更高效,代碼更整潔。

  • 自動裝箱/拆箱、泛型、注解、for循環增強、枚舉、可變參數等新特性讓你的小手指少敲了不少代碼,可以寫更優雅的實現;
  • API提供并發庫大大減少并發編程的難度;
  • 虛擬機層面改進了內存模型,增加虛擬機監控和管理相關的api和工具等等。

但是,<font color=red>語法層面的改變對應于JVM卻沒有多大變化,只是編譯器在編譯字節碼時偷偷做了手腳。</font>
所以我們應該了解下到底編譯器干了啥壞事,有助于寫更合理的代碼,少踩坑,掉陷阱里也得知道怎么掉的。

另外原因,目前從各種各樣的項目代碼看,其實多數開發人員常用的還是Java SE 5.0 的特性,甚至習慣用Java SE 1.4及以前的語法特性。
學java也有幾年了,許多特性也知道個一二,但是要寫下來,還是得查閱不少文章,很多東西欠缺完整性和系統性。
碼農寫文章(更合理說是整理資料)也是一個學習的過程。

學習一門語言,一旦實際應用于實際開發中,了解背后的原理和理念,深入了解語言的特點,有好處沒壞處。

注:javac XXXXX.java 編譯命令
javap -c XXXXX 反編譯命令
-c 反編譯
-s 輸出內部類型簽名 需要看方法簽名時 要加上這個參數
-v 輸出附加信息 會輸出比較多信息 包括常量表 line number table 等信息, 但沒有-s的輸出內容

一、自動裝箱/拆箱

1、包裝類型(存在于Java 1.5之前)

Java中,類型分成兩大類,基本類型(Primitive Type)和引用類型(Reference Type)。基本類型是內定的,有確定的取值范圍,值占有確定的內存空間。

有八大基本類型,分成兩個浮點類型(float、double),五個整型(byte, short, int, long,char), 一個布爾型(boolean)。
沒看錯char也是整型,在語言規范中說明,char是一個16bit無符號整形,用來表示一個UTF-16編碼的單元(在Java5中對應Unicode4.0,Java8中對應Unicode6.2)。

基本類型的值不是對象,最基本的對象(Object)方法(toString, hashCode, getClass, equals等)也不能調用。

為了把基本類型當引用類型來用,具備對象的特質,JDK中定義了各種基本類型相對應的包裝類。
所謂裝箱,就是將基本類型的值包裝成(轉換-conversion)對應的包裝類型的對象,拆箱,就是講包裝類型的對象,轉換成基本類型的值。

裝箱和拆箱:

Integer i = 100;
int j = new Integer(250);
基本類型 大小 數值范圍 包裝類型 默認值
boolean --- true, false Boolean false
byte 1字節(8bit) -2^7 -- 2^7-1 Byte 0
char 2字節(16bit) \u0000--\uffff Character \u0000
short 2字節(16bit) -2^15 -- 2^15-1 Short 0
int 4字節(32bit) -2^31 -- 2^31-1 Integer 0
long 8字節(64bit) -2^63 -- 2^63-1 Long 0
float 4字節(32bit) IEEE754 Float 0.0f
double 8字節(64bit) IEEE754 Double 0.0d

2、自動裝箱/拆箱背后

前面說了,語法特性的改變并沒改變JVM的實現方式,那么我們可以看看背后編譯器到底干了啥事情。
下面代碼和編譯后的反編譯結果:

       public void boxUnBox(){
              Integer i = 100;
               int j = new Integer(250);
       }
enter description here
enter description here

反編譯結果可以看到,以上代碼實際等同于以下代碼的編譯結果:

       public void boxUnBox(){
              Integer i = Integer. valueOf(100);
              
              Integer t = new Integer(250);
               int j = t .intValue();
       }

八大基本類型的裝箱操作都調用的是valueOf方法,拆箱操作調用各自賭贏的xxxValue()方法,有興趣可以試試。

3、注意==比較的陷阱

在java中,計算類型的運算符,
先來看下比較的代碼編譯結果:

       public void boxUnBoxCMP(){             
               Integer i = 100; 
               int j = new Integer(250);
               if(j == i ){}
    
               Integer h = new Integer(100);
               Integer k = new Integer(100);
               if(h == k ){}    
       }
enter description here
enter description here

==第一個紅框==是if(j == i ) 的反編譯代碼
從上面的反編譯結果可以看出,包裝類型的單目運算符計算其實是需要通過拆箱=>計算=>裝箱實現的,
而雙目運算符的運算也是需要將包裝類型轉換成基本類型,然后再參與運算。

但是,== 的比較要牢記它的本質,如果==比較兩邊都是引用類型,那么比較的是引用地址,如果其中一邊是基本類型,那么非引用類型的值將轉換成基本類型再做比較。
==第二個紅框==中是引用比較,沒有轉換。

4、Cache帶來的坑

我們看看自動裝箱的valueOf的代碼吧

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache. high)
            return IntegerCache.cache[i + (-IntegerCache. low)];
        return new Integer(i);
    }

一眼就可以看到IntegerCache這個玩意,完整代碼(JDK1.8的代碼)如下:

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            // 根據配置獲取緩存最大值,最大值配置范圍 127 < h < Integer.MAX_VALUE-129
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt( integerCacheHighPropValue);
                    i = Math. max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math. min(i, Integer.MAX_VALUE - (- low) -1);
                } catch( NumberFormatException nfe ) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h ;
            cache = new Integer[(high - low) + 1];// 也許有人會疑惑為什么會有個+1,其實就是0這個數占了個坑
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k ] = new Integer(j++);
            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache. high >= 127;
        }

        private IntegerCache() {}
    }

IntegerCache的意思就是將low到high的值先緩存起來,low恒定是-128, high默認是127,可以配置成127<= high <= Interger.MAX_VALUE-129
注意緩存的是Integer對象,所以是引用對象。既然是引用對象,那么==的比較就會有問題了。

        public static void trap(){
              Integer i = 100;
              Integer k = 100;
              
               if(i == k ){System.out.println( "i == k");}
              
              Integer j = 500;
              Integer h = 500;
               if(j == h ){System.out.println( "j == h");}else { System.out.println("j != h" );}
              
       }

輸出結果是什么呢?
i == k
j != h

因為i和k都是從IntegerCache中取得的緩存對象,引用是一樣的,j和h沒有緩存,必須valueOf必須重新new一個Integer對象,所以引用是不等的。

類型Byte、Short、Long和Integer類似,只是沒有可配置的最大緩存值,Byte所有值都被緩存了,所以不存在==的坑。
Character緩存的是0~127。
Float和Double沒有緩沖,也沒辦法緩存。

5、建議

  • 不會參與運算的用包裝, 比如數據庫自增的記錄ID,用Long類型
  • 參與運算的,如果計算復雜,盡量先轉成基本類型,計算后再轉回對應的包裝類對象;特別是頻繁的單目運算符,如循環中的自增自減
  • 參與比較,注意包裝類的cache坑
  • 記得所有集合中只能存對象類型,基本類型都是經過裝箱/拆箱的

舉個不好的例子吧:

        public static Long bad(List<Integer> list){
              Long sum = 0L;
               for(Integer i : list ){
                      if(i % 2 == 0 ){
                            sum += i;
                     } else {
                            sum += i * 2;        
                     }
              }
               return sum ;
       }

有興趣的童鞋可以反編譯看下,類似于以下代碼完成的事情:

        public static Long badOrigin(List<Integer> list){
              Long sum = Long. valueOf(0L);
              Iterator<Integer> it = list.iterator();
              Integer value = null;
               long sumTmp = 0L;
               while(it .hasNext()){
                      value = it.next();
                      if(value .intValue() % 2 == 0){
                            sumTmp = sum.longValue();
                            sumTmp = sumTmp+ value.intValue();
                            sum = Long. valueOf(sumTmp);
                     } else {
                            sumTmp = sum.longValue();
                            sumTmp = sumTmp+ value.intValue()*2;
                            sum = Long. valueOf(sumTmp);
                     }
              }
               return sum ;
       }

按照建議來,可以改成以下代碼:

        public static Long good(List <Integer> list ){
               long sum = 0L;
               int value = 0;
               for(Integer i : list ){
                      value = i.intValue();
                      if(value % 2 == 0 ){
                            sum += value;
                     } else {
                            sum += value * 2;    
                     }
              }
               return sum ;
       }

以上反編譯下看看字節碼,是不是清爽多了^^

二、for循環增強

for循環增強也是1.5里的一個語法糖,讓大家寫for循環更加便利,再加上IDE的代碼模板,非常方便

1、先看看List的for循環增強怎么寫:

        public void iteratorForeach(){
              List<String> list = new ArrayList<String>();
               for (String str : list ) {
              }
       }

反編譯結果如下,可以看出,其實就是調用Iterable接口的iterator方法,獲得一個迭代器(Iterator), 利用迭代器進行遍歷所有數據。
從這里也可以推出,只要實現Iterable接口的類型,都可以在for循環增強中使用:

enter description here
enter description here

比如自己實現一個只有add方法,只能通過iterator遍歷的List:

        public void myListForeach(){
              MyList<String> myList = new MyList<>();
               for (String str : myList ) {
                     
              }
        }
       
        public static class MyList<V> implements Iterable<V>{
               private List<V> datas = new ArrayList<>();
              
               public void add(V data ){
                      datas.add( data);
              }
              
               @Override
               public Iterator<V> iterator() {
                      final Iterator<V> it = datas .iterator();
                      return new Iterator<V>() {
                            @Override
                            public boolean hasNext() {
                                   return it .hasNext();
                           }
                            @Override
                            public V next() {
                                   return it .next();
                           }
                     };
              }      
       }

2、再看看數組類型的for循環增強怎么寫:

        public void arrayForeach(){
              String[] strs = new String[10];
               for (String str : strs ) {
                     
              }
              
              System. out.println();
              String str = null ;
               for(int i = 0; i < strs .length ; i ++){ //傳統for循環寫法
                      str = strs[ i];
              }
       }

跟傳統for循環相比,數組的for增強循環更加簡潔,從反編譯代碼中也可以看出,用到的指令序列基本上是一樣的。

enter description here
enter description here

3、不適應的地方

這么好的東西什么情況下用不了呢? 主要是for增強循環中沒能得到下標也沒能得到iterator對象引用導致的。
第一種是數組或者List集合類型,需要用到下標的情況;
第二種是需要調用到Iterator接口的remove方法的情況;

三、可變參數

Java SE 5.0中增加了可變參數特性,對于以往用數組表示的參數可以調整到最后一個參數,作為可變參數定義,
調用方省去顯示創建數組,可空數組可以直接可以省略:

    public static void varargs(String s, String... ss) {
    }

    public static void main(String[] args) {
        varargs("aaa" );
        varargs("aaa" , "bbb" );
        varargs("aaa" , "bbb" , "ccc" , "ddd" );
        varargs("", null) ;
        varargs("aaa" , new String[]{"abc", "ccc", "ddd" });
    }

可變參數背后編譯器也是創建一個數組來傳遞參數的,可以方編譯以上代碼, varargs的方法簽名中第二個參數就是一個string數組:

enter description here
enter description here

==注意事項:==

  • 不能有多個可變參數,并且只能是最后一個參數;
  • 因為可變參數是由數組實現的,調用方忽略可變參數時,可變參數為空數組;但是既然是數組,就可以設置成null,所以要注意空判斷;
  • 如果被調用的方法,既匹配了可變參數方法,有匹配了固定參數方法,固定參數方法將被調用;
  • 盡量避免可變參數方法的重載(overload):
    • 可變參數類型與前一個參數的類型一樣時,與只有可變參數類型方法重載沖突,會導致調用不明確;
    • 可變參數類型不同,但可變參數為空時,可以省略,或者設置成null,都會導致被調用方法不明確;
    • 可變參數類型是基本類型或包裝類型,重載會因為自動裝箱/拆箱導致調用不明確
  • override的方法參數類型和形式必須一致,不能將可變參數改成數組,雖然背后實現是一樣的;
    /**不能有多個可變參數,并且只能是最后一個參數**/
    public static void varargs10(Object ... objs, String abc){ //編譯出錯
    }
    public static void varargs11(String abc, Object ... objs){
    }

    /**因為可變參數是由數組實現的,調用方忽略可變參數時,可變參數為長度為0的數組;但是既然是數組,就可以設置成null,所以要注意null判斷;**/
    public static void varargs2Test(){
        varargs2();
        varargs2(null); //NullPointerException
    }
   
    public static void varargs2(String...strs){
       //strs 可能為null, 應該做 strs是否為空的判斷
       for (String str : strs ) {
        }
    }
   
    /**如果被調用的方法,既匹配了可變參數方法,有匹配了固定參數方法,固定參數方法將被調用;**/
    public static void varargs3Test(){
       varargs3(11, 22); //varargs30
    }

    public static void varargs3(int i, int j ){
       System. out.println("varargs30" );
    }
    public static void varargs3(int i , int... arr){
       System. out.println("varargs31" );
    }
   
    /**可變參數類型與前一個參數的類型一樣時,與只有可變參數類型方法重載沖突,會導致調用不明確;**/
    public static void varargs4Test(){
       varargs4("abc" , "def" , "ijk" ); //編譯出錯
    }
    public static void varargs4(String...strs){
    }
    public static void varargs4(String str, String... strs){
    }
   
    /**可變參數類型不同,但可變參數為空時,可以省略,或者設置成null,都會導致被調用方法不明確;**/
    public static void varargs5Test(){
       varargs5();   //編譯出錯
       varargs5("abc" , null); //編譯出錯
    }
    public static void varargs5(String str, String... strs){
    }
    public static void varargs5(String str, Integer... datas){
    }
   
    /**可變參數類型是基本類型或包裝類型,重載會因為自動裝箱/拆箱導致調用不明確**/
    public static void varargs6Test(){
       varargs6("abc" , 1, 2, 3); //編譯出錯
    }
    public static void varargs6(String str, int... datas){
    }
    public static void varargs6(String str, Integer... datas){
    }
   
    /**override的方法參數類型和形式必須一致,不能將可變參數改成數組,雖然背后實現是一樣的**/
    public static void varargs7Test(){
       Sub sub = new Sub();
       
       Base base = sub;
       base.varargs7( "abc", "def" );
       base.varargs7();
       
       sub.varargs7();  //編譯錯誤
       sub.varargs7("abc" , "def" ); //編譯錯誤
    }
   
    public static interface Base {
       public void varargs7(String...strs );
    }
   
    public static class Sub implements Base{
       @Override
       public void varargs7(String[] strs) {
              System. out.println("varargs7" );
       }
    }

四、StringBuilder和字符串+(非1.5特性,順便提一下而已)

JDK 5.0中增加了StringBuilder, 基本上和StringBuffer一樣,但去掉了所有synchronized同步關鍵字。
性能上StringBuilder優于StringBuffer, 所以非并發情況下使用StringBuilder沒商量。

Java中對象沒有參與運算符運算的可能,也沒有提供像C++那樣重載運算符語法支持,不要被String的+操作欺騙了。
Java1.4中,字符串的+操作在編譯器生成的字節碼可以看到使用的是StringBuffer進行append,
Java5.0中,+操作改成StringBuilder的append:

       public static void sbTest(String s1, String s2){
              String str = s1 +s2 ;
       }
enter description here
enter description here
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 前言 人生苦多,快來 Kotlin ,快速學習Kotlin! 什么是Kotlin? Kotlin 是種靜態類型編程...
    任半生囂狂閱讀 26,252評論 9 118
  • 轉自:http://blog.csdn.net/jackfrued/article/details/4492194...
    王帥199207閱讀 8,588評論 3 93
  • ? 欣還在家啊,讀書認識幾個字就可以了,女孩子家家的,過幾年嫁人就可以了,我Y頭在南城打工,吃的好穿的好,要不你也...
    李慧英_c3a3閱讀 398評論 2 0
  • 幾乎察覺,母親的鬢邊就生長了稀疏的白發;幾乎沒有察覺,父親的腳步就變得蹣跚;幾乎沒有察覺我的孩子已然長大;幾乎沒有...
    東方地秀閱讀 237評論 6 6
  • 生命的存在和延續,是因為身體本來有心臟跳動及肺功能?生理的呼吸。得以讓整個生命系統的正常運轉和發育.......。...
    劉礦山閱讀 398評論 0 0