通过Java字节码发现有趣的内幕之String篇--引用相等性

下面的小程序用来判断两个字符串引用变量是否相等:

public class TestString {

   public static void main(String[] args) {

        String str1=new String("Hello Java!");

        String str2=str1;

        String str3="Hello Java!";

        String str4="Hello Java!";

       

        System.out.println(str1==str2);

        System.out.println(str1==str3);

        System.out.println(str3==str4);

   }

}

输出结果:

true

false

true

 解析:

首先引用str1指向了一个内容为“Hello Java!”的字符串对象,然后把str1赋给str2,所以打印判断str1==str2是true的因为他们存储同一个地址值,接着隐含的创建了了字符串对象str3虽然st1与str3内容相同,但是他们是不同的对象,地址不同,所以打印str1= =str3结果是false。最后以同样的方式创建了字符串str4为什么判断str3==str4是true呢?由于字符串在程序中经常用到,Java为了加快程序的执行速度,把隐式创建的字符串对象放在栈中一个特殊区域—字符串池(String Pool)中,相同内容的字符串对象只保留一份,用引号新产生字符串对象时先从字符串池中寻找是否已经存在,若已经存在就取出来直接使用。而用new创建的字符串对象即使内容都是”Hello Java!”他们也是不同的对象实例,在内存中占不同的空间。

一、java中的基本数据类型(int、double、short、long、byte、float、boolean、char)判断是否相等,直接使用"=="就行了,相等返回true,否则,返回false。

二、但是java中的引用类型的对象比较变态,假设有两个引用对象obj1,obj2,

obj1==obj2 判断是obj1,obj2这两个引用变量是否相等,即它们所指向的对象是否为同一个对象。言外之意就是要求两个变量所指内存地址相等的时候,才能返回true,每个对象都有自己的一块内存,因此必须指向同一个对象才返回ture。

三、如果想要自定义两个对象(不是一个对象,即这两个对象分别有自己的一块内存)是否相等的规则,那么必须在对象的类定义中重写equals()方法,如果不重写equals()方法的话,默认的比较方式是比较两个对象是否为同一个对象。

在Java API中,有些类重写了equals()方法,它们的比较规则是:当且仅当该equals方法参数不是 null,两个变量的类型、内容都相同,则比较结果为true。这些类包括:String、Double、Float、Long、Integer、Short、Byte、、Boolean、BigDecimal、BigInteger等等,太多太多了,但是常见的就这些了,具体可以查看API中类的equals()方法,就知道了。



很多时候我们在编写Java代码时,判断和猜测代码问题时主要是通过运行结果来得到答案,本博文主要是想通过Java字节码的方式来进一步求证我们已知的东西。这里没有对Java字节码知识进行介绍,如果想了解更多的Java字节码或对其感兴趣的朋友可以先阅读字节码基础:JVM字节码初探

String字面量可以通过’==’判断两个字符串是否相同,是因为大家都知道’==’是用来判断两个对象的值引用地址是否一致,两个值一样的字符串字面量定义是否指向同一个值内存地址呢?答案是肯定的。

1
2
3
4
5
6
7
8
9
10
package com.jaffa.test.string;
 
public class ConstPoolTest {
     public static void main(String[] args){
         String str1 = "strVal_1" ;
         String str2 = "strVal_1" ;
         //print str1==str2 is true
         System.out.printf( "str1==str2 is %b" ,str1==str2);
     }
}

代码中声明了str1和str2的字面量值都为strVal_1,并且打印出str1==str2为true,说明两个str1和str2变量同时指向同一个字符串常量值的内存地址,下面通过Java字节码来验证这个结果。

在命令行我们通过javap工具来查看一个class文件的字节码。

1
javap -v com.jaffa.test.string.ConstPoolTest

在Constant pool列表中看到#16为一个String类型并值指向#17,而#17是一个utf8字符集编码值为strVal_1,所以#16和#17最终表达就是在常量池中有个String类型值为strVal_1的常量数据。

那接下来需要确认str1和str2两个变量值是否都是指向#16呢?

1
2
3
4
5
//ldc表示将一个常量加载到操作数栈
  0 : ldc           # 16     //将#16对应的常量值加载到操作数栈中           
  2 : astore_1              //将当前操作数栈中赋于变量1,即str1
  3 : ldc           # 16     //再次将#16对应的常量值加载到操作数栈中
  5 : astore_2              //将当前操作数栈中赋于变量2,即str2

从上面字节码执行来看,str1和str2都是被赋于同一个常量值,由此可以得出两个变更指向同一个内存地址。

通过同样的方式,我们来看一下如果是非字面量的情况会是怎么样的,Java代码如下:

1
2
3
4
5
6
7
8
9
10
package com.jaffa.test.string;
 
public class ConstPoolTest {
     public static void main(String[] args){
         String str1 = "strVal_1" ;
         String str2 = new String( "strVal_1" );
 
         System.out.printf( "str1==str2 is %b" ,str1==str2);
     }
}

上面代码输出结果为false,通过javap查看发现str2变量的字节码指令发生了变化,如下现两截图:

1
2
3
4
5
6
7
8
//ldc表示将一个常量加载到操作数栈
  0 : ldc           # 16     //将#16对应的常量值加载到操作数栈中           
  2 : astore_1              //将当前操作数栈中赋于变量1,即str1
  3 : new           # 18     //创建了一个类实例,#18指向一个实例类型String
  6 : dup                   //配置上行完成操作栈指令
  7 : ldc           # 16     //将#16对应的常量值加载到操作数栈中
  9 : invokespecial # 20     //调用String实例初始化方法,并将#16输入
12 : astore_2              //将new出来的实例赋于变量2,即str2

这时str2变量是创建一个新的内存地址,而非直接指向#16常量内存地址,所以str1==str2的结果为false。同时可以看到通过new String()带来看更多的字节码指令操作,运行上花费了更多的系统资源。



猜你喜欢

转载自blog.csdn.net/hellojoy/article/details/80266965