String 与 StringBuffer解析

本文详细解析Java中的String类与StringBuffer类的区别、使用场景及注意事项,包括对象引用、内容比较、字符串不可变性、内存管理等方面,帮助开发者更高效地使用Java字符串相关功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题一:我声明了什么!

String s = "Hello world!";

许多人都做过这样的事情,但是,我们到底声明了什么?回答通常是:一个String,内容是“Hello world!”。这样模糊的回答通常是概念不清的根源。如果要准确的回答,一半的人大概会回答错误。

这个语句声明的是一个指向对象的引用,名为“s”,可以指向类型为String的任何对象,目前指向"Hello world!"这个String类型的对象。这就是真正发生的事情。我们并没有声明一个String对象,我们只是声明了一个只能指向String对象的引用变量。所以,如果在刚才那句语句后面,如果再运行一句:

String string = s;

我们是声明了另外一个只能指向String对象的引用,名为string,并没有第二个对象产生,string还是指向原来那个对象,也就是,和s指向同一个对象。

 

问题二:"=="和equals方法究竟有什么区别?

==操作符专门用来比较变量的值是否相等。比较好理解的一点是:

int a=10;

int b=10;

则a==b将是true。

但不好理解的地方是:

String a=new String("foo");

String b=new String("foo");

则a==b将返回false。

根据前一帖说过,对象变量其实是一个引用,它们的值是指向对象所在的内存地址,而不是对象本身a和b都使用了new操作符,意味着将在内存中产生两个内容为"foo"的字符串,既然是“两个”,它们自然位于不同的内存地址。a和b的值其实是两个不同的内存地址的值,所以使用"=="操作符,结果会是false。诚然,a和b所指的对象,它们的内容都是"foo",应该是“相等”,但是==操作符并不涉及到对象内容的比较。

对象内容的比较,正是equals方法做的事。

看一下Object对象的equals方法是如何实现的:

boolean equals(Object o){

return this==o;

}

Object对象默认使用了==操作符。所以如果你自创的类没有覆盖equals方法,那你的类使用equals和使用==会得到同样的结果。同样也可以看出,Object的equals方法没有达到equals方法应该达到的目标:比较两个对象内容是否相等。因为答案应该由类的创建者决定,所以Object把这个任务留给了类的创建者。

看一下一个极端的类:

Class Monster{

private String content;

...

boolean equals(Object another){

return true;

}

}

我覆盖了equals方法。这个实现会导致无论Monster实例内容如何,它们之间的比较永远返回true。

所以当你是用equals方法判断对象的内容是否相等,请不要想当然。因为可能你认为相等,而这个类的作者不这样认为,而类的equals方法的实现是由他掌握的。如果你需要使用equals方法,或者使用任何基于散列码的集合(HashSet,HashMap,HashTable),请察看一下java doc以确认这个类的equals逻辑是如何实现的。

 

问题三:String到底变了没有?

没有。因为String被设计成不可变(immutable)类,所以它的所有对象都是不可变对象。请看下列代码:

String s = "Hello";

s = s + " world!";

s所指向的对象是否改变了呢?从本系列第一篇的结论很容易导出这个结论。我们来看看发生了什么事情。在这段代码中,s原先指向一个String对象,内容是"Hello",然后我们对s进行了+操作,那么s所指向的那个对象是否发生了改变呢?答案是没有。这时,s不指向原来那个对象了,而指向了另一个String对象,内容为"Hello world!",原来那个对象还存在于内存之中,只是s这个引用变量不再指向它了。

通过上面的说明,我们很容易导出另一个结论,如果经常对字符串进行各种各样的修改,或者说,不可预见的修改,那么使用String来代表字符串的话会引起很大的内存开销。因为String对象建立之后不能再改变,所以对于每一个不同的字符串,都需要一个String对象来表示。这时,应该考虑使用StringBuffer类,它允许修改,而不是每个不同的字符串都要生成一个新的对象。并且,这两种类的对象转换十分容易。

同时,我们还可以知道,如果要使用内容相同的字符串,不必每次都new一个String。例如我们要在构造器中对一个名叫s的String引用变量进行初始化,把它设置为初始值,应当这样做:

