關於泛型——java自動拆箱,裝箱,遍歷循環(foreach)的理解

JAVA中泛型

泛型,即“參數化類型”。一提到參數,最熟悉的就是定義方法時有形參,然後調用此方法時傳遞實參。那麼參數化類型怎麼理解呢?顧名思義,就是將類型由原來的具體的類型參數化,類似於方法中的變量參數,此時類型也定義成參數形式(可以稱之爲類型形參),然後在使用/調用時傳入具體的類型(類型實參)。
泛型的本質是爲了參數化類型(在不創建新的類型的情況下,通過泛型指定的不同類型來控制形參具體限制的類型)。也就是說在泛型使用過程中,操作的數據類型被指定爲一個參數,這種參數類型可以用在類、接口和方法中,分別被稱爲泛型類、泛型接口、泛型方法。

1.java的泛型:

java的泛型,只在程序源碼中存在,在編譯後的字節碼文件中,就已經替換爲原來的原生類型了(raw type,也稱爲:裸類型) 並在相應的地方插入要強制轉型的代碼。對於運行期的java來說,ArrayList< int> ,與ArrayList< String> 就是同一個類,所以泛型技術實際上是java語言的一個語法糖,java語言中泛型實現方法稱爲類型擦除,基於這種方法實現的泛型稱爲僞泛型。

情景一: 僞泛型

泛型擦除前:

public class ParameterizedType {

    public static void main(String[] args) {

       Map<String,String> map = new HashMap<String, String>();

       map.put("1", "OK");

       map.put("2", "hello");

       System.out.println(map.get("1"));

       System.out.println(map.get("2"));

    }

}

使用Java Decompiler (JD-GUI)反編譯,結果如下:
泛型擦除後:

public class ParameterizedType

{

  public static void main(String[] args)

  {

    Map map = new HashMap();

    map.put("1", "OK");

    map.put("2", "hello");

    System.out.println((String)map.get("1"));

    System.out.println((String)map.get("2"));

  }

}

問題來了,這就是泛型嗎?這不是真泛型。

情景二:當泛型遇上重載

在一類裏出現如下代碼,編譯能通過嗎

public void test(List<String> list) {

    }

public void test(List<Integer> list) {

    }

反編譯下:

public void test(List<String> list)
  {
    throw new Error("Unresolved compilation problem: /n/tMethod test(List<String>) has the same erasure test(List<E>) as another method in type ParameterizedType/n");

  }

編譯不通過,報錯了。是因爲:List< String> 和List< Integer> 編譯之後被擦除了,變成了原生類型List< E>,擦除動作導致兩個方法簽名一模一樣,無法重載。
修改下代碼如下:

public void test(List<String> list) {
        return 2;
    }

public int test(List<Integer> list) {

       return 1;

    }

看看反編譯後的代碼先:

  public void test(List<String> list) {
        return 2;
  }

  public int test(List<Integer> list) {

    return 1;

  }

編譯通過了,這是怎麼回事呢,看看修改的那部份代碼

修改內容有:將第二個test方法返回類型爲int,並且添加return語句(只要這個返回值不需要實際中用到就OK)。之所以後面能編譯和執行成功是因爲:兩個method()方法加入了不同的返回值 後才能共存在一個class文件中。方法重載的要求:要求方法具備不同的特徵簽名,返回值並不包含在方法的特徵簽名之中,所以返回值不參與重載選擇,但是在class文件格式中,只要描述符不是完全一致的兩個方法就可以共存。也就是說兩個方法如果有相同的名稱和特徵簽名,但返回值不同,他們就可以共存在一個class文件中。

之後,引入signature,LocalVariableTypeTable,等新屬性用於解決伴隨泛型而來的參數類型的識別問題。

情景三:自動裝箱,自動拆箱與遍歷循環

    public void test(){

       List<Integer> list = Arrays.asList(1,2,3,4);
       // 如果在JDK 1.7中,還有另外一顆語法糖 ,
    // 能讓上面這句代碼進一步簡寫成List<Integer> list = [1, 2, 3, 4];
       int sum = 0;
       for (Integer integer : list) {
           sum +=i;
       }
        System.out.println(sum);
    }

