JAVA中的String剖析

首先把问题摆出来,先看以下代码(我们姑且称为“代码一”):

  1. String a = "ab";  
  2. String b = "a" + "b";   
  3. System.out.println((a == b));  

估计对java有一定了解的都会回答是true,答案是对的,但是解释呢?也许你会说:String a = "ab";创建了新的对象"ab"; 再执行String b = "a" +"b";结果b="ab",这里没有创建新的对象,而是从JVM字符串常量池中获取之前已经存在的"ab"对象。因此a,b具有对同一个string对象 的引用,两个引用相等,结果true。很遗憾,这个答案,是不够准确的。或者说,根本没有运行时计算b = "a" + "b";这个操作.实际上运行时只有Stringb = "ab";这个观点的解释适合以下情况(我们姑且称为“代码二”):

  1. String a = "ab";   
  2. String b = "ab";   
  3. System.out.println((a == b));  

如果String b = "a" +"b";是在运行期执行,则以上的解释是行不通的。运行期的两个string相加,会产生新的对象的。(本文后面对此有解释)。

        其实正确的解释应该是String b = "a" + "b";编译器将这个"a" +"b"作为常量表达式,在编译时进行优化,直接取结果"ab",这样这个问题退化为“代码二”的问题了,然后再使用第一个最开始给的不是很准确的解释就说的通了。这里有一个疑问就是String不是基本类型,像int secondsOfDay = 24 * 60 * 60;这样的表达式是常量表达式,编译器在编译时直接计算容易理解,而"a" +"b" 这样的表达式,string是对象不是基本类型,编译器会把它当成常量表达式来优化吗?下面用事实来证明:首先编译这个类:

  1. public class Test {   
  2.     private String a = "aa";   
  3.       
  4. }  

复制class文件,备用,然后修改为:

  1. public class Test {   
  2.     private String a = "a" + "a";   
  3.       
  4. }  

再次编译,用ue之类的文本编辑器打开,察看二进制内容,可以发现,两个class文件完全一致,连一个字节都不差。ok,真相大白了.根本不存在运行期的处理String b = "a" +"b";这样的代码的问题,编译时就直接优化掉了。

      下面进一步探讨,什么样的string + 表达式会被编译器当成常量表达式?String b = "a" + "b";这个String + String被证实是ok的,那么string + 基本类型呢?

  1. String a = "a1";  
  2. String b = "a" + 1;   
  3. System.out.println((a == b)); //result = true   
  4. String a = "atrue";   
  5. String b = "a" + true;   
  6. System.out.println((a == b)); //result = true   
  7. String a = "a3.4";   
  8. String b = "a" + 3.4;   
  9. System.out.println((a == b)); //result = true  

可见编译器对string + 基本类型是当成常量表达式直接求值来优化的。再注意看这里的string都是"**"这样的,我们换成变量来试试:

  1. String a = "ab";  
  2. String bb = "b";   
  3. String b = "a" + bb;  
  4. System.out.println((a == b)); //result = false  

这个好理解,"a" + bb中的bb是变量,不能进行优化。这里很很好的解释了为什么3的观点不正确,如果String+String的操作是在运行时进行的,则会产生新的对象,而不是直接从jvm的string池中获取。再修改一下,把bb作为常量变量:

  1. String a = "ab";  
  2. final String bb = "b";  
  3. String b = "a" + bb;   
  4. System.out.println((a == b)); //result = true  

竟然又是true,编译器的优化好厉害啊,考虑下面这种情况:

  1. String a = "ab";  
  2. final String bb = getBB();   
  3. String b = "a" + bb;   
  4. System.out.println((a == b)); //result = false  
  5. private static String getBB() { return "b"; }  

看来java(包括编译器和jvm)对string的优化,真的是到了极点了,string这个所谓的"对象",完全不可以看成一般的对象,java对string的处理近乎于基本类型,最大限度的优化了几乎能优化的地方。另外感叹一下,string的+号处理,算是java语言里面唯一的一个"运算符重载"。


以上文章转自百度文库的一篇,进行了轻微的修改,网址http://wenku.baidu.com/view/3252cd37ee06eff9aef80791.html


   去华为面试的时候, 第一笔试题就让我费神去想了, 回来在机子上运行结果, 发现自己当时答错了, 于是就狠下心来花了点时间研究这个:

  1. <span style="font-size:16px;">String a = "a" + "b";  
  2. String b = "a" + "b";  
  3. System.out.println(a == b);</span>  


答案是nullabc!