public class Demo {

private String s;

...

public Demo {

s = "Initial Value";

}

...

}

而非

s = new String("Initial Value");

后者每次都会调用构造器,生成新对象,性能低下且内存开销大,并且没有意义,因为String对象不可改变,所以对于内容相同的字符串,只要一个String对象来表示就可以了。也就说,多次调用上面的构造器创建多个对象,他们的String类型属性s都指向同一个对象。

上面的结论还基于这样一个事实:对于字符串常量,如果内容相同,Java认为它们代表同一个String对象。而用关键字new调用构造器,总是会创建一个新的对象,无论内容是否相同。

至于为什么要把String类设计成不可变类,是它的用途决定的。其实不只String,很多Java标准类库中的类都是不可变的。在开发一个系统的时候,我们有时候也需要设计不可变类,来传递一组相关的值,这也是面向对象思想的体现。不可变类有一些优点,比如因为它的对象是只读的,所以多线程并发访问也不会有任何问题。当然也有一些缺点,比如每个不同的状态都要一个对象来代表,可能会造成性能上的问题。所以Java标准类库还提供了一个可变版本,即StringBuffer。

 

大家先来看看一段奇怪的程序:

public class TestString {

public static void main(String[] args) {

String s1 = "Monday";

String s2 = "Monday";

}

}

这个程序真是简单啊!可是有什么问题呢?

1.  来自 String 的忧虑

上面这段程序中,到底有几个对象呢?

可能很多人脱口而出:两个,s1 和 s2

为什么?

String 是 final 类,它的值不可变。

看起来似乎很有道理,那么来检测一下吧,稍微改动一下程序

就可以看到结果了:

public class TestString {

public static void main(String[] args) {

String s1 = "Monday";

String s2 = "Monday";

if (s1 == s2)

System.out.println("s1 == s2");

else

System.out.println("s1 != s2");

}

}

呵呵,很多人都会说已经不止两个对象了

编译并运行程序,输出:s1 == s2

啊! 为什么 s1 == s2 ?

== 分明是在说:s1 与 s2 引用同一个 String 对象 -- "Monday"!

2.  千变万化的 String

再稍微改动一下程序,会有更奇怪的发现:

public class TestString {

public static void main(String[] args) {

String s1 = "Monday";

String s2 = new String("Monday");

if (s1 == s2)

System.out.println("s1 == s2");

else

System.out.println("s1 != s2");

if (s1.equals(s2))

System.out.println("s1 equals s2");

else

System.out.println("s1 not equals s");

}

}

我们将 s2 用 new 操作符创建

程序输出:

s1 != s2

s1 equals s2

嗯,很明显嘛

s1 s2分别引用了两个"Monday"String对象

可是为什么两段程序不一样呢?

 

3.  在 String 的游泳池中游泳

原来,程序在运行的时候会创建一个字符串缓冲池,当使用 s2 = "Monday" 这样的表达是创建字符串的时候,程序首先会在这个String缓冲池中寻找相同值的对象,在第一个程序中,s1先被放到了池中,所以在s2被创建的时候,程序找到了具有相同值的 s1,将 s2 引用 s1 所引用的对象"Monday" ,第二段程序中,使用了 new 操作符,他明白的告诉程序: “我要一个新的!不要旧的!”与是一个新的"Monday"Sting对象被创建在内存中。他们的值相同,但是位置不同,一个在池中游泳,一个在岸边休息。哎呀,真是资源浪费,明明是一样的非要分开做什么呢?

 

4.  继续潜水

再次更改程序:

public class TestString {

public static void main(String[] args) {

String s1 = "Monday";

String s2 = new String("Monday");

s2 = s2.intern();

if (s1 == s2)

System.out.println("s1 == s2");

else

System.out.println("s1 != s2");

if (s1.equals(s2))

System.out.println("s1 equals s2");

else

System.out.println("s1 not equals s2");

}

}

这次加入:s2 = s2.intern();

哇!程序输出:

s1 == s2

s1 equals s2