反編譯下: 自動裝箱,拆箱與循環遍歷編譯之後

public static void main(String[] args) {
    List list = Arrays.asList( new Integer[] {
         Integer.valueOf(1),
         Integer.valueOf(2),
         Integer.valueOf(3),
         Integer.valueOf(4) });

    int sum = 0;
    for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) {
        int i = ((Integer)localIterator.next()).intValue();
        sum += i;
    }
    System.out.println(sum);
}

上述代碼一共包含了泛型、自動裝箱、自動拆箱、遍歷循環與變長參數5中語法糖,第二份代碼是他們編譯後的變化。泛型在編譯過程中會進行擦除,將泛型參數去除;自動裝箱、拆箱在變之後被轉化成了對應的包裝盒還原方法,如Integer.valueOf()與Integer.intValue()方法;而遍歷循環則被還原成了迭代器的實現,這也是爲什麼遍歷器循環需要被遍歷的類實現Iterator接口的原因。

情形四:自動裝箱的陷阱

public class Main {
    public static void main(String []args)
    {
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        Integer d = 3;
        Integer e = 321;
        Integer f = 321;
        Long g = 3L;
        System.out.println(c==d);  //true
        System.out.println(e==f);  //false
        System.out.println(c==(a+b));  //true
        System.out.println(c.equals(a+b));  //true
        System.out.println(g==(a+b));  //true
        System.out.println(g.equals(a+b));  //false

    }
  int x = 3;
  long y = 3L;

   //x,y雖然類型不同但是可以直接進行數值比較
   //true
   System.out.println(x == y);
   //包裝類緩存

首先看一下反編譯的結果:

Integer a= Integer.valueOf(1);
Integer b= Integer.valueOf(2);
Integer c= Integer.valueOf(3);
Integer d= Integer.valueOf(3);
Integer e= Integer.valueOf(321);
Integer f= Integer.valueOf(321);
Long g = Long.valueOf(3L);


System.out.println(c== d);
System.out.println(e== f);
System.out.println(e.intValue() == a.intValue() + b.intValue());
System.out.println(e.equals(Integer.valueOf(a.intValue() + b.intValue())));
System.out.println(g.longValue() == a.intValue() + b.intValue());
System.out.println(g.equals(Integer.valueOf(a.intValue() + b.intValue())));

首先注意兩點:

  • “==”運算在不遇到算術運算的情況下不會自動拆箱。
  • equals()方法不處理數據轉型的問題

分析:

  • 爲什麼會出現c == d爲true,e==f爲false的原因?First,Integer c = 3這句代碼的內部實現是Integer c = Integer.valueOf(3),那麼接下我們看一下Integer.valueOf()的方法內部是如何實現的,如下代碼:
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

static final int low = -128;
static final int high = 127;

在Java運行時內存的常量池裏面有一個int類型的常量池,常量池裏數據的大小在-128~127之間。所以c和d都指向常量池裏的同一個數據。Integer c=3;被翻譯成Intger c=Integer.valueOf(3);就是說從緩存裏返回那個3的對象,所以c d是一個對象,而e,f,不在範圍內所以是NEW出來的對象。這個需要主要,裝箱拆箱。

  • 爲什麼g == (a + b)爲true,而g.equals(a + b)爲false?

首先Long.longValue(g) == (a + b)編譯後爲g.longValue() == (long)(Integer.intValue(a) + Integer.intValue(a)),由於包裝類的”==”右邊遇到了運算符,所以對於”==”左邊的Long型會自動拆箱爲long基本數據類型,而右邊首先對a、b進行自動拆箱,相加後自動類型轉換爲long型。所以輸出爲true。
而對於g.equals(a + b)而言,a+b直接自動拆箱進行相加,之後進行裝箱爲Integer類型,不同類型的包裝類不能相互轉換,而Long的equals()方法的不處理數據類型的轉型關係。實現如下:

public boolean equals(Object obj) {
    if (obj instanceof Long) {
        return value == ((Long)obj).longValue();
    }
    return false;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章