本文转载自微信公众号《飞天小牛》,作者飞天小牛。转载本文请联系飞天小牛公众号。字符串操作无疑是计算机编程中最常见的行为之一,尤其是在Java发挥很大作用的Web系统中。全文思维导图如下:一、三剑客之首:不可变String概述“Java没有内置的字符串类型”,而是在标准Java中提供了一个“预定义类”String类库。每个“用双引号括起来的字符串都是String类的一个实例”:Stringe="";//空字符串Stringstr="hello";看看String的源码,“在Java8中,String内部是使用char数组来存储数据”。publicfinalclassStringimplementsjava.io.Serializable,Comparable,CharSequence{/**Thevalueisusedforcharactersstorage.*/privatefinalcharvalue[];}可见String类是final修饰的,所以“String类不允许被继承”。“Java9之后,String类的实现使用字节数组来存储字符串”,使用coder来识别使用了哪种编码。publicfinalclassStringimplementsjava.io.Serializable,Comparable,CharSequence{/**Thevalueisusedforcharacterstorage.*/privatefinalbyte[]value;/**Theidentifieroftheencodingusedtoencodethebytesin{@codevalue}.*/privatefinalbytecoder;}但是不管是Java8还是Java9,“用于存储数据的char或byte数组值一直声明为final”,意思是value数组初始化后,不能再引用其他数组。并且String内部没有改变值数组的方法,所以我们说String是不可变的。所谓不可变,就像数字3永远是数字3一样,字符串“hello”总是包含字符h、e、1、1、o的编码单元序列,其中的任何一个字符都不能被修改。当然,字符串变量str可以修改为指向另一个字符串,就像存储3的值变量可以改为存储4。我们来看一个例子:Stringstr="asdf";Stringx=str.toUpperCase();toUpperCase用于将所有字符串转换为大写字符。进入toUpperCase的源码后,我们发现这个看似修改String值的方法,实际上最终是创建了一个全新的String对象,而原String对象没有动过。EmptystringandNullEmptystring""很容易理解,它是一个长度为0的字符串。可以调用下面的代码来检查一个字符串是否为空:if(str.length()==0){//todo}orif(str.equals("")){//todo}"空字符串是一个Java对象",有自己的字符串长度(0)和内容(空),即值数组为空。字符串变量还可以存储一个名为null的特殊值,这意味着“当前没有对象与该变量相关联”。检查一个字符串是否为null,可以这样判断:if(str==null){//todo}有时候需要检查一个字符串既不为null也不为空,这时就需要使用如下条件:if(str!=null&&str.length()!=0){//todo}有的同学会觉得,这么简单的条件判断还看你呢?是的,虽然简单,但还是有一个小坑,那就是我们“必须先检查str是否为null,因为如果在null值上调用该方法,编译器会报错”。字符串拼接既然说String是不可变的,那么我们看代码,这里的字符串a为什么会发生变化呢?Stringa="hello";Stringb="world";a=a+b;//a="helloworld"其实在使用+进行字符串拼接的时候,JVM会初始化一个StringBuilder进行拼接。等效的编译代码如下:Stringa="hello";Stringb="world";StringBuilderbuilder=newStringBuilder();builder.append(a);builder.append(b);a=builder.toString();AboutStringBuilder下面会详细讲解,你只需要知道StringBuilder是可变字符串类型就OK了。我们看一下builder.toString()的源码:很明显,toString方法也生成了一个新的String对象,而不是改变旧字符串的内容,相当于把旧字符串的引用指向了新的String对象.这就是字符串a发生变化的原因。此外,我们还需要了解一个特性,当字符串与非字符串值连接时,后者会自动转换为字符串(“任何Java对象都可以转换为字符串”)。例如:intage=13;Stringrating="PG"+age;//rating="PG13"这个特性通常用在输出语句中。例如:inta=12;System.out.println("a="+a);结合以上两个特点,我们来看一个小问题,“空字符串和null拼接的结果是什么”?Stringstr=null;str=str+"";System.out.println(str);答案是null,大家应该都能猜到,但是为什么是null呢?上面说到,使用+进行拼接实际上会转换为StringBuilder使用append方法拼接,编译后的代码如下:Stringstr=null;str=str+"";StringBuilderbuilder=newStringBuilder();builder.append(str);builder.append("");str=builder.toString();查看append的源码:可以看出,当传入的字符串为null时,会调用appendNull方法,该方法返回null。检查字符串是否相等您可以使用equals方法来检查两个字符串是否相等。例如:Stringstr="hello";System.out.println("hello".equals(str));//trueequals其实是Object类中的一个方法,所有的类都继承自Object类。在讲解equals方法之前,我们先回顾一下运算符==的用法,它有两种使用情况:对于基本数据类型,==比较值是否相同;对于引用数据类型,==比较的是内存地址是否相同。例如:Stringstr1=newString("hello");Stringstr2=newString("hello");System.out.println(str1==str2);//false如果你还不明白Java中的数据存储区,你可以先回去看看第一章?。对于上面的代码,str1和str2使用构造函数newString()创建了两个不同的字符串,取Stringstr1=newString("hello");举个例子,新对象存放在堆内存中,使用A引用str1指向这个对象的地址,这个对象的引用str1存放在栈内存中。str1和str2是两个不同的对象,地址不同,所以==比较的结果是false。其实Object类中原来的equals方法内部调用了运算符==,“判断两个对象是否有相同的引用(地址),==的效果是一样的”:也就是说,如果你的新类确实不覆盖equals方法,则此方法比较对象的地址。String方法重写equals方法。我们看源码:可以看出String重写的equals方法比较的是对象的内容,而不是地址。总结一下equals()的两个用例:Case1:类没有覆盖equals()方法。那么通过equals()比较这个类的两个对象时,就相当于通过==(比较地址)来比较这两个对象。情况2:类重写equals()方法。一般我们重写equals()方法来判断两个对象的内容是否相等,比如String类就是这样。当然,您不必这样做。例如:Stringa=newString("ab");//a是一个字符串引用Stringb=newString("ab");//b是另一个字符串引用,这两个对象的内容是一样的if(a.equals(b))//trueSystem.out.println("aEQb");if(a==b)//false,不是同一个对象,不同地址System.out.println("a==b");String常量池String由于String在Java中是一个类,与其他对象分配一样,需要很高的时间和空间成本。作为最基本、最常用的数据类型,频繁地创建大量的字符串会极大地影响程序的性能。为此,为了提高性能,减少内存开销,JVM在实例化字符串常量的时候做了一些优化:为字符串开辟了一个“字符串常量池StringPool”,可以理解为在创建字符串常量时在cache,首先检查字符串是否存在于字符串常量池中“如果字符串存在于字符串常量池中,则直接返回引用实例,无需重新实例化”;如果不是,则实例化字符串并放入池中。例如:Stringstr1="hello";Stringstr2="hello";System.out.printl("str1==str2":str1==str2)//true对于上面的代码,Stringstr1="hello";,"编译器会先在栈中创建变量名str1的引用,然后检查字符串常量池中是否有值为"hello"的引用,如果没有找到,则在字符串常量池中开辟一个地址存储字符串“hello”,然后将引用str1设置为“hello”。需要注意的是,字符串常量池的位置在JDK1.7中发生了变化:“在JDK1.7之前”,字符串常量池存在于“常量存储”(Constantstorage)“在JDK1.7之后”,字符串常量池存在于“常量存储”(Constantstorage)中“堆内存”(Heap)。此外,我们还可以“在运行过程中使用String的intern()方法手动添加字符串到StringPool中”。具体过程是这样的:当一个字符串调用intern()方法时,如果StringPool中已经存在与该字符串值相等的字符串,则返回该字符串在StringPool中的引用;否则,它将向字符串池中添加一个新字符串并返回对新字符串的引用。看下面的例子:Stringstr1=newString("hello");Stringstr3=str1.intern();Stringstr4=str1.intern();System.out.println(str3==str4);//trueForstr3,str1.intern()会先在StringPool中检查是否已经有等于str1值的字符串,如果没有,则在StringPool中添加一个新的值等于str1的字符串,并返回新的字符String引用.对于str4,str1.intern()在StringPool中找到一个等于str1的值的字符串,所以直接返回这个字符串的引用。所以s3和s4指的是同一个字符串,也就是地址相同,所以str3==str4的结果为真。"总结:"Stringstr="i",java虚拟机会自动分配给常量池;Stringstr=newString("i")会分配到堆内存。可以通过intern方法手动加入常量池。newString("hello")创建几个字符串对象。以下代码行创建了多少个字符串对象?堆中只创建一个?");显然不是。对于str1,newString("hello")分为两步:首先,"hello"是一个字符串字面量,所以它会检查String中是否有值为"hello"的引用编译时池,如果没有找到,就在字符串常量池中开辟地址空间,创建一个字符串对象,指向“hello”字符串字面量;然后,使用new在堆中创建一个字符串对象。因此,使用该方法一共会创建两个字符串对象(前提是StringPool中没有“hello”字符串对象)。2.双胞胎:MutableStringBuffer和StringBuilderString字符串连接问题有时需要从较短的字符串构建字符串,例如,文件中的键或单词。使用字符串拼接来达到这个目的的效率是比较低的。由于String类的对象内容无法更改,因此每当执行字符串连接时,总会在内存中创建一个新对象。不仅费时,而且浪费空间。例如:Strings="Hello";s+="World";这段简单的代码实际上一共产生了三个字符串,分别是“Hello”、“World”和“HelloWorld”。“Hello”和“World”会作为字符串常量在StringPool中创建,连接操作+会创建一个新的对象来存储“HelloWorld”。使用StringBuilder/StringBuffer类可以避免这个问题。毕竟String的+操作底层是StringBuilder实现的。StringBuilder和StringBuffer有相同的父类:但是StringBuilder不是线程安全的,在多线程环境下使用会出现数据不一致的情况,而StringBuffer是线程安全的。这是因为在StringBuffer类中,常用的方法都是使用synchronized关键字进行同步,所以是线程安全的。而StringBuilder没有。这也是StringBuilder比StringBuffer快的原因。因此,如果您使用单线程,请先使用StringBuilder。初始化操作StringBuilder和StringBuffer两个类的API相同,这里以StringBuilder为例来演示其初始化操作。StringBuiler/StringBuffer不能像String那样直接赋值一个字符串,所以不能那样初始化。它“需要通过构造函数进行初始化”。首先,构建一个空字符串构建器:StringBuilderbuilder=newStringBuilder();每次需要添加一部分,调用append方法:charch='a';builder.append(ch);Stringstr="ert"builder.append(str);当需要构建String时调用toString方法,可以得到一个String对象:Stringmystr=builder.toString();//aert3.String、StringBuffer、StringBuilder比较可变和线程安全String是不可变的Immutable,所以是线程安全的StringBuffer变量是线程安全的,因为它的内部方法大多使用synchronized进行同步。它的效率很低。StringBuilder变量不是线程安全的,因为没有使用synchronized进行同步,这也是它效率比StringBuffer高的原因。单线程下,优先使用StringBuilder。关于synchronized保证线程安全的问题,我们会在后续的文章中讲到。参考《Java 核心技术 - 卷 1 基础知识 - 第 10 版》《Thinking In Java(Java 编程思想)- 第 4 版》