字符串的分类
其实说起来会有些别扭,为什么字符串会有分类这种东西。了解的朋友可能会知道字符串的操作除了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对象(当常量池里面没有“ABCD”时就会想在常量池里面创建,然后在内存空间里面创建一个),至少创建一个String对象。new关键字绝对会在堆空间创建一块新的内存区域,所以至少创建一个String对象。
String对象的空值
一种是表示引用为空(null)的空值:
String str1 = null; // 没有初始化,没有分配内存空间
另外一种表示内容为空的空值:
String str2 = "; // 分配有内存空间,有内容。
所以当你需要判断字符串是否为空的时候,实际上应该这样:
字符串的比较
从上图可以明显看出,使用“==”,只能比较引用的内存地址是否相同,而使用“equals”方法,则比较的是字符串的内容。
我们可以跟到String类的equals方法:
这里补充一下instanceof这个关键字的作用:判断其左边对象是否为其右边类的实例,返回boolean类型的数据。可以用来判断继承中的子类的实例是否为父类的实现(简单的讲,就是一个类跟另外的一个类有亲戚关系,从object到最后的子类,只有你左边的类属于这个亲戚链子里面。就会返回true)
“+”号是怎么来连接字符串的
先来直接看一个简单的例子,程序中创建了三个String对象,str是hello和wrold两个字符串连接赋值后的对象,程序的结果很明显,但我们关心的是,hello和world是怎样连接起来的呢?
我们在XJad(Java反编译程序,把生成的class反编译成java)中打开刚刚生成的class文件会发现:
编译器自动引入了一个java.lang.StringBuilder类。虽然我们在源代码中并没有使用StringBuilder类,但是编译器却自作主张地使用了它,因为它更高效。
在这个例子中,编译器创建了一个StringBuilde对象,用以构造最终的String,并为每个字符串调用了一次StringBuilder的append()方法,总计两次。最后调用toString()生成结果。这是编译器自动优化的结果,包括自动生成的Tester()无参数默认的构造函数也是。
现在,你也许会觉得可以随意使用String对象,反正编译器会为你自动地优化性能。可是在这之前,我们先要看看编译器究竟能给我们优化到什么程度(下面再详细介绍StringBuilder)。
可变的字符串
StringBuilder/StringBuffer:当对象创建完毕之后,该对象的内容可以发生改变,当内容发生改变的时候,对象保持不变。
接着上面的问题,我们继续来看一个例子:
程序的结果显而易见,我们来看看反编译之后的代码:
可以看到,对比两个对象,后者的循环部分的代码更简短、更简单,而且它只生成了一个StringBuilder对象。
结论是:如果字符串操作比较简单,那就可以信赖编译器,它会为你合理地构造最终的字符串结果。但如果你还使用循环,多次地改变字符串的内容,那就更适合StringBuilder对象。
但是如果你想要走捷径,例如append(a+":"+c),则编译器就会调入陷阱,从而为你另外创建一个StringBuilder对象处理括号内的字符串操作
String对象的比较
StringBuilder是Java SE5引入的,在这之前Java用的是StringBuffer。后者线程安全(只需要了解,该对象方法中所有的方法都是用了synchronized修饰符),因此开销也会大。有没有用synchronized修饰符,就是这两者唯一的区别。我们可以简单地来比较一下这三个String对象在拼接字符串中的性能:
创建好三个方法,分别测试三个类型的对象的拼接效率:
面试题
最后再有一个String的面试题:
说说下面的String对象,彼此之间是否相等
如果你自己写几个判断相等的语句,分别判断str1和另外五个是否相等,则会发现:
str1和str2/str3相等,和另外几个都不相等。我们先来看一下反编译之后的代码:
知识点(纯干货):
1 单独使用""引号创建的字符串都是直接量,编译期就已经确定存储到常量池中;
2 使用new String("")创建的对象会存储到堆内存中,是运行期才创建;
3 使用只包含直接量的字符串连接符如"aa" + "bb"创建的也是直接量编译期就能确定,已经确定存储到常量池中(str2和str3);
4 使用包含String直接量(无final修饰符)的字符串表达式(如"aa" + s1)创建的对象是运行期才创建的,存储在堆中;
5 通过变量/调用方法去连接字符串,都只能在运行时期才能确定变量的值和方法的返回值,不存在编译优化操作.
个人GitHub项目,记录学习Java知识的过程 欢迎star