原来,程序新建了 s2 之后,又用intern()把他打翻在了池里

哈哈,这次 s2 和 s1 有引用了同样的对象了,我们成功的减少了内存的占用

 

5.  == 与 equals() 的争斗

String 是个对象,要对比两个不同的String对象的值是否相同,明显的要用到 equals() 这个方法,可是如果程序里面有那么多的String对象,有那么多次的要用到 equals ,哦,天哪,真慢啊

更好的办法:

把所有的String都intern()到缓冲池去吧,最好在用到new的时候就进行这个操作

String s2 = new String("Monday").intern();

嗯,大家都在水池里泡着了吗?哈哈

现在我可以无所顾忌的用 == 来比较 String 对象的值了

真是爽啊,又快又方便!

 

1.  回顾一下坏脾气的 String 老弟

例程1:

class Str {

public static void main(String[] args) {

String s = "Hi!";

String t = "Hi!";

if (s == t)

System.out.println("equals");

else

System.out.println("not equals");

}

}

程序输出什么呢?

程序输出:equals

 

2.  哦,天哪,它又在搅混水了

例程2:

class Str {

public static void main(String[] args) {

String s = "HELLO";

String t = s.toUpperCase();

if (s == t)

System.out.println("equals");

else

System.out.println("not equals");

}

}

那么这个程序有输出什么呢?

慎重!再慎重!不要被 String 这个迷乱的家伙所迷惑!

它输出:equals

 

把程序简单的更改一下:

class Str2 {

public static void main(String[] args) {

String s = "Hello";

String t = s.toUpperCase();

if (s == t)

System.out.println("equals");

else

System.out.println("not equals");

}

}

你可能会说:不是样吗?

不!千真万确的,不一样!这一次输出:not equals

 

3.  你了解你的马吗?

“要驯服脱缰的野马,就要了解它的秉性”牛仔们说道。

你了解 String 吗?

解读 String 的 API ,可以看到:

toUpperCase() 和 toLowerCase() 方法返回一个新的String对象, 它将原字符串表示字符串的大写或小写形势; 但是要注意:如果原字符串本身就是大写形式或小写形式,那么返回原始对象。

这就是为什么第二个程序中 s 和 t 纠缠不清的缘故

对待这个淘气的、屡教不改的 String ,似乎没有更好的办法了

让我们解剖它,看看它到底有什么结构吧:

(1)  charAt(int n) 返回字符串内n位置的字符,第一个字符位置为0,

最后一个字符的位置为length()-1,访问错误的位置会扔出一块大砖头:

StringIndexOutOfBoundsException 真够大的

(2) concat(String str) 在原对象之后连接一个 str ,但是返回一个新的 String 对象

(3) EqualsIgnoreCase(String str) 忽略大小写的 equals 方法

这个方法的实质是首先调用静态字符方法toUpperCase() 或者 toLowerCase() ,将对比的两个字符转换,然后进行 == 运算

(4) trim() 返回一个新的对象,它将原对象的开头和结尾的空白字符切掉,同样的,如果结果与原对象没有差别,则返回原对象

(5) toString() String 类也有 toString() 方法吗?

真是一个有趣的问题,可是如果没有它,你的 String 对象说不定真的不能用在System.out.println() 里面啊
小心,它返回对象自己

String 类还有很多其他方法,掌握他们会带来很多方便

也会有很多困惑,所以坚持原则,是最关键的

 

4.  我想买一匹更好的马

来购买更驯服温和的 String 的小弟 StringBuffer 吧

这时候会有人反对:它很好用,它效率很高,它怎么能够是小弟呢?

很简单,它的交互功能要比 String 少,如果你要编辑字符串,它并不方便,你会对它失望,但这不意味着它不强大

public final class String implements Serializable, Comparable, CharSequence

public final class StringBuffer implements Serializable, CharSequence

很明显的,小弟少了一些东东,不过这不会干扰它的前途

StringBuffer 不是由 String 继承来的,不过要注意兄弟它也是 final 啊,本是同根生

看看他的方法吧,这么多稳定可靠的方法,用起来比顽皮的 String 要有效率的多

