String类详解

字符串的分类

其实说起来会有些别扭,为什么字符串会有分类这种东西。了解的朋友可能会知道字符串的操作除了String,还有StringBuffer和StringBuilder(区别我们在下面来说)

不可变的字符串

String是一个奇葩。

String对象不可变,也就是说当对象创建完毕之后,该对象的内容(字符序列)是不允许改变的,如果内容改变则会创建一个新的String对象,返回到原地址中。

细心的朋友也许会发现,String类维护的char数组不仅被final所修饰,并且查看JDK源码你就会发现,String类中每一个看起来会修改String值得方法,实际上都是创建了一个全新的String对象,以包含修改后的字符串对象。而最初的String对象则丝毫未动。我们可以简单的来看一个实例(从替换操作中就能明显看出):

replace方法就是替换字符串中的内容,
如果替换之后跟原来的字符串相同则返回this,
如果不相同则new一个新的对象返回。这明显体现了内容改变则返回新对象而不是直接修改String对象的值。

 

表面的错觉

关于String对象是否可变,有些操作确实会给人错觉,先来看一段程序:

 

从结果来看,s1的值最初是“A”,经过赋值以后,变成了“C”,经过字符串连接运算并赋值以后,变成了“BC”。String对象的内容真的改变了吗?实际上,这只是错觉而已。有疑惑的朋友可以去看我的上一篇笔记,你就能知道:

String对象“A”,“B”,“C”在全程中都没有任何改变,改变的只是引用s1所指向的内容,也就是s1的值。

String对象的创建

有两种方式:

// 第一种:直接赋一个字面量
String str1 = "ABCD";
// 第二种:通过构造器创建
String str2 = new String("ABCD");

 上述的两种方法分别创建了几个String对象?

回答这个问题也特别简单,首先你需要直到JVM的内存模型是怎样的,在上一篇笔记中也有简单提到,这里需要补充的是:常量池(专门存储常量的地方,都指的是方法区中)分为编译常量池(不研究,存储字节码的相关信息)和运行常量池(存储常量数据)。

 

  • 当执行第一句话的时候,会在常量池中添加一个新的ABCD字符,str1指向常量池的ABCD
  • 当执行第二句话的时候,因为有new操作符,所以会在堆空间新开辟一块空间用来存储新的String对象,因为此时常量池中已经有了ABCD字符,所以堆中的String对象指向常量池中的ABCD,而str2则指向堆空间中的String对象。
所以结论:
String str1 = "ABCD";
最多创建一个String对象,最少不创建String对象.如果常量池中,存在”ABCD”,那么str1直接引用,此时不创建String对象.否则,先在常量池先创建”ABCD”内存空间,再引用.
String str2 = new String("ABCD");
最多创建两个String对象,至少创建一个String对象。new关键字绝对会在堆空间创建一块新的内存区域,所以至少创建一个String对象。

 

 

String对象的空值

一种是表示引用为空(null)的空值:

String str1 = null;  // 没有初始化,没有分配内存空间

 另外一种表示内容为空的空值:

String str2 = ";  // 分配有内存空间,有内容。

所以当你需要判断字符串是否为空的时候,实际上应该这样:

 

 

字符串的比较

从上图可以明显看出,使用“==”,只能比较引用的内存地址是否相同,而使用“equals”方法,则比较的是字符串的内容。

我们可以跟到String类的equals方法:

 

“+”号是怎么来连接字符串的 

先来直接看一个简单的例子,程序中创建了三个String对象,str是hello和wrold两个字符串连接赋值后的对象,程序的结果很明显,但我们关心的是,hello和world是怎样连接起来的呢?

我们在XJad(Java反编译程序,把生成的class反编译成java)中打开刚刚生成的class文件会发现:

编译器自动引入了一个java.lang.StringBuilder类。虽然我们在源代码中并没有使用StringBuilder类,但是编译器却自作主张地使用了它,因为它更高效。

在这个例子中,编译器创建了一个StringBuilde对象,用以构造最终的String,并为每个字符串调用了一次StringBuilderappend()方法,总计两次。最后调用toString()生成结果。这是编译器自动优化的结果,包括自动生成的Tester()无参数默认的构造函数也是。

现在,你也许会觉得可以随意使用String对象,反正编译器会为你自动地优化性能。可是在这之前,我们先要看看编译器究竟能给我们优化到什么程度(下面再详细介绍StringBuilder)。

可变的字符串

StringBuilder/StringBuffer:当对象创建完毕之后,该对象的内容可以发生改变,当内容发生改变的时候,对象保持不变。

接着上面的问题,我们继续来看一个例子:

程序和程序的结果

 

可以看到,对比两个对象,后者的循环部分的代码更简短、更简单,而且它只生成了一个StringBuilder对象。

结论是:如果字符串操作比较简单,那就可以信赖编译器,它会为你合理地构造最终的字符串结果。但如果你还使用循环,多次地改变字符串的内容,那就更适合StringBuilder对象。

但是如果你想要走捷径,例如append(a+":"+c),则编译器就会调入陷阱,从而为你另外创建一个StringBuilder对象处理括号内的字符串操作。

 

String对象的比较

StringBuilder是Java SE5引入的,在这之前Java用的是StringBuffer。后者线程安全(只需要了解,该对象方法中所有的方法都是用了synchronized修饰符),因此开销也会大。有没有用synchronized修饰符,就是这两者唯一的区别。我们可以简单地来比较一下这三个String对象在拼接字符串中的性能:

创建好三个方法,分别测试三个类型的对象的拼接效率:

 

面试题

最后再有一个String的面试题:

 

如果你自己写几个判断相等的语句,分别判断str1和另外五个是否相等,则会发现:
str1和str2/str3相等,和另外几个都不相等。我们先来看一下反编译之后的代码:

 

知识点(纯干货):

  • 单独使用""引号创建的字符串都是直接量,编译期就已经确定存储到常量池中;
  • 使用new String("")创建的对象会存储到堆内存中,是运行期才创建
  • 使用只包含直接量的字符串连接符如"aa" + "bb"创建的也是直接量编译期就能确定,已经确定存储到常量池中(str2和str3);
  • 使用包含String直接量(无final修饰符)的字符串表达式(如"aa" + s1)创建的对象是运行期才创建的,存储在堆中;
  • 通过变量/调用方法去连接字符串,都只能在运行时期才能确定变量的值和方法的返回值,不存在编译优化操作.

文章结尾

其实还想写关于正则表达的东西的,还是改天找时间另外研究研究写一篇像样的吧。关于String的操作,就简单给一下图吧,感兴趣也可以自己百度或者跟踪进源代码里面去看,这里就不细说了:

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值