深度剖析java开发手册纪实一

【强制】所有整型包装类对象之间值的比较,全部使用 equals 方法比较。

说明:对于 Integer var = ? 在 - 128 至 127 范围内的赋值,Integer 对象是在 IntegerCache.cache 产 生,会复用已有对象,这个区间内的 Integer 值可以直接使用 == 进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。

为什么这么说呢?

先看看这个题:Integer var = ? 会缓存 -128 到 127 之间的赋值?

public class Test {
	    public static void main(String[] args) 
	    {
	    	Integer a = 100, b = 100, c = 150, d = 150;
	  	    System.out.println(a == b);
	  	    System.out.println(c == d);
	  	    System.out.println(a.equals(b));
	  	    System.out.println(c.equals(d));
	    }
}

以上的答案是多少呢?你答对了么?

true
false
true
true

那么经过代码规约之后呢?看似正确的代码会提示错误么?

WHY?WHAT?别急!

我们反编译之后呢?

Compiled from "Test.java"
public class test.Test {
  public test.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: bipush        100
       2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       5: astore_1
       6: bipush        100
       8: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      11: astore_2
      12: sipush        150
      15: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      18: astore_3
      19: sipush        150
      22: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      25: astore        4
      27: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      30: aload_1
      31: aload_2
      32: if_acmpne     39
      35: iconst_1
      36: goto          40
      39: iconst_0
      40: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V
      43: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      46: aload_3
      47: aload         4
      49: if_acmpne     56
      52: iconst_1
      53: goto          57
      56: iconst_0
      57: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V
      60: return
}

编译源代码: javac Test.java

对编译后的类文件进行反汇编: javap -c Test.class

得到下面反编译的代码:

偏移为 0 的指令为:bipush 100 ,其含义是将单字节整型常量 100 推入操作数栈的栈顶;

偏移为 2 的指令为:invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 表示调用一个 static 函数,即 java.lang.Integer#valueOf(int)

偏移为 5 的指令为:astore_1 ,其含义是从操作数栈中弹出对象引用,然后将其存到第 1 个局部变量 Slot 中;

偏移 6 到 25 的指令和上面类似;

偏移为 30 的指令为 aload_1 ,其含义是从第 1 个局部变量 Slot 取出对象引用(即 a),并将其压入栈;

偏移为 31 的指令为 aload_2 ,其含义是从第 2 个局部变量 Slot 取出对象引用(即 b),并将其压入栈;

偏移为 32 的指令为 if_acmpn,该指令为条件跳转指令,if_ 后以 a 开头表示对象的引用比较。

由于该指令有以下特性:

  • if_acmpeq 比较栈两个引用类型数值,相等则跳转
  • if_acmpne 比较栈两个引用类型数值,不相等则跳转

由于 Integer 的缓存问题,所以 a 和 b 引用指向同一个地址,因此此条件不成立(成立则跳转到偏移为 39 的指令处),执行偏移为 35 的指令。

偏移为 35 的指令: iconst_1,其含义为将常量 1 压栈( Java 虚拟机中 boolean 类型的运算类型为 int ,其中 true 用 1 表示,详见 2.11.1 数据类型和 Java 虚拟机

然后执行偏移为 36 的 goto 指令,跳转到偏移为 40 的指令。

偏移为 40 的指令:invokevirtual #4 // Method java/io/PrintStream.println:(Z)V

可知参数描述符为 Z ,返回值描述符为 V

根据 4.3.2 字段描述符 ,可知 FieldType 的字符为 Z 表示 boolean 类型, 值为 true 或 false
根据 4.3.3 字段描述符 ,可知返回值为 void

因此可以知,最终调用了 java.io.PrintStream#println(boolean) 函数打印栈顶常量即 true

然后比较执行偏移 43 到 57 之间的指令,比较 c 和 d, 打印 false 。

执行偏移为 60 的指令,即 retrun ,程序结束。

上面的指令看懂了么?我们从上述代码中发现 Ineger.valueOf(int) 的确是通过 java.lang.Ineger#valueOf(int) 来构造对象的。

我们再聊聊缓存问题?

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

那么InegerCache里面到底是怎么缓存的呢?

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;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
           // 省略其它代码
    }
      // 省略其它代码
}

这本质上属于空间换时间,预存这部分数据就要占空间,但是下次用的时候就不需要重新创建。 另外体现了对象池设计模式,缓存这部分数据直接复用又避免了重复重复创建。

敲重点:如果想减少内存占用,提高程序运行的效率,可以将常用的对象提前缓存起来,需要时直接从缓存中提取。

另附上JVM指令手册:链接:https://pan.baidu.com/s/10_Pm613_fIlDnUIzGO43ZQ 提取码:h4xn 

不鞋不鞋!
 

参考资料

剖析《阿里巴巴 Java 开发手册》:https://www.imooc.com/read/55

猜你喜欢

转载自blog.csdn.net/qq_28583239/article/details/105547862
今日推荐