现在的位置: 首页 > 综合 > 正文

ZT – Java学习笔记:Java中处理字符串

2018年05月09日 ⁄ 综合 ⁄ 共 5835字 ⁄ 字号 评论关闭

分析一下Java中处理字符串的问题。探讨JVM处理基本数据类型和引用数据类型的机制、String类的equals()方法和intern()方法。

    

@author:ZJ

    

Blog: http://zhangjunhd.blog.51cto.com/ zhangjunhd 51cto技术博客


1.Java中的基本数据类型

    

    Java中有2种数据类型:基本数据类型(在Java中,boolean、byte、short、int、long、char、float、double这八种是基本数据类型)、引用类型。其中,引用类型包括类类型(含数组)、接口类型。 

2.java中栈(stack)与堆(heap)

    在java中内存分为“栈”和“堆”这两种(Stack and Heap).基本数据类型存储在“栈”中,对象引用类型实际存储在“堆”中,在栈中只是保留了引用内存的地址值。

3.基本数据类型在栈中的存储

    基本类型的定义是通过诸如int a = 3; long b = 255L;的形式来定义的,称为自动变量。值得注意的是,自动变量存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在。如int a = 3; 这里的a是一个指向int 类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。

    另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义

int a = 3; 

int b = 3;

    编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址, 然后将a指向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a 与b同时均指向3的情况。

    特别注意的是,这种字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。如上例,我们定义完a与b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有 4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。 

4.关于String str = "abc"的内部工作

    Java内部将此语句转化为以下几个步骤:

①先定义一个名为str的对String类的对象引用变量:String str;

②在栈中查找有没有存放值为"abc"的地址,如果没有,则开辟一个存放字面值为"abc"的地址,接着创建一个新的String类的对象o,并将o 的字符串值指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o。如果已经有了值为"abc"的地址,则查找对象o,并返回o的地址。

③将str指向对象o的地址。

    值得注意的是,一般String类中字符串值都是直接存值的。但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用!

    为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。 

String str1 = "abc";

String str2 = "abc";

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

    注意,我们这里并不用str1.equals(str2);的方式,因为这将比较两个字符串的值是否相等。==号,根据JDK的说明,只有在两个引用都指向了同一个对象时才返回真值。而我们在这里要看的是,str1与str2是否都指向了同一个对象。

    再看以下代码: 

String str1 = "abc";

String str2 = "a"+"bc";

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

    由上面两段代码结果说明,JVM创建了两个引用str1和str2,但只创建了一个对象,而且两个引用都指向了这个对象。

    我们再来更进一步,将以上代码改成:

String str1 = "abc";

String str2 = "abc";

str1 = "bcd";

System.out.println(str1 + "," + str2); //bcd, abc

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

    这就是说,赋值的变化导致了类对象引用的变化,str1指向了另外一个新对象!而str2仍旧指向原来的对象。上例中,当我们将str1的值改为"bcd"时,JVM发现在栈中没有存放该值的地址,便开辟了这个地址,并创建了一个新的对象,其字符串的值指向这个地址。

    事实上,String类被设计成为不可改变(immutable)的类。如果你要改变其值,可以,但JVM在运行时根据新值悄悄创建了一个新对象,然后将这个对象的地址返回给原来类的引用。

    再修改原来代码:

String str1 = "abc";

String str2 = "abc";

str1 = "bcd";

String str3 = str1; 

System.out.println(str3); //bcd

String str4 = "bcd";

System.out.println(str1 == str4); //true 

   str3 这个对象的引用直接指向str1所指向的对象(注意,str3并没有创建新对象)。当str1改完其值后,再创建一个String的引用str4,并指向因str1修改值而创建的新的对象。可以发现,这回str4也没有创建新的对象,从而再次实现栈中数据的共享。

    我们再接着看以下的代码。

String str1 = new String("abc");

String str2 = "abc";

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

    创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。 

String str1 = "abc";

String str2 = new String("abc");

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


    创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。

    以上两段代码说明,只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。

    这里作如下总结(对后文引入intern()方法有帮助): 


String str1 = "abc";                     //共享内容值

String str2 = new String("abc");//不共享内容值 


    我们从另一个角度来分析上面两句语句,从而引入常量池的概念。常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。 