Java 为需要改变的字符串对象提供了独立的 StringBuffer 类

它的实例不可变(final),之所以要把他们分开是因为,字符串的修改要求系统的开销量增大,占用更多的空间也更复杂,相信当有10000人挤在一个狭小的游泳池里游泳 而岸边又有10000人等待进入游泳池而焦急上火 又有10000人在旁边看热闹的时候,你这个 String 游泳池的管理员也会焦头烂额

在你无需改变字符串的情况下,简单的 String 类就足够你使唤的了,而当要频繁的更改字符串的内容的时候,就要借助于宰相肚里能撑船的StringBuffer 了

 

5. 宰相肚里能撑船

(1) length() 与 capacity()

String 中的 length() 返回字符串的长度,兄弟 StringBuffer 也是如此,他们都由对象包含的字符长度决定 capacity()呢?

public class TestCapacity {

public static void main(String[] args){

StringBuffer buf = new StringBuffer("it was the age of wisdom,");

System.out.println("buf = " + buf);

System.out.println("buf.length() = " + buf.length());

System.out.println("buf.capacity() = " + buf.capacity());

String str = buf.toString();

System.out.println("str = " + str);

System.out.println("str.length() = " + str.length());

buf.append(" " + str.substring(0,18)).append("foolishness,");

System.out.println("buf = " + buf);

System.out.println("buf.length() = " + buf.length());

System.out.println("buf.capacity() = " + buf.capacity());

System.out.println("str = " + str);

}

}

程序输出:

buf = it was the age of wisdom.

buf.length() = 25

buf.capacity() = 41

str = it was the age of wisdom

str.length() = 25

buf = it was the age of wisdom, it was the age of foolishness,

buf.length() = 56

buf.capacity() = 84

str = it was the age of wisdom,

可以看到,在内容更改之后,capacity也随之改变了

长度随着向字符串添加字符而增加

而容量只是在新的长度超过了现在的容量之后才增加

StringBuffer 的容量在操作系统需要的时候是自动改变的

程序员们对capacity所能够做的仅仅是可以在初始化 StringBuffer对象的时候,为它强行的分配一个固定的capacity,但是之后,它愿不愿以改变就完全看自己的了,capacity 会随着 length 超出而改变,总之,它总是要比 length 大

 

5.  把好你的舵不要翻船

StringBuffer的胃口看起来很好啊,看起来似乎它可以随意的变成一艘航空母舰

不过请注意:

capacity 的每一次更改和变化,整个对象必须被重建并且移动到另外一个内存区,为了避免类似的溢出,最好在初始化的时候为他分配足够的容量

如果你是船长,在航行中突然下令:“我们准备弃船,换另外一艘大船去啦!”,你的水手们怎么想?“让我们自杀吗?”,所以出发前尽量准备一艘合适的船,不要超载,也不要太大,千万不要在中途翻船啊!

StringBuffer 还有一个炫耀的调皮方法:reverse()

它可以让它肚子里面的东西反一个个儿

 

6.  大马吃小马

哦,天哪,String 和 StringBuffer 用谁都会使性子

应该如何是好呢?

(1)如果你的 String 仅出现极少数次,随他们去吧,毕竟草原很辽阔

(2)如果他们数目惊人混乱不堪挤作一团到处破坏,将他们 intern() ,学会游泳比淹死了强

另外配合着 StringBuffer 的使用,它很像这群乱马的头头可以舒舒服服的调教哪些野性的 String ,把他们头接尾、尾接头的组合起来形成一个团体,更容易控制和管理(不过令人沮丧的是它的编辑功能不太好)

不过要小心,虽然StringBuffer 的肚子很大,但是不要把它撑破了,适当的给它一个空间,它会发挥更好的作用

(2)  小心谨慎的对待 == 和 equals()

千万不要忘了本,他们都是对象

你可以用各种高效率的办法进行尝试(intern() 然后 ==)

但是在最终的原则上要把握好

(3)  String 和 StringBuffer不可以互相使用 equals()

不要忘了他们不是同类

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值