关于泛型——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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章