2009年12月22日星期二

[转帖][精品]String in Java

转自:http://hxraid.javaeye.com/blog/522167

作者:Java标准类库有几千个类,唯独String不太一样。为什么这么说?就因为每次上网冲杯Java时,都能看到关于String无休无止的争论。还是觉得有必要让这个讨厌又很可爱的String美眉,赤裸裸的站在我们这些Java色狼面前了。嘿嘿....

众所周知,String是由字符组成的串,在程序中使用频率很高。Java中的String是一个类,而并非基本数据类型。 不过她却不是普通的类哦!!!

 

【镜头1】 String对象的创建
   1、关于类对象的创建,很普通的一种方式就是利用构造器,String类也不例外:
           String s=new String("Hello world");
   问题:参数"Hello world"是什么东西,也是字符串对象吗?莫非用字符串对象创建一个字符串对象???

   2、当然,String类对象还有一种大家都很喜欢的创建方式:
           String s="Hello world";
   问题:有点怪呀,怎么与基本数据类型的赋值操作(int i=1)很像呀???

在开始解释这些问题之前,我们先引入一些必要的知识:
(1) Java class文件结构
       我们都知道,Java程序要运行,首先需要编译器将源代码文件编译成字节码文件(也就是.class文件)。然后在由JVM解释执行。
       class文件是8位字节的二进制流 。这些二进制流的涵义由一些紧凑的有意义的项 组成。比如class字节流中最开始的4个字节组成的项叫做魔数 (magic),其意义在于分辨class文件(值为0xCAFEBABE)与非class文件。class字节流大致结构如下图左侧。

                               

      其中,在class文件中有一个非常重要的项——常量池 。这个常量池专门放置源代码中的常量信息(并且不同的常量存放在不同标志的常量表中)。如上图右侧是HelloWorld代码中的常量表 (HelloWorld代码如下),其中有四个不同类型的常量表(四个不同的常量池入口)。关于常量池的具体细节,请参照《深入Java虚拟机》第二版第 6章。

Java代码
  1. public class HelloWorld{  
  2.     void hello(){  
  3.         System.out.println("Hello world");  
  4.     }  
  5. }  

      显然,HelloWorld代码中的"Hello world"被编译之后,可以清楚的看到存放在了class二进制流的常量池项中(上图右侧红框区域)。并且我们还发现常量池中专门有为String类型设置的常量表 。也就是说,在编译阶段,就已经将代码中的这种("****")形式作为了字符串常量存放在常量池中了 ,这一点和下面代码中出现的整形常量(142),浮点型常量(12.1)等的处理是没有区别的。

                   String s="Hello world";
                    int intData=142;
                    double dblData=12.1;

(2) Java虚拟机运行class文件
      当Java虚拟机需要运行一个class文件时,它首先会用类装载器装载进class文件。当然也就需要在内存中存放许多东西。比如class的二进制字 节码。还有需要存储class文件中得到的其他信息,比如程序创建的对象,传递给方法的参数,返回值,局部变量等等。怎么多麻烦的数据当然需要管 理,JVM会把这些东西都组织到几个“运行时数据区 ”中。这些数据区中就有我们动不动就谈到的"堆"呀,"栈"呀什么的?想要详细了解这部分东西可以看《深入Java虚拟机》第二版第5章。
   
      在这里我只谈谈“方法区 ”这个运行时数据区。在Java虚拟机中,关于被装载类型的信息会在一个逻辑上被称为"方法区"的内存中,当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入该文件。紧接着虚拟机提取其中的类型信息,并将这些信息存储到方法区中。
   
      方法区中的这些类型信息是很有用的,比如:这个类型的全限定名(net.single.HelloWorld);这个类型的直接超类的全限定名;这个类型 是类类型还是接口类型;这个类型的访问修饰符(public,final,static)等。还有两个大家都很熟悉的引用:指向类ClassLoader 的引用和指向Class类的引用。这是Java反射机制能够运行的关键所在。这里我们要提到的是一个非常重要的信息——该类型的常量池

    上面提到的,class文件结构中的常量池二进制流就被JVM存储在方法区中进行管理。当程序运行时需要使用到常量值的时候,直接在方法区常量池所在的内存中寻找就可以了。

