Java基础之String、StringBuffer、StringBuilder浅析
一、前言:
位于java.lang包下的String、StringBuilder、StringBuffer一般都是用来操作字符串的,这三个类都被final关键字修饰,不能被继承
相关的API我们可以在这里查看:https://docs.oracle.com/javase/8/docs/api/index.html
首先说明:
- String:字符串常量
- StringBuffer:字符串变量(线程安全的)
- StringBuilder:字符串变量(非线程安全的)
二、Java中的String
在这三个类中String类应该算是我们最熟悉的了吧
String的对象是不可变得。查看jdk文档你就会发现,String类中每一个看起来会修改String值的方法,实际上都是创建了一个全新的String对象,以包含修改后的字符串内容。而最初的String对象则丝毫未动
2.1 java里 String 类的 本质
String类的书面解释在本文前言已经提到过了, 是1个用于字符串的类.
但是这个解释并没有指明String类的本质.
我们知道, Java类的本质大致上可以理解为 成员(属性) 和 方法的集合体.
String类也一样, 只不过String类有1个关键的成员, 这个成员保存着数据区的某个字符串的内存地址. 可以理解为1个指针.
而String类的方法是一些对对应字符串的查询方法(例如indexOf(), charAt()等). 注意, 并没有对这个字符串进行修改的方法哦, 字符串是常量, 不能修改.
虽然String类不能修改字符串, 但是上面保存字符串地址的成员却是可以被改变的, 也就是说String类的对象可以指向另1个字符串.
见上图, java的String类实例化1个对象后, 会在堆区划分一块对象的内存, 其中1个关键成员存放的是数据区字符串的地址.
而下面若干个方法内存, 存放的是该函数(方法)在代码区的2进制代码的地址.
2.2 String类实例化对象的第一个方法. new String("abc")
当然, String类的构造函数有很多个(参数不同), 但是在coding中,常用的实例化对象方法无非是两种.
第一种就是与其他类一样, 利用构造方法:
上面的代码做了下面若干个事情.
- 在数据区中划分一块内存存放字符串, 值是"abc", 这块内存一旦创建, 值"abc" 不能被修改.
- 在堆区划分1块对象内存, 其中小块用于存放上面字符串的地址, 另一些用于存放函数指针.
- 在栈区划分一块内存, 存放上面堆区的头部地址.
下面是1个例子:
- package String_kng;
- public class String_4{
- public static void f(){
- String s = new String("cat");
- String s2 = new String("cat");
- System.out.printf("s: %s ", s);
- System.out.printf("s2: %s ", s2);
- System.out.println(s == s2);
- System.out.println(s.equals(s2));
- }
- }
上面利用new 实例化了两个对象s和s2 , 它们所指向的字符串值都是"cat"
然后用 "==" 和 equals来比较两者
输出:
- [java] s: cat
- [java] s2: cat
- [java] false
- [java] true
可见用equals 来比较s 和 s2, 它们是相等的, 因为它们的内容相同. 而且equals方法在String类里重写过了.
而用 "==" 比较的是两个对象s 和 s2所指向的地址, 它们所指向的地址是不同的.
如下图:
亦即两个new语句分别在数据区和堆区各自都划分2个内存.
数据区中有两个字符串内存, 它们的值是一样的都是"cat".
堆区有两个对象内存, 它们分别保存了各自对应的字符串地址.
而stuck区中两个s1 s2 保存了各自的堆区内存地址. 这两个地址明显是不同的. 也就是 s == s2 返回false的原因.
2.3 String类实例化对象的另一个方法. = "abc"
事实上, 我们在编程中新建1个字符串更多情况下会用如下的方式:
- String s = "abc";
这种方式更上面那种有什么区别呢?
- package String_kng;
- public class String_5{
- public static String g(){
- String s4 = "cat";
- return s4;
- }
- public static void f(){
- String s = new String("cat");
- String s2 = "cat";
- String s3 = "cat";
- System.out.printf("s: %s ", s);
- System.out.printf("s2: %s ", s2);
- System.out.printf("s3: %s ", s3);
- System.out.println(s == s2);
- System.out.println(s2 == s3);
- System.out.println(s2 == g());
- }
- }
这个例子步骤也不复杂:
首先f()方法里 利用第一种方法实例化了1个值为"cat"的对象s
然后里利用第二种方法 又 创建了两个String 对象s2 和 s3, 它们的值都是"cat".
然后用"==" 来比较它们.
然后f()方法调用g()方法, g()方法利用第二方式实例化了1个值为"cat"的String 对象s4
最后用 "==" 比较s2 和 s4 的地址.
输出:
- [java] s: cat
- [java] s2: cat
- [java] s3: cat
- [java] false
- [java] true
- [java] true
由结果得知, s4 和 s2 和 s3的地址是相同的! 而由第一种方法创建的s 跟前面三者地址不同.
我们来分析一下,为什么会出现这样的结果:
首先我们要明白的是
String str = "abc";
这样的代码,可能会创建一个对象或者不会创建对象:这里会出现一个名词“字符串实例池”
- 实例池中存在字符串:
这个名词很形象,在这个字符串实例池中,存放着很多字符串,可能包含有字符串:"abc",所以
在这种情况下面,上面的语句是不会创建对象的,而是直接引用池子中的字符串:"abc";
- 实例池中不存在字符串:
如果字符串"abc"在实例池中并不存在,那么这时,就会初始化一个字符串:"abc",即创建
一个字符串对象:"abc",并且会把创建好的字符串放入到"字符串实例池"中。
String str = new String("abc");
对于关键字:new ,即会产生新的对象,也就是说,每次都会产生新的字符串对象
所以结论如下:
利用 = "cat" 方式创建1个String对象时, java 首先会检测当前进程的数据区是否有1个以相同方式创建的值是一样的字符串存在.
如果无, 则类似 new Sring("cat")方式, 在数据区和堆区都各自划分一块新内存, 用于该创建的对象.
如果有, 则直接把该对象的地址指向 已存在的堆区内存地址.
也就是讲, 在f() 里的String s2 = "cat" 相当于执行了 String s2 = new String("cat");
而在f()里的 String s3 = "cat" 相当执行了String s3 = s2;
而在g()里, 理论上g()是不能访问f()里的 局部变量的, 但是g()还是检测到数据区存在用相同方式创建而且值1个样的字符串.
所以s4 也指向了堆区的那一块内存.
如下图:
这个例子说明了, 在同1个java程序中, 所有用 " = "abc" " 方式创建的而且具有相同值的多个String对象其实都是同1个对象. 因为它们指向同一块堆区的内存.
由于这种特性, 所以这种用" = "abc"" 方式创建的对象十分适合做 synchronized对象锁 要锁的对象. 不用担心锁的是两个不同的对象导致 多线程同步失败.
2.4.String类的常用方法.
1.public char charAt(int index) //返回字符串中第index个字符
2.public int length() //返回字符串的长度
3.public int indexOf(String str)//返回字符串中出现str的第1个位置
4.public int indexOf(String str, int fromIndex)//返回字符串中, 从第fromIndex个字符数起, 出现str的第1个位置, 这个方法是上面方法的重载
5.public boolean equalsIgnoreCase(String str)//忽略大小写, 比较两个字符是否相等.
6.public String replace(char oldChar, char newChar)//返回1个新字符串, 该新字符串内的oldChar被newChar替换掉, 注意旧字符串没有被修改.
7.public boolean startsWith(String prefix)//判断字符串是否以 prefix 开头
8.public boolean endsWith(String suffix)//判断字符产是否以suffix 结尾
9.public String subString(int beginIndex)//截取从第beginIndex个字符开始到最后1个字符, 返回1个新字符串
10.public String subString(int beginIndex, int endIndex)//截取从第beginIndex个字符开始, 第endIndex个字符, 返回1个新字符串, 是上面方法的重载
11.public static String valueOf(...)//注意这个是静态方法. 可以把其他基本数据类型转换成String对象
12.Integer.parseInt(String s)//这个是另1个类Integer 的方法, 可以把字符串转换成int类型. 会抛出异常..
三、Java中的StringBuffer
StringBuffer类和String一样,也用来代表字符串,只是由于StringBuffer的内部实现方式和String不同,所以StringBuffer在进行字符串处理时,不生成新的对象,在内存使用上要优于String类。
所以在实际使用时,如果经常需要对一个字符串进行修改,例如插入、删除等操作,使用StringBuffer要更加适合一些。
在StringBuffer类中存在很多和String类一样的方法,这些方法在功能上和String类中的功能是完全一样的。
但是有一个最显著的区别在于,对于StringBuffer对象的每次修改都会改变对象自身,这点是和String类最大的区别。
另外由于StringBuffer是线程安全的,关于线程的概念后续有专门的章节进行介绍,所以在多线程程序中也可以很方便的进行使用,但是程序的执行效率相对来说就要稍微慢一些。
下面谈谈StringBuffer的使用:
- StringBuffer对象的初始化
StringBuffer对象的初始化不像String类的初始化一样,Java提供的有特殊的语法,而通常情况下一般使用构造方法进行初始化。
例如: StringBuffer s = new StringBuffer();
这样初始化出的StringBuffer对象是一个空的对象。
如果需要创建带有内容的StringBuffer对象,则可以使用:
StringBuffer s = new StringBuffer(“abc”);
这样初始化出的StringBuffer对象的内容就是字符串”abc”。
需要注意的是,StringBuffer和String属于不同的类型,也不能直接进行强制类型转换,下面的代码都是错误的:
StringBuffer s = “abc”; //赋值类型不匹配
StringBuffer s = (StringBuffer)”abc”; //不存在继承关系,无法进行强转
StringBuffer对象和String对象之间的互转的代码如下:
String s = “abc”;
StringBuffer sb1 = new StringBuffer(“123”);
StringBuffer sb2 = new StringBuffer(s); //String转换为StringBuffer
String s1 = sb1.toString(); //StringBuffer转换为String
- StringBuffer的常用方法
StringBuffer类中的方法主要偏重于对于字符串的变化,例如追加、插入和删除等,这个也是StringBuffer和String类的主要区别。
①append方法
public StringBuffer append(boolean b)
该方法的作用是追加内容到当前StringBuffer对象的末尾,类似于字符串的连接。调用该方法以后,StringBuffer对象的内容也发生改变, 例如:
StringBuffer sb = new StringBuffer(“abc”);
sb.append(true);
则对象sb的值将变成”abctrue”。
使用该方法进行字符串的连接,将比String更加节约内容,例如应用于数据库SQL语句的连接,例如:
StringBuffer sb = new StringBuffer();
String user = “test”;
String pwd = “123”;
sb.append(“select * from userInfo where username=“).append(user).append(“ and pwd=”).append(pwd);
这样对象sb的值就是字符串“select * from userInfo where username=test and pwd=123”。
②deleteCharAt方法
public StringBuffer deleteCharAt(int index)
该方法的作用是删除指定位置的字符,然后将剩余的内容形成新的字符串。例如:
StringBuffer sb = new StringBuffer(“Test”);
sb. deleteCharAt(1);
该代码的作用删除字符串对象sb中索引值为1的字符,也就是删除第二个字符,剩余的内容组成一个新的字符串。所以对象sb的值变为”Tst”。
还存在一个功能类似的delete方法:
public StringBuffer delete(int start,int end)
该方法的作用是删除指定区间以内的所有字符,包含start,不包含end索引值的区间。例如:
StringBuffer sb = new StringBuffer(“TestString”);
sb. delete (1,4);
该代码的作用是删除索引值1(包括)到索引值4(不包括)之间的所有字符,剩余的字符形成新的字符串。则对象sb的值是”TString”。
③insert方法
public StringBuffer insert(int offset, boolean b)
该方法的作用是在StringBuffer对象中插入内容,然后形成新的字符串。
例如:
StringBuffer sb = new StringBuffer(“TestString”);
sb.insert(4,false);
该示例代码的作用是在对象sb的索引值4的位置插入false值,形成新的字符串,则执行以后对象sb的值是”TestfalseString”。
④reverse方法
public StringBuffer reverse()
该方法的作用是将StringBuffer对象中的内容反转,然后形成新的字符串。例如:
StringBuffer sb = new StringBuffer(“abc”);
sb.reverse();
经过反转以后,对象sb中的内容将变为”cba”。
⑤setCharAt方法
public void setCharAt(int index, char ch)
该方法的作用是修改对象中索引值为index位置的字符为新的字符ch。例如:
StringBuffer sb = new StringBuffer(“abc”);
sb.setCharAt(1,’D’);
则对象sb的值将变成”aDc”。
⑥trimToSize方法
public void trimToSize()
该方法的作用是将StringBuffer对象的中存储空间缩小到和字符串长度一样的长度,减少空间的浪费。
总之,在实际使用时,String和StringBuffer各有优势和不足,可以根据具体的使用环境,选择对应的类型进行使用
四、Java中的StringBuilder
一个可变的字符序列。此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。 在 StringBuilder 上的主要操作是 append 和 insert 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符添加或插入到字符串生成器中。append 方法始终将这些字符添加到生成器的末端;而 insert 方法则在指定的点添加字符。
StringBuilder中的常用方法:
(1)Append 方法可用来将文本或对象的字符串表示形式添加到由当前 StringBuilder对象表示的字符串的结尾处。以下示例将一个 StringBuilder对象初始化为“Hello World”,然后将一些文本追加到该对象的结尾处。将根据需要自动分配空间。
StringBuilderMyStringBuilder = new StringBuilder("Hello World!");
MyStringBuilder.Append(" What a beautiful day."); Console.WriteLine(MyStringBuilder);
此示例将 Hello World! What abeautiful day.显示到控制台。
(2)AppendFormat 方法将文本添加到 StringBuilder的结尾处,而且实现了 IFormattable接口,因此可接受格式化部分中描述的标准格式字符串。可以使用此方法来自定义变量的格式并将这些值追加到 StringBuilder的后面。以下示例使用 AppendFormat方法将一个设置为货币值格式的整数值放置到 StringBuilder的结尾。
int MyInt= 25;
StringBuilder MyStringBuilder = new StringBuilder("Your total is ");
MyStringBuilder.AppendFormat("{0:C} ", MyInt);
Console.WriteLine(MyStringBuilder);
此示例将 Your total is $25.00显示到控制台。
(3)Insert 方法将字符串或对象添加到当前 StringBuilder中的指定位置。以下示例使用此方法将一个单词插入到 StringBuilder的第六个位置。
StringBuilderMyStringBuilder = new StringBuilder("Hello World!");
MyStringBuilder.Insert(6,"Beautiful ");
Console.WriteLine(MyStringBuilder);
此示例将 Hello BeautifulWorld!显示到控制台。
(4)可以使用 Remove方法从当前 StringBuilder中移除指定数量的字符,移除过程从指定的从零开始的索引处开始。以下示例使用 Remove方法缩短 StringBuilder。
StringBuilderMyStringBuilder = new StringBuilder("Hello World!");
MyStringBuilder.Remove(5,7);
Console.WriteLine(MyStringBuilder);
此示例将 Hello显示到控制台。
(5)使用 Replace方法,可以用另一个指定的字符来替换 StringBuilder对象内的字符。以下示例使用 Replace方法来搜索 StringBuilder对象,查找所有的感叹号字符 (!),并用问号字符 (?)来替换它们。
StringBuilderMyStringBuilder = new StringBuilder("Hello World!");
MyStringBuilder.Replace('!', '?');
Console.WriteLine(MyStringBuilder);
五、三者的区别:
这三个类之间的区别主要是在两个方面,即运行速度和线程安全这两方面。
- 首先说运行速度,或者说是执行速度,在这方面运行速度快慢为:StringBuilder > StringBuffer > String
String最慢的原因:
String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。以下面一段代码为例:
1 String str="abc";
2 System.out.println(str);
3 str=str+"de";
4 System.out.println(str);
如果运行这段代码会发现先输出“abc”,然后又输出“abcde”,好像是str这个对象被更改了,其实,这只是一种假象罢了,JVM对于这几行代码是这样处理的,首先创建一个String对象str,并把“abc”赋值给str,然后在第三行中,其实JVM又创建了一个新的对象也名为str,然后再把原来的str的值和“de”加起来再赋值给新的str,而原来的str就会被JVM的垃圾回收机制(GC)给回收掉了,所以,str实际上并没有被更改,也就是前面说的String对象一旦创建之后就不可更改了。所以,Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。
而StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度要比String快很多。
另外,有时候我们会这样对字符串进行赋值
1.String str="abc"+"de";
2 StringBuilder stringBuilder=new StringBuilder().append("abc").append("de");
3 System.out.println(str);
4 System.out.println(stringBuilder.toString());
这样输出结果也是“abcde”和“abcde”,但是String的速度却比StringBuilder的反应速度要快很多,这是因为第1行中的操作和
String str="abcde";
是完全一样的,所以会很快,而如果写成下面这种形式
1 String str1="abc";
2 String str2="de";
3 String str=str1+str2;
那么JVM就会像上面说的那样,不断的创建、回收对象来进行这个操作了。速度就会很慢。
- 再来说线程安全
在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的
如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。
- 总结一下
String:适用于少量的字符串操作的情况
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
参考文章:
http://www.360doc.com/content/18/0120/12/52205844_723589162.shtml
https://www.cnblogs.com/springcsc/archive/2009/12/03/1616330.html
http://blog.csdn.net/l_kanglin/article/details/53291301