黑马程序员技术交流社区

标题: “第三贴”之String、StringBuilder解析,搞定所有string面试题 [打印本页]

作者: 黑马伍哲沂    时间: 2013-5-5 16:29
标题: “第三贴”之String、StringBuilder解析,搞定所有string面试题
本帖最后由 曹睿翔 于 2013-5-5 19:23 编辑

我领的是第五第六题。String练习中遇到的问题,以及StringBuilder与StringBuffer的区别。我觉得这两题关联很大。而且要讲的基本上主要是String与其它两类的区别。所以两题做一帖了。摘录一些很经典的代码段。个人觉得特别有代表性,能更深入的理解String以及StringBuilder。值得看看。参考了不少别人的资料,但都是自己仔细总结的。期待大家为我纠正错误之处。
=================================================================
                         String
一.String的内存解析。
这是刚开始接触java的同学相对较多遇到问题的地方吧。先看两个语句:
String str0 = "java";
String str1 = new String("hello java!");
两种声明字符串的方式。前一种,并没有在堆中分配内存。只是将"java"保存在常量池中。第二种方式,是将"hello java!"保存在常量池中,new操作所开辟的堆空间里会复制一份"hello java!"。然后讲str1指向new后的堆空间。这就是为什么说第二个语句实际上是两个对象。如图:

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
二.String是不可变的。
刚看到String是不可变的时候,有点不明白。后来查看一些资料,再看看String类的源代码,就明白了。这也解决了另外一个常被问到的面试题:问,String类可以被继承吗?先来看看String的源码,String类的声明是这样的:public final class String......这样是不是就有答案了。至于String是不可变的原因是,从源码可以看出,String的成员变量差不多都是被final修饰
的。再看两个语句:
String str = "java";
str = "hello";
String的不可变是指,虚拟机并没有改变原来的对象"java",而是新创建了一个对象"hello",再让str指向这个新的对象"hello"。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
三.String相关的几段经典代码。
*****代码段一
  1. String str0 = "ab";   
  2.         String str1 = "a" + "b";   
  3.         System.out.println(str0 == str1);
复制代码
*****代码段二
  1. String str0 = "ab";   
  2.         String str1 = "b";   
  3.         String str2 = "a" + str1;   
  4.         System.out.println(str0 == str2);   
复制代码
*****代码段三
  1. String str = "java";
  2.        String str1 = new String("java");
  3.        System.out.println(str==str1.intern());
复制代码
代码段一,输出的结果是true,代码段二的结果是false。
对于以上两段代码的说明:
1.当使用String str = "java";方式来创建对象时,java运行时会拿这个"java"去String的常量池里去找是否有这个字符串,如果有,直接将常量池中地址给str。如果没有,讲"java"加入常量池。
2.当使用String str = new String("java");来创建对象时,同样会拿"java"去常量池中找,如果没有,则添加。并复制一份到new的堆空间,将堆空间地址给str。如果有,直接复制一份到new的堆空间,将堆空间地址给str。
3.使用常量来串联表达式时,不会在堆空间新建对象。只是在常量池中检查,有则直接指向,没有则在池中创建再被指向。如代码段一String str1 = "a" + "b";因为只在常量池中操作,所以返回true。
4.使用带变量表达式创建String,则不仅检查常量池,还会在堆中新建对象。如代码段二。返回false。
5.关于字符串常量池,还有一个intern()方法。以前没接触毕老师视频的时候,因为不知道字符串有个常量池,所以和一同学不理解编程中遇到的现象。为此查了不少资料才搞明白。当时在一资料上看到这个方法。印象比较深,这个方法的意思就是将常量池的字符串进行比较(我是这么理解的)。如代码段三。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------



未命名.jpg (15.63 KB, 下载次数: 0)

未命名.jpg

作者: 黑马伍哲沂    时间: 2013-5-5 16:29
本帖最后由 黑马伍哲沂 于 2013-5-5 16:43 编辑

四.关于一道面试题的多种说法解析。

有位同学在论坛发这道面试题帖后,看到有很多不同的回答。我都一一思考了下。并查了相关资料。首先,说下比较好的不同理解的回答:
  1. public class Example
  2. {
  3.                 String str = new String("good");
  4.                 char[] ch = {'a','b','c'};
  5.          public static void main(String[] args)
  6.                {
  7.                                Example ex = new Example();
  8.                                ex.change(ex.str, ex.ch);
  9.                                System.out.print(ex.str+"......and.....");
  10.                                System.out.println(ex.ch);
  11.                }

  12.                public void change(String str,char ch[])
  13.               {
  14.                str = "test ok";    //加this处
  15.                               ch[0]='g';
  16.                }
  17. }
复制代码
1.字符串是不可变的;
2.值传递与引用传递的不同;
3.成员变量与局部变量区别;(这是我的理解和回答)
在我当时看来,觉得这三个理解比较靠谱。并且因为看到和自己不同的回答我怀疑自己理解错了。但经过验证,确实在程序change函数里的str前面加上this。就会出现不一样的结果。经过查阅许多资料后,我的理解是这样的:
1.字符串是不可变的。这话没有错。如第二部分的解说。也许也有些类似以上的题目,被解说为字符串是不可变的。我觉得这种说法,不太合适。这里的字符串不可变体现在,当change函数里str前加上this时,change被调用后,str指向了"test ok",原来的"good"暂时还在,只不过str不再指向它了。这就是说"good"是不可变的。我将程序稍微调整如代码段四。运行结果说明了所有问题。
******代码段四
  1. //相比前面代码增加了成员变量num,以及change函数增加同名形参。
  2. public class Example
  3. {
  4.                 String str = new String("good");
  5.                 char[] ch = {'a','b','c'};
  6.                 int num = 9;
  7.                 public static void main(String[] args)
  8.                {
  9.                  Example ex = new Example();
  10.                                 ex.change(ex.str, ex.ch,ex.num);
  11.                                 System.out.print(ex.str+"......and.....");
  12.                                 System.out.println(ex.ch);
  13.                                 System.out.println(ex.num);
  14.                }

  15.                public void change(String str,char ch[],int num)
  16.               {
  17.                               str = "test ok";
  18.                               ch[0]='g';
  19.                               num = 11;
  20.                }
  21. }
复制代码
2.关于值传递和引用传递的说法。查过资料说,String是对象类型,但有值传递的特征。我的理解是,同样如代码段四。这里是不是可以这么理解,具有值传递特征的变量才会出现局部变量和成员变量的问题。
3.如代码段五。这一段不需要任何解释了。结合上面一条说法,大家可以自己思考下。
******代码段五
  1. class Person
  2. {
  3.           String name;
  4.                  int age;
  5.                  Person(String name,int age)
  6.                 {
  7.                                 this.name = name;
  8.                                 this.age = age;
  9.                 }
  10. }
复制代码
最后,我觉得可以这样分析这道面试题(有错之处欢迎跟帖纠正):
因为String类以及Stirng类几乎所有成员变量都被final修饰,所以String是不可变的,因此String虽然是对象,但有值传递的特征。在java中,具有值传递特征的变量,当函数的形参与成员变量同名时,可以使用this来区分。这里change函数里没有添加this,所以结果为good gbc。(回答的是不是有点猛啊
,但我觉得回答的知识面越宽,越是有好处的)关于String的各种变种题目,我觉得只要透彻理解以上几点。什么变种都没关系了。
作者: 黑马伍哲沂    时间: 2013-5-5 16:29
本帖最后由 黑马伍哲沂 于 2013-5-5 16:45 编辑

                                                                    String、StringBuilder、StringBuffer

StringBuilder是1.5才出现的。与StringBuffer有至少两个不一样的地方。第一,名字不一样(废话)。第二,StringBuilder是线程非安全的。也就是说,频繁的操作中不需要判断同步锁,在单线程情况下,可以大大提高效率。帮助文档里是这样介绍的:一个可变的字符序列。此类提供一个与StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。我打开两份帮助文档,分别找到StringBuilder和StringBuffer,一一比对,从上到下,每一个方法都是一模一样的。所以,再没什么好说的了。既然官方文档说字符串缓冲区被单个线程使用的情况很普遍,不如介绍下
StringBuilder里的一部分方法实在:
在 StringBuilder 上的主要操作是 append 和 insert 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符添加或插入到字符串生成器中。append 方法始终将这些字符添加到生成器的末端;而 insert 方法则在指定的点添加字符。其它的方法,都参照帮助文档吧。之所以前面加个String,是这里有必要说明一下String与StringBuilder的区别(StringBuffer是一样的了)。如前文叙述,String是不可变的。通过以下两个语句来看看String是怎样完成字符串的串联操作的。
String str = new String("hello");
str += " java";
1.第一句,首先,检查常量池是否有"hello",没有则创建"hello",再将"hello"复制到new的堆空间,让str指向堆中的"hello"。
2.第二句,在常量池中创建" java"。但没有指向。然后创建一个StringBuilder对象(1.5前是StringBuffer),调用append(),将字符串串联后,通过StringBuilder.toString()转化为String。也即使说,String的串联操作是通过StringBuilder完成的。而且整个过程中创建了四个对象。创建对象以及频繁操作后多余的对象要被gc回收,都是非常耗费时间的。所以说StringBuilder效率高。可以自己试着写小程序来比较运行时间。(差距是非常大的)


作者: 曹睿翔    时间: 2013-5-5 17:24
我还能说什么呢,顶起!
作者: Jacky_Chen1990    时间: 2013-5-5 17:29
这个要顶。谢谢分享!
作者: 曹睿翔    时间: 2013-5-5 19:24
老伍都不说你了,还不按我要求,标题不对,帮你改了
作者: 曹睿翔    时间: 2013-5-5 19:26
大家确实很给力,这次集合贴,榜上有名!!
技术分送上!
作者: 梁海君    时间: 2013-5-5 20:15
{:soso_e116:}厉害,厉害,盗走LZ的果实,只能万分感谢~
作者: 黑马伍哲沂    时间: 2013-5-5 22:01
曹睿翔 发表于 2013-5-5 19:24
老伍都不说你了,还不按我要求,标题不对,帮你改了

sorry。。。兽sir= =。
作者: 黑马伍哲沂    时间: 2013-5-5 22:01
梁海君 发表于 2013-5-5 20:15
厉害,厉害,盗走LZ的果实,只能万分感谢~

哈哈  谢谢顶帖。
作者: 汪平乐    时间: 2013-5-6 09:01
顶......{:soso_e179:}
作者: 风乐    时间: 2013-5-7 01:56
楼主总结的相当好,虽然很基础的东西,但还是让我从头看到尾了,而且感觉又学到了很多
作者: 黑马伍哲沂    时间: 2013-5-7 06:05
风乐 发表于 2013-5-7 01:56
楼主总结的相当好,虽然很基础的东西,但还是让我从头看到尾了,而且感觉又学到了很多 ...

多谢支持。。。。
作者: Miss小强    时间: 2013-5-8 09:39
黑马伍哲沂 发表于 2013-5-5 16:29
四.关于一道面试题的多种说法解析。

有位同学在论坛发这道面试题帖后,看到有很多不同的回答。我都一一思 ...

关于字符串String是不可变的我补充一点,在 HardCode Java 这本书中介绍过不可变类型,即immutable type;
怎么样将一个类变的不可变能,很简单,就是不提供属性的写方法,也就是set方法;同各属性也是私有的,同时为了保证不能被反射,这些属性都被声明为final,
也就是说你外界是无法改变的。String就是这样一个不可变类型。
作者: Miss小强    时间: 2013-5-8 09:48
有个问题不知道怎么理解:

用char数组存多个中文字符 比用string好在哪?
没一点思路
作者: Miss小强    时间: 2013-5-8 10:08
看过你对这道面试题的分析,我回去又好好看了下,从hardcode java这本书里面看到final关键字;
如果给change函数的形式参数列表都加上final关键字修饰,将解决所有问题,也就是:
  1. public void change(final String str,final char ch[])
复制代码
根据final特性,引用本身不能变,但是引用指向的对象是可变的;打个比方说,
小明和她女朋友结婚了,那么这辈子他只能是跟着他女朋友一起白头到老,也就是他只能有他女朋友这么一个了,在也不能换了。
而对于他女朋友来说,他女朋友可以变漂亮,可以变苗条;

小明就是那个引用,而他女朋友就是指向的那个对象;
因此在change方法中要改变str和ch,只能是用this改变成员变量了,而不是传递进来的形式参数;
感觉你想问题比我们都深入,欢迎批评指正。。。
作者: 黑马伍哲沂    时间: 2013-5-8 10:34
Miss小强 发表于 2013-5-8 10:08
看过你对这道面试题的分析,我回去又好好看了下,从hardcode java这本书里面看到final关键字;
如果给chang ...

其实也不是想的更深入,只是遇到问题,感兴趣的就会多查点资料。
final关键字,在毕老师的大概第6-8天左右的视频里有讲的(具体哪天记不得),final用于修饰变量主要有两种情况:
1.一些数据的出现的是固定值,为了增强阅读性,给这些值起个名字。方便于阅读。比如圆周率。这个值有可能在程序中多次出现,但又不需要改变的。加上final之后,就变成常量。
2,内部类定义在类中的局部位置上,只能访问该局部被final修饰的局部变量。(这个不看资料我也记不住,也没注意到是什么原因。)
所以,这里应该不需要这么做。我觉得独立思考总是好的。虽然我的独立思考经常被证明不是对的,但我还是坚持的很好...嘿。。。


作者: 黑马伍哲沂    时间: 2013-5-8 10:35
Miss小强 发表于 2013-5-8 09:48
有个问题不知道怎么理解:

用char数组存多个中文字符 比用string好在哪?

我觉得你应该把问题描述的更具体一点。   String就是用char[]数组实现的。可以看看源码。
作者: Miss小强    时间: 2013-5-8 10:42
这也是群里的一个哥们问的。。。题目就这样
想了半天没明白为啥。
一个哥们的回答:
一个是基本类型 一个是对象类型 char数组多少个成员就站多少字节  String内存占用比char高
确切的说string无论里面多少数据 都至少占用28个字节
String开发时初始内存分配就是28bytes 在28的基础上 超过再递增28 依次类推
超过28 相当于你重新开了一个String
ps:Java中有没有什么方法计算某个变量占用的内存空间

作者: 黑马伍哲沂    时间: 2013-5-8 10:54
Miss小强 发表于 2013-5-8 10:42
这也是群里的一个哥们问的。。。题目就这样
想了半天没明白为啥。
一个哥们的回答:

上链接:http://tech.ccidnet.com/art/3539/20080508/1443759_1.html
看来属于内存优化的内容了。你认真看看就都明白了。
作者: Miss小强    时间: 2013-5-8 20:19
黑马伍哲沂 发表于 2013-5-5 16:29
String、StringBuilder、StringBuf ...
  1. package cn.itcast.problem;

  2. public class StringBufferTest {

  3.         /**
  4.          * @param args
  5.          */
  6.         public static void main(String[] args) {
  7.                 long begin=System.currentTimeMillis();
  8.                 testStringBuilder();
  9.                 long end=System.currentTimeMillis();
  10.                 System.out.println("cost.."+(end-begin));
  11.         }

  12.         private static void testString() {
  13.                 String str=new String();
  14.                 //这里循环不能太大,否则会耗时很久,甚至内存溢出;
  15.                 for(int i=0;i<20000;i++){
  16.                         //这样最垃圾的操作,每次都会有很多的new String();对象;
  17.                         str+=" * "+i+" * ";
  18.                 }
  19.         }
  20.        
  21.         private static void testStringBuffer(){
  22.                 StringBuffer sb=new StringBuffer();
  23.                 for(int i=0;i<20000;i++){
  24.                         //这样用+号,每次+都会有一个StringBuilder创建出来
  25.                         sb.append(" * "+i+"  * ");
  26.                 }
  27.                 String str=sb.toString();
  28.         }
  29.        
  30.         private static void testStringBuffer2(){
  31.                 StringBuffer sb=new StringBuffer();
  32.                 for(long i=0;i<100000;i++){
  33.                         //注意和上面+有所不同,这样虽然更麻烦,但是更加高效;他只有一个StringBuffer;
  34.                         sb.append(" * ");
  35.                         sb.append(i);
  36.                         sb.append(" * ");
  37.                 }
  38.                 String str=sb.toString();
  39.         }
  40.        
  41.         private static void testStringBuilder(){
  42.                 StringBuilder sb=new StringBuilder();
  43.                 //这里为了看到效果设置大一点:
  44.                 for(long i=0;i<100000;i++){
  45.                         //这是最高效的方式。推荐使用;
  46.                         sb.append(" * ");
  47.                         sb.append(i);
  48.                         sb.append(" * ");
  49.                        
  50.                 }
  51.                 String str=sb.toString();
  52.         }
  53.        
  54.         private static void testStringBuilder2(){
  55.                 StringBuilder sb=new StringBuilder();
  56.                 for(long i=0;i<100000;i++){
  57.                         //这样不推荐使用,跟上面的一样。。。
  58.                         sb.append(" * "+i+"  * ");
  59.                 }
  60.                 String str=sb.toString();
  61.                
  62.         }

  63. }
复制代码
结论:使用StringBuilder而不是StringBuffer和String;
使用多个append而不是+号;
ps:看了你的帖子回去查了下资料,虽然试验了下,如书上所说,可是我找不到原因,悲剧啊。。
建议版主加精。。。
作者: Miss小强    时间: 2013-5-8 20:21
我注释的地方是书上说的,可是我不知道为什么,看源代码也找不到为什么。。。
这几天跟你的贴,学到了好多东西,真幸运。。。谢谢哥们了。。。
作者: 黑马伍哲沂    时间: 2013-5-8 20:26
Miss小强 发表于 2013-5-8 20:19
结论:使用StringBuilder而不是StringBuffer和String;
使用多个append而不是+号;
ps:看了你的帖子回去 ...

使用append而不使用+号  是可以理解的。  因为+号就是用来连接字符串的。
你在append方法里用+号,就是两个字符串的链接。两个字符串串联会产生很多多余对象。
之前好像看到有资料说+是调用了concat方法。但我不确定。

作者: Miss小强    时间: 2013-5-8 20:27
追问:突然想到,如果这样的话,

用char数组存多个中文字符 比用string好在哪?
这个问题的答案应该是使用字符数组更高效了??
作者: 黑马伍哲沂    时间: 2013-5-8 20:28
Miss小强 发表于 2013-5-8 20:21
我注释的地方是书上说的,可是我不知道为什么,看源代码也找不到为什么。。。
这几天跟你的贴,学到了好多 ...

客气了。多交流,相互学习。更容易进步。其实我也只是菜鸟。。。  不过我整理帖子会比较认真。所以,别的人看起来就好像我懂很多一样。。  都是假象  哈哈  努力中。。。  
作者: 黑马伍哲沂    时间: 2013-5-8 20:32
Miss小强 发表于 2013-5-8 20:27
追问:突然想到,如果这样的话,

用char数组存多个中文字符 比用string好在哪?

这个问题。我表示没有研究,不清楚就不乱说了。  但String类这里其实有个设计模式,叫做不变模式。  如果你想深究,你可以参考下不变模式,然后再找找字符数组与String的比较资料。  这个我暂时不想深究了。  因为还有很都多基础任务没完成。
作者: Miss小强    时间: 2013-5-8 20:36
黑马伍哲沂 发表于 2013-5-8 20:32
这个问题。我表示没有研究,不清楚就不乱说了。  但String类这里其实有个设计模式,叫做不变模式。  如果 ...

好吧,哥们加油。。。我在看看源代码去。。




欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2