String str1 = "abc";//是字符串常量,它在编译期被确定。

String str2 =new String("abc");

//不是字符串常量,不在编译期确定。

//new String()创建的字符串不放入常量池中。 


5.总结Java中处理字符串的机制

    由上面这些例子可发现JVM处理字符串的机制。Java虚拟机会维护一个内部的滞留字符串对象的列表(唯一字符串的池)来避免在堆内存中产生重复的String对象。当JVM从class文件里加载字符串字面量并执行的时候,它会先检查一下当前的字符串是否已经存在于滞留字符串列表,如果已经存在,那就不会再创建一个新的String对象而是将引用指向已经存在的String对象,JVM会在内部为字符串字面量作这种检查,但并不会为通过new关键字创建的String对象作这种检查。当然你可以明确地使用String.intern()方法强制JVM为通过
new关键字创建的String对象作这样的检查。这样可以强制JVM检查内部列表而使用已有的String对象。

    所以结论是,JVM会内在地为字符串字面量维护一些唯一的String对象,程序员不需要为字符串字面量而发愁,但是可能会被一些通过 new关键字创建的String对象而困扰,不过他们可以使用intern()方法来避免在堆内存上创建重复的String对象来改善Java的运行性能。 


6.Java中处理字符串的相关方法 


6.1 Java.lang.Object对象的equals()源代码 

Public boolean equals(Object obj){ 

Return (this= =obj); 



    显然,当两个变量指向同一个对象时,equals()方法返回true。


6.2 String.equals()的代码 

public boolean equals(Object anObject) {

           if (this == anObject) {

               return true;

           }

           if (anObject instanceof String) {

               String anotherString = (String)anObject;

               int n = count;

               if (n == anotherString.count) {

                      char v1[] = value;

                      char v2[] = anotherString.value;

                      int i = offset;

                      int j = anotherString.offset;

                      while (n-- != 0) {

                          if (v1[i++] != v2[j++]) 

                              return false;

                      }

               return true;

               }

           }

           return false;

    } 

    由此,可发现String.equals() 方法比较二者的内容,是一个个的比较的。它不同与java.lang.Object的equals()方法,它仅仅比较两个对象的引用。 

6.3 String的intern()方法

    存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法;当一个String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用;看下面代码: 


String s0= “abc”;

String s1=new String(”abc”); 

String s2=new String(“abc”); 


System.out.println( s0==s1 ); 


s1.intern(); 

s2=s2.intern(); //把常量池中“abc”的引用赋给s2 


System.out.println( s0==s1); 

System.out.println( s0==s1.intern() ); 

System.out.println( s0==s2 ); 


    结果为: 

false       

false       //虽然执行了s1.intern(),但它的返回值没有赋给s1 

true       //说明s1.intern()返回的是常量池中”abc”的引用 

true       ////说明s2.intern()返回的是常量池中”abc”的引用 


    回到前文引出常量池定义的部分:

String str1 = "abc";//是字符串常量,它在编译期被确定。

String str2 =new String("abc");

//不是字符串常量,不在编译期确定。

//new String()创建的字符串不放入常量池中。

    看下面代码: 

String s1=new String("abc"); 

String s2=s1.intern(); 


System.out.println( s1==s1.intern() ); 

System.out.println( s1+" "+s2 ); 

System.out.println( s2==s1.intern() ); 


    结果: 

false    //说明原来的“abc”仍然存在

abc abc

true    // s2现在为常量池中“abc”的地址,所以有s2==s1.intern()为true 


    当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。这是Java api文档中关于intern 方法的定义。

    回到上面的代码示例。在这个类中一开始,我们没有声名一个”abc”常量,所以常量池中一开始是没有”abc”。当我们调用s1.intern()后就在常量池中新添加了一个”abc”常量,原来的不在常量池中的”abc”,即s1(表现为存储地址)仍然存在,所以s1==s1.intern()返回flase。 

7.参考资料

⑴Sarkuya,关于Java栈与堆的思考, http://blog.csdn.net/tanghw

⑵[翻译]提高String和StringBuffer性能的技巧,http://blog.csdn.net/wingtrace

⑶JDK5.0 api

抱歉!评论已关闭.