(3) 操作码助忆符指令集
     将String s=new String("Hello world");编译成class文件后的指令(由eclipse打开class文件查看的):

Class字节码指令集代码
  1. 0  new java.lang.String [15]    
  2. 3  dup  
  3. 4  ldc <String "Hello word"> [17]   
  4. 6  invokespecial java.lang.String(java.lang.String) [19]  
  5. 9  astore_1 [s]  
  6. 10  retur  

    下面通俗的解释一下这些指令,详细见《深入Java虚拟机》第二版附表:按操作码助忆符排列的指令集。
    ★ new指令: 在内存的堆区域中为新字符串对象分配足够大的空间,并将对象的实例变量设为默认值。
    ★ ldc指令:在内存的方法区常量池中找到String类型字面值常量表 的入口,然后定位到的"Hello word"所在内存中的位置。
    ★ invokespecial指令:调用指定的类构造器(这里调用的是String(String)这一个构造器。将ldc指令所找到的"Hello word"的内容传入到new指令所开辟在堆中的字符串对象中。
    ★ astore_1:将new指令所开辟堆的内存位置存入局部变量s中

 

    将String s="Hello world";编译成class文件后的指令:

Class字节码指令集代码
  1. 0  ldc <String "Hello world"> [15]  
  2. 2  astore_1 [str]  
  3. 3  return  

    ★ ldc指令:在内存的方法区常量池中找到String类型字面值常量表 的入口,然后定位到的"Hello word"所在内存中的位置(如果常量池中没有"Hello word",则会在其中添加一个"Hello word")。
    ★ astore_1:将ldc指令定位到的常量池中的位置存入局部变量s中

镜头总结: String类型脱光了其实也很普通。真正让她神秘的原因就在于String类型字面值常量表 的存在。

相关问题解决 

   (问题1) 代码1                                                           代码2

              String sa=new String("Hello world");          String sc="Hello world";
              String sb=new String("Hello world");          String sd="Hello world";
              System.out.println(sa==sb);  // false          System.out.println(sc==sd);  // true
              变量sa,sb中存储的内容是JVM在堆中开辟的两个String对象的内存地址。==比较就是sa,sb变量存储的内容,也就是两个不同的内存地址,当然是false;
              变量sc,sd中存储的内容也是地址,但却都是方法区常量池中"Hello word"所在的地址,自然一样。

   (问题2) 代码1                                                          代码2
              String sa = "ab";                                        String sc="ab"+"cd";
              String sb = "cd";                                        String sd="abcd";
              String sab=sa+sb;                                     System.out.println(sc==sd); //true
              String s="abcd";
              System.out.println(sab==s); // false
              代码1中sa+sb被编译以后使用的是StringBuilder.append(String)方法。JVM会在堆中创建一个 StringBuilder类,将sa所指向常量池中的内容"ab"传入,然后调用append(sb所指向的常量池内容)完成字符串合并功能,最后将堆 中StringBuilder对象的地址赋给变量sab。而s存储的是常量池中"abcd"的地址。sab与s地址当然不一样了。
              代码2中"ab"+"cd"会直接在编译阶段就合并成常量"abcd",所以相同的字符串在常量池中的地址也相同了。

【镜头二】  String三姐妹(String,StringBuffer,StringBuilder),谁更性感?
        String不用多说,扒的差不多了。但他还有两个妹妹StringBuffer,StringBuilder长的也不错哦!我们首先对这三姐妹下个定义:
        String(大姐,出生于JDK1.0时代)          不可变字符序列
        StringBuffer(二姐,出生于JDK1.0时代)    线程安全的可变字符序列
        StringBuilder(小妹,出生于JDK1.5时代)   非线程安全的可变字符序列

讨论1、StringBuffer与String的可变性问题。
         我们先看看这两个类的简要源代码:

Java代码
  1. //String   
  2. public final class String  
  3. {  
  4.         private final char value[];  
  5.   
  6.          public String(String original) {  
  7.               // 把原字符串original切分成字符数组并赋给value[];  
  8.          }  
  9. }  
  10.   
  11. //StringBuffer   
  12. public final class StringBuffer extends AbstractStringBuilder  
  13. {  
  14.          char value[]; //继承了父类AbstractStringBuilder中的value[]  
  15.          public StringBuffer(String str) {  
  16.                  super(str.length() + 16); //继承父类的构造器,并创建一个大小为str.length()+16的value[]数组  
  17.                  append(str); //将str切分成字符序列并加入到value[]中  
  18.         }  
  19. }  

      很显然,String和StringBuffer中的value[]都用于存储字符序列。但是,
      (1) String中的是常量(final)数组,只能被赋值一次。
      比如:new String("abc")使得value[]={'a','b','c'},之后这个String对象中的value[]再也不能改变了。这也正是大家常说的,String是不可变的原因 。   
      注意:这个对初学者来说有个误区,有人说String str1=new String("abc"); str1=new String("cba");不是改变了字符串str1吗?那么你有必要先搞懂对象引用和对象本身的区别。
      (2) StringBuffer中的value[]就是一个很普通的数组,而且可以通过append()方法将新字符串加入value[]末尾。这样也就改变了value[]的内容和大小了。
      比如:new StringBuffer("abc")使得value[]={'a','b','c','',''...}(注意构造的长度是str.length()+16)。如果再将这个对象append("abc"),那么这个对象中的value[]={'a','b','c','a','b','c',''....}。这也就是为什么大家说 StringBuffer是可变字符串 的涵义了。
      从这一点也可以看出,StringBuffer中的value[]完全可以作为字符串的缓冲区功能。
               String s1=new String("aaa");
               StringBuffer sb1=new StringBuffer(); //一个字符串缓冲区
               sb1.append(s1);//将字符串s1加进缓冲区
    注意,讨论String和StringBuffer可不可变。本质指对象内部的value[]字符数组可不可变,而不是对象引用可不可变。
讨论2、StringBuffer与StringBuilder的线程安全性问题
     StringBuffer和StringBuilder可以算是双胞胎了,这两者的方法没有很大区别。但在线程安全性方面,StringBuffer允许多线程进行字符操作。这是因为在源代码中StringBuffer的很多方法都被关键字synchronized 修饰了,而StringBuilder没有。
     有多线程编程经验的程序员应该知道synchronized。这个关键字是为线程同步机制 设定的。我简要阐述一下synchronized的含义:
      每一个类对象都对应一把锁,当某个线程A调用类对象O中的synchronized方法M时,必须获得对象O的锁才能够执行M方法,否则线程A阻塞。一旦 线程A开始执行M方法,将独占对象O的锁。使得其它需要调用O对象的M方法的线程阻塞。只有线程A执行完毕,释放锁后。那些阻塞线程才有机会重新调用M方 法。这就是解决线程同步问题的锁机制。
     了解了synchronized的含义以后,大家可能都会有这个感觉。多线程编程中StringBuffered比StringBuilder要安全多了 ,事实确实如此。如果有多个线程需要对同一个字符串缓冲区进行操作的时候,StringBuffer应该是不二选择。
     注意:是不是String也不安全呢?事实上不存在这个问题,String是字符串常量,不可变。线程对于String对象只能读取,无法修改。试问:还有什么不安全的呢?
讨论3、String和StringBuffer的效率问题(这可是个热门话题呀!)

      首先说明一点:StringBuffer和StringBuilder可谓双胞胎,StringBuilder是1.5新引入的,其前身就是StringBuffer。在Core Java editior 7 中文版P611页中有这么一句话: StringBuffer比StringBuilder的效率稍低
      我们用下面的代码运行1W次字符串的连接操作,测试String,StringBuffer所运行的时间。

Java代码
  1. //测试代码  
  2. public class RunTime{  
  3.     public static void main(String[] args){  
  4.            //位置1  
  5.           long beginTime=System.currentTimeMillis();  
  6.           for(int i=0;i<10000;i++){  
  7.                 //位置2  
  8.           }  
  9.           long endTime=System.currentTimeMillis();  
  10.           System.out.println(endTime-beginTime);     
  11.     }  
  12. }  

    (1) String常量与String变量的"+"操作比较
         首先声明两个概念:String str="Heart"中"Heart"是String常量(存放在常量池中);而str则是String变量,是String对象的引用。
         ▲测试①代码:     (测试代码位置1)  String str="";
                                   (测试代码位置2)  str="Heart"+"Raid";
            [耗时:  0ms]
            
         ▲测试②代码        (测试代码位置1)  String s1="Heart";
                                                           String s2="Raid";
                                                           String str="";
                                  (测试代码位置2)  str=s1+s2;
            [耗时:  15—16ms]
        结论:String常量的“+连接” 稍优于  String变量的“+连接”。
        原因:测试①的"Heart"+"Raid"在编译阶段就已经连接起来,并形成了一个新的字符串常量"HeartRaid"。运行阶段只需要 将"HeartRaid"存入常量池。循环1W次也只不过将常量池中"HeartRaid"的地址取出1W次赋值给String对象的引用而已。
                测试②在编译阶段,编译器并不知道s1和s2代表什么,它会将这句话编译成几个指令,大体上是这样的。
                      首先:StringBuilder temp=new StringBuilder(s1),
                      然后:temp.append(s2)
                      最后:str=temp.toString();
                我们发现,虽然在中间的时候也用到了append()方法,但是在开始和结束的时候分别创建了StringBuilder和String对象。可想而知:调用1W次,是不是就创建了1W次这两中对象呢?不划算。

         不言而喻:String变量的"+"连接操作比String常量操作使用的更加广泛。
     (2)String对象的"累+"连接操作与StringBuffer对象的append()累和连接操作比较。
          ▲测试①代码:     (代码位置1)  String s1="Heart";
                                                       String s="";
                                    (代码位置2)  s=s+s1;
             [耗时:  4200—4500ms]
            
          ▲测试②代码        (代码位置1)  String s1="Heart";
                                                       StringBuffer sb=new StringBuffer();
                                    (代码位置2) sb.append(s1);
             [耗时:  0ms(当循环100000次的时候,耗时大概16—31ms)]
         结论:StringBuffer的append()累和连接    远好于    String对象的"累+"连接
         原因:测试①中s=s+s1我们知道编译器也会使用StringBuilder的append方法,但是首先编译器会将new StringBuilder存放s,然后在通过 StringBuilder.append(s1),最后在new一个新的String对象存放 StringBuilder的连接后的内容 。试想这个过程一次就需要创建一个新的String并赋值给引用s,10000次就需要创建了1W次新String对象。正式因为String中的value[]数组不可变,只能不停的创建新的String来存放变化中的字符串。效率可想而知了。
                  测试②中sb.append(s1);只需要将自己的value[]数组不停的扩大来存放s1即可。无需在堆中创建一大堆的对象。效率高就不足为奇了。
      
         不言而喻:大规模累加字符串的时候,StringBuffer就比她姐姐不知道强多少倍了。
镜头总结: (1) 在编译阶段就能够确定的字符串常量,完全没有必要创建String或StringBuffer对象。直接使用字符串常量的"+"连接操作效率最高。
                (2)否则,StringBuffer对象的append效率要高于String对象的"+"连接操作。
                (3) intern()方法很有用。
                     当调用String 对象的intern方法时,如果池已经包含一个等于此对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。
                     也就是如果我们需要使用new关键字来创建很多内容重复的字符串的话,使用String.intern()方法可以提高效率。

没有评论: