博客地址:http://blog.csdn.net/izard999/article/details/6708433网上剖析String的不少,关于其他的String的知识我就不累赘去说了!本文只解释下我在面试中遇到的String拼接的问题以及最近看到了网上的一道机试题跟这个有关系, 所以就想把自己对String拼接的理解分享给大家! 去华为面试的时候, 第一笔试题就让我费神去想了, 回来在机子上运行结果, 发现自己当时答错了, 于是就狠下心来花了点时间研究这个:
String s = null;  
s += "abc";  
System.out.println(s);  答案是nullabc!就这三行代码, 我问了不下于50个人, 有资深的人也有新手的, 在不运行的情况下全答错了。!  可见现在学java的人有很多人都是速成的,而且这种原理级而又看似不怎么实用的东西几乎没什么人去研究, 但是后面说的机试如果能知道String拼接的原理的话。将很容易就解决!很早的时候我就知道String拼接中间会产生StringBuilder对象(JDK1.5之前产生StringBuffer),但是当时也没有去深究内部, 导致在华为笔试此题就错了!运行时, 两个字符串str1, str2的拼接首先会调用 String.valueOf(obj),这个Obj为str1,而String.valueOf(Obj)中的实现是return obj == null ? "null" : obj.toString(), 然后产生StringBuilder, 调用的StringBuilder(str1)构造方法, 把StringBuilder初始化,长度为str1.length()+16,并且调用append(str1)! 接下来调用StringBuilder.append(str2), 把第二个字符串拼接进去, 然后调用StringBuilder.toString返回结果!所以那道题答案的由来就是StringBuilder.append("null").append("abc").toString();大家看了我以上的分析以后, 再碰到诸如此类的面试题应该不会再出错了!
那么了解String拼接有什么用呢?在做多线程的时候, 往往会用到一个同步监视器对象去同步一个代码块中的代码synchronized(Obj),   对同一个对象才会互斥,不是同一个对象就不会互斥!这里有个机试题,现有程序同时启动了4个线程去调用TestDo.doSome(key, value)方法,由于TestDo.doSome(key, value)方法内的代码是先暂停1秒,然后再输出以秒为单位的当前时间值,所以,会打印出4个相同的时间值,如下所示:
  4:4:1258199615
  1:1:1258199615
  3:3:1258199615
  1:2:1258199615
        请修改代码,如果有几个线程调用TestDo.doSome(key, value)方法时,传递进去的key相等(equals比较为true),则这几个线程应互斥排队输出结果,即当有两个线程的key都是"1"时,它们中的一个要比另外其他线程晚1秒输出结果,如下所示:
  4:4:1258199615
  1:1:1258199615
  3:3:1258199615
  1:2:1258199616
   总之,当每个线程中指定的key相等时,这些相等key的线程应每隔一秒依次输出时间值(要用互斥),如果key不同,则并行执行(相互之间不互斥)。原始代码如下:    package syn;  
     
   //不能改动此Test类      
   public class Test extends Thread{  
         
       private TestDo testDo;  
       private String key;  
       private String value;  
         
       public Test(String key,String key2,String value){  
           this.testDo = TestDo.getInstance();  
           /*常量"1"和"1"是同一个对象,下面这行代码就是要用"1"+""的方式产生新的对象, 
           以实现内容没有改变,仍然相等(都还为"1"),但对象却不再是同一个的效果*/  
           this.key = key+key2;   
           this.value = value;  
       }  
     
     
       public static void main(String[] args) throws InterruptedException{  
           Test a = new Test("1","","1");  
           Test b = new Test("1","","2");  
           Test c = new Test("3","","3");  
           Test d = new Test("4","","4");  
           System.out.println("begin:"+(System.currentTimeMillis()/1000));  
           a.start();  
           b.start();  
           c.start();  
           d.start();  
       }  
         
       public void run(){  
           testDo.doSome(key, value);  
       }  
   }  
     
   class TestDo {  
     
       private TestDo() {}  
       private static TestDo _instance = new TestDo();   
       public static TestDo getInstance() {  
           return _instance;  
       }  
     
       public void doSome(Object key, String value) {  
     
           // 以大括号内的是需要局部同步的代码,不能改动!  
           {  
               try {  
                   Thread.sleep(1000);  
                   System.out.println(key+":"+value + ":"  
                           + (System.currentTimeMillis() / 1000));  
               } catch (InterruptedException e) {  
                   e.printStackTrace();  
               }  
           }  
       }  
     
   } 此题解题的思路有很多种,不可或缺的步骤就是在doSome方法内部用synchronized(o)把那个写了注释的代码块同步, 有些人肯定会说:我直接synchronized(key),不就完了么.?  这类人肯定是新手级别的了!上面说了,synchronized(Obj),   对同一个对象才会互斥,不是同一个对象就不会互斥! 大家请看下Test类中的构造方法里面对key做了什么处理?this.key = key + key2;关于字符串的拼接,  如果是两个常量的拼接, 那么你无论拼接多少下都是同一个对象,  这个是编译时 编译器自动去优化的(想知道具体原理的自己去网上搜下).   String a = "a" + "b";  
   String b = "a" + "b";  
   System.out.println(a == b);  这段代码输出true没有问题但是一旦涉及到变量了, 我在上面标红加粗的运行时,    此时拼接字符串就会产生StringBuilder,  然而拼接完返回的字符串是怎么返回的呢?在StringBuilder.toString()中的实现是new String(char value[], int offset, int count), 既然是创建String返回的, 那么调用一次toString,就是一个不同的对象   String a = "a";  
   String b = "b";  
   String s1 = a + b;  
   String s2 = a + b;  
   System.out.println(s1 == s2);  所以在那道机试题中, 就不能直接用synchronized(key)去同步了,  如果你完完全全很耐心的看完本文, 那么应该知道如何用synchronized(key)同步那段代码了!不错, 就是修改Test构造方法中的 this.key = key + key2;为this.key = key;因为字符串不涉及到拼接的时候, 只要不new, 多少都是指向同一个对象!当然这道多线程的题你也可以把那个key丢到集合里面去,用集合去的contains(obj)去判断,如果集合中存在, 就取集合中的, 否则往集合中添加,但是记住一定要使用并发包下面的集合, 否则可能会抛出ConcurrentModificationException