很早的时候我就知道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不同,则并行执行(相互之间不互斥)。原始代码如下:

  1. <span style="font-size:16px;">package syn;    
  2.     
  3. //不能改动此Test类        
  4. public class Test extends Thread{    
  5.         
  6.     private TestDo testDo;    
  7.     private String key;    
  8.     private String value;    
  9.         
  10.     public Test(String key,String key2,String value){    
  11.         this.testDo = TestDo.getInstance();    
  12.         /*常量"1"和"1"是同一个对象,下面这行代码就是要用"1"+""的方式产生新的对象,  
  13.         以实现内容没有改变,仍然相等(都还为"1"),但对象却不再是同一个的效果*/    
  14.         this.key = key+key2;     
  15.         this.value = value;    
  16.     }    
  17.     
  18.     
  19.     public static void main(String[] args) throws InterruptedException{    
  20.         Test a = new Test("1","","1");    
  21.         Test b = new Test("1","","2");    
  22.         Test c = new Test("3","","3");    
  23.         Test d = new Test("4","","4");    
  24.         System.out.println("begin:"+(System.currentTimeMillis()/1000));    
  25.         a.start();    
  26.         b.start();    
  27.         c.start();    
  28.         d.start();    
  29.     }    
  30.         
  31.     public void run(){    
  32.         testDo.doSome(key, value);    
  33.     }    
  34. }    
  35.     
  36. class TestDo {    
  37.     
  38.     private TestDo() {}    
  39.     private static TestDo _instance = new TestDo();     
  40.     public static TestDo getInstance() {    
  41.         return _instance;    
  42.     }    
  43.     
  44.     public void doSome(Object key, String value) {    
  45.     
  46.         // 以大括号内的是需要局部同步的代码,不能改动!    
  47.         {    
  48.             try {    
  49.                 Thread.sleep(1000);    
  50.                 System.out.println(key+":"+value + ":"    
  51.                         + (System.currentTimeMillis() / 1000));    
  52.             } catch (InterruptedException e) {    
  53.                 e.printStackTrace();    
  54.             }    
  55.         }    
  56.     }    
  57.     
  58. }  </span>  

此题解题的思路有很多种,不可或缺的步骤就是在doSome方法内部用synchronized(o)把那个写了注释的代码块同步, 有些人肯定会说:

我直接synchronized(key),不就完了么.?  这类人肯定是新手级别的了!

上面说了,synchronized(Obj),   对同一个对象才会互斥,不是同一个对象就不会互斥! 大家请看下Test类中的构造方法里面对key做了什么处理?

this.key = key + key2;

关于字符串的拼接,  如果是两个常量的拼接, 那么你无论拼接多少下都是同一个对象,  这个是编译时 编译器自动去优化的(想知道具体原理的自己去网上搜下).

  1. <span style="font-size:16px;">String a = "a" + "b";    
  2. String b = "a" + "b";    
  3. System.out.println(a == b);  </span>  

这段代码输出true没有问题

但是一旦涉及到变量了, 我在上面标红加粗的运行时,    此时拼接字符串就会产生StringBuilder,  然而拼接完返回的字符串是怎么返回的呢?

在StringBuilder.toString()中的实现是new String(char value[], int offset, int count), 既然是创建String返回的, 那么调用一次toString,就是一个不同的对象

  1. <span style="font-size:16px;">String a = "a";    
  2. String b = "b";    
  3. String s1 = a + b;    
  4. String s2 = a + b;    
  5. System.out.println(s1 == s2); </span>  

这个输出就是false!

所以在那道机试题中, 就不能直接用synchronized(key)去同步了,  如果你完完全全很耐心的看完本文, 那么应该知道如何用synchronized(key)同步那段代码了!

不错, 就是修改Test构造方法中的 this.key = key + key2;为this.key = key;

因为字符串不涉及到拼接的时候, 只要不new, 多少都是指向同一个对象!

当然这道多线程的题你也可以把那个key丢到集合里面去,用集合去的contains(obj)去判断,如果集合中存在, 就取集合中的, 否则往集合中添加,但是记住一定要使用并发包下面的集合, 否则可能会抛出ConcurrentModificationException


所以在那道机试题中, 就不能直接用synchronized(key)去同步了,  如果你完完全全很耐心的看完本文, 那么应该知道如何用synchronized(key)同步那段代码了!

不错, 就是修改Test构造方法中的 this.key = key + key2;为this.key = key;

因为字符串不涉及到拼接的时候, 只要不new, 多少都是指向同一个对象!

当然这道多线程的题你也可以把那个key丢到集合里面去,用集合去的contains(obj)去判断,如果集合中存在, 就取集合中的, 否则往集合中添加,但是记住一定要使用并发包下面的集合, 否则可能会抛出ConcurrentModificationException.


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值