解决方案 »

  1.   

    String s = null;  
    s += "abc";  
    System.out.println(s); 这个结果我还是知道的,嘿嘿。
      

  2.   

    +=其实是java对其做了特殊的处理
      

  3.   

    可惜我不是版主,无法帮楼主推荐。。要不我申请一个版主? 大家到时候支持一下? Java版的版主已经常年见不着人了       首先关于前半部分 string的 +=操作。 楼主没必要讲这么多。 直接用 javap 看一下字节码就懂  
         后半部分, 关于synchronized的互斥,可以看一下我博客,前些日子周末无聊,就写写,很多人都容易犯这个错
      

  4.   

    同步对象还有很多陷阱
    比如
    Integer i = 10;
    synchronized(i) {
        i++; //此时i指向也变了
    }
      

  5.   


    我是看JAVA版老是没见着版主,想当人民公仆好不
      

  6.   

    支持~ 现在java版比较冷清~
      

  7.   

    ......问了30个人 全回答错了?..太夸张了吧...LZ还知道+=操作符重载是StringBuffer还回答错鸟..我看LZ是不知道 String s = null + "abc"的答案吧..
      

  8.   

    嗯, 以前没深入过,  +的操作符重载生成StringBuilder, 回来跟源码看, 才知道死在String.valueOf()的实现上面,  嘿嘿
      

  9.   

    借楼主贵地,打个小广告:申请版主,求支持:
    http://topic.csdn.net/u/20110822/16/fcd9a437-278c-4f4b-846e-e218ece4bf34.html?seed=551291236&r=75095250#r_75095250
      

  10.   

    在用String类对象直接拼接时,JVM会创建一个临时的StringBuffer类对象,并调用其append()方法完成字符串的拼接,这是因为String类是不可变的,拼接操作不得不使用StringBuffer类(并且--JVM会将"You are nice."和"I love you so much."创建为两个新的String对象)。之后,再将这个临时StringBuffer对象转型为一个String,代价不菲!
      

  11.   

    学C++  表示有点汗
    String s = null;  
    s += "abc";  
    System.out.println(s); 开始觉得  nullabc  应该很简单呀,
    后来一看 怎么 null 没用引号,难道表示的 "";
    ..... 然后就 越看越糊涂了
      

  12.   


    C++就直接连接了。哪有null这种东西,全都是""。干净安全。
      

  13.   

    下面这个面试题我就做错了,看来学习东西还是必须要好好遵循:
    What -> Why -> How
    -----------------------------------------------
    int i=0;
    for(test('a');test('b')&&i<2;test('c')){
         i++;
         test('d');
    }
    -----------------------------------------------
    public boolean test(char c){
       System.out.pritn(c);
        return true;
    }
      

  14.   

    package syn;  
         
       //不能改动此Test类      
       public class Test extends Thread{  
             
           private TestDo testDo;  
           private String key;  
           private String value;  
             
           public Test(String key,String key2,String value){  
               this.testDo = TestDo.getInstance();  
               /*常量"1"和"1"是同一个对象,下面这行代码就是要用"1"+""的方式产生新的对象, 
               以实现内容没有改变,仍然相等(都还为"1"),但对象却不再是同一个的效果*/  
               this.key = key+key2;   
               this.value = value;  
           }  
         
         
           public static void main(String[] args) throws InterruptedException{  
               Test a = new Test("1","","1");  
               Test b = new Test("1","","2");  
               Test c = new Test("3","","3");  
               Test d = new Test("4","","4");  
               System.out.println("begin:"+(System.currentTimeMillis()/1000));  
               a.start();  
               b.start();  
               c.start();  
               d.start();  
           }  
             
           public void run(){  
               testDo.doSome(key, value);  
           }  
       }  
         
       class TestDo {  
         
           private TestDo() {}  
           private static TestDo _instance = new TestDo();   
           public static TestDo getInstance() {  
               return _instance;  
           }  
         
           public void doSome(Object key, String value) {  
         
               // 以大括号内的是需要局部同步的代码,不能改动!  
               {  
                   try {  
                       Thread.sleep(1000);  
                       System.out.println(key+":"+value + ":"  
                               + (System.currentTimeMillis() / 1000));  
                   } catch (InterruptedException e) {  
                       e.printStackTrace();  
                   }  
               }  
           }  
         
       } 
    ---------------------------
    关于以上代码只需这样修改:
     private static String key; 
    synchronized(key),不就完了么
      

  15.   

    我只是实现了同一个KEY对象实现同步,但由于是key为static,所以4个类对象的key的值会都一样
      

  16.   


    大胡子能详细讲讲String的+=操作么?怎么通过字节码看?
      

  17.   

    你之前发了一个 java 关于字符串+ 重载的问题 我感觉有些问题
    按你的解释如下代码  char[] str={1,2,3}
    System.out.println("aa"+"bb"+str);
    等同于 StringBuilder.append(aa).append("bb").append(str).toString();
     那结果应该是  aabb123  但是结果确是 aabb+数组的地址所以我感觉你说的是不对的对于+ 操作如果引用为 null 则把它转换为 “null”。否则,该转换的执行就像无参数调用该引用对象的toString() 方法一样
      

  18.   

    自己写一个类, 用+号拼字符串, 到class文件的目录下去,命令行 javap 就可以了
      

  19.   

    我来模拟一下来证明你说的是错误的
    char[] numbers={'1','2','3'};
    String s = null;  
    s += "abc";
    s +=numbers ;
    System.out.println(s);  

    String i=null;
    StringBuilder builder=new StringBuilder(String.valueOf(i));
    builder.append("abc");
    builder.append(numbers);
    String temp=builder.toString();

    System.out.println(temp);上面是代码  
    nullabc[C@89ae9e
    nullabc123下面是结果 ,所以事实证明 +的执行 并不是你分析的那样
      

  20.   

    说掉了一个, javap只输出package以上级别的,  要查看编译出来的字节码要指定参数, javap -c ....就可以了
      

  21.   

    还好String s = null;  
    s += "abc";  
    System.out.println(s);  这个我知道。。因为之前遇到过这个问题,结果在画面text中输出的是null字符串。。
      

  22.   

    对于 char[]来说 ,StringBuilder 的append(char[])  String的valueof(char[])  都给出了重载
     所以都能以正确的形式表现。 但是对于 + 却没有先去吃饭 求讲解  
      

  23.   

    虽然我不什么懂,但非常感谢楼主,MARK!
      

  24.   

    你能提出这样的问题说明你是个用心学习的人!  我来解释下这个问题和你82楼得出的答案是为什么!
    如果你细心的话应该看到我上面写的这么一句话: 用+拼的华先会调用 String.valueOf(obj), 这个很重要, 这个是导致结果不同的直接原因.!  append(Obj)和append(char[])这两个实现完全是不同的!
    你自己显示去拿StringBuilder调用append(char[])那当然是char实现的那一套咯, 而用+拼的编译器会自己去给你调用Obj重载的那个版本, 就是toString, 而数组的toString就是显示首地址相信应该不用我解释!
    你自己拿javap -c 去看看你那两段代码所调用StringBuilder.append的版本, 就会明白了!
    至于说编译器为什么会调用Obj的那个版本. 这个貌似我还解释不了, LZ有兴趣的话可以一起研究下, 如果你那边有结果了记得分享给我啊!理论来说上面两段代码其实应该来说结果是一样的,  导致不同的结果是由于编译器把代码编译成的字节码有出入!   有问题自己应该去深入下!  
      

  25.   

    我今天在读  java解惑凑巧就看到了  ,如果楼主手中有此书 可以看看 解惑12
      

  26.   

    这个书上说调用的 toString() 方法啊 而不是 String.valueOf(),一直没有看字节码的习惯, 哈哈一会去看看 
      

  27.   

    java解惑?好的, 我去搜一下.! 呵呵! 谢啦
      

  28.   

    免得你专门去看字节码了, 我贴给你吧
    原始代码:
    char[] str={'1','2','3'};
    System.out.println("aa"+"bb"+str);

    StringBuilder sb = new StringBuilder();
    sb.append("abcc").append(str);
    System.out.println(sb.toString());
    编译后的字节码:public static void main(java.lang.String[]);
      Code:
       0: iconst_3
       1: newarray char
       3: dup
       4: iconst_0
       5: bipush 49
       7: castore
       8: dup
       9: iconst_1
       10: bipush 50
       12: castore
       13: dup
       14: iconst_2
       15: bipush 51
       17: castore
       18: astore_1
       19: getstatic #16; //Field java/lang/System.out:Ljava/io/PrintStream;
       22: new #22; //class java/lang/StringBuilder
       25: dup
       26: ldc #24; //String aabb
       28: invokespecial #26; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
       31: aload_1
       32: invokevirtual #29; //Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
       35: invokevirtual #33; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
       38: invokevirtual #37; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
       41: new #22; //class java/lang/StringBuilder
       44: dup
       45: invokespecial #42; //Method java/lang/StringBuilder."<init>":()V
       48: astore_2
       49: aload_2
       50: ldc #43; //String abcc
       52: invokevirtual #45; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
       55: aload_1
       56: invokevirtual #48; //Method java/lang/StringBuilder.append:([C)Ljava/lang/StringBuilder;
       59: pop
       60: getstatic #16; //Field java/lang/System.out:Ljava/io/PrintStream;
       63: aload_2
       64: invokevirtual #33; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
       67: invokevirtual #37; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
       70: return}
    注意对比32和56! 调用append(Obj)的时候虽然会调String.valueOf,但此时已经是向上转型了, char[]已经转成了Object啦!
      

  29.   

    好东西 支持 ~
    public static String valueOf(Object obj)
        {
            return obj != null ? obj.toString() : "null";
        }    public static String valueOf(char ac[])
        {
            return new String(ac);
        }