ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] ## String类 ~~~ public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** The offset is the first index of the storage that is used. */ private final int offset; /** The count is the number of characters in the String. */ private final int count; /** Cache the hash code for the string */ private int hash; // Default to 0 /** use serialVersionUID from JDK 1.0.2 for interoperability */ private static final long serialVersionUID = -6849794470754667710L; ...... ~~~ 从上面可以看出几点: 1)**String类是final类,也即意味着String类不能被继承,并且它的成员方法都默认为final方法**。在Java中,被final修饰的类是不允许被继承的,并且该类中的成员方法都默认为final方法。 2)上面列举出了String类中所有的成员属性,从上面可以看出String类其实是**通过char数组来保存字符串的**。 下面再继续看String类的一些方法实现: ~~~ public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > count) { throw new StringIndexOutOfBoundsException(endIndex); } if (beginIndex > endIndex) { throw new StringIndexOutOfBoundsException(endIndex - beginIndex); } return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value); } public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } char buf[] = new char[count + otherLen]; getChars(0, count, buf, 0); str.getChars(0, otherLen, buf, count); return new String(0, count + otherLen, buf); } public String replace(char oldChar, char newChar) { if (oldChar != newChar) { int len = count; int i = -1; char[] val = value; /* avoid getfield opcode */ int off = offset; /* avoid getfield opcode */ while (++i < len) { if (val[off + i] == oldChar) { break; } } if (i < len) { char buf[] = new char[len]; for (int j = 0 ; j < i ; j++) { buf[j] = val[off+j]; } while (i < len) { char c = val[off + i]; buf[i] = (c == oldChar) ? newChar : c; i++; } return new String(0, len, buf); } } return this; ~~~ 从上面的三个方法可以看出,无论是sub操、concat还是replace操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,**最原始的字符串并没有被改变**。 在这里要永远记住一点:**“String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象”**。 ## 字符串常量池 我们知道字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串我们使用的非常多。JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:**使用字符串常量池**。**每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。****由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串**(这点对理解上面至关重要)。 Java中的常量池,实际上分为两种形态:**静态常量池**和**运行时常量池**。 所谓**静态常量池**,即\*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。 而**运行时常量池**,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。 JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。 JDK1.8开始,取消了Java方法区,取而代之的是位于直接内存的元空间(metaSpace)。 来看下面的程序: ~~~ String a = "chenssy"; String b = "chenssy"; ~~~ a、b和字面上的chenssy都是指向JVM字符串常量池中的"chenssy"对象,他们指向同一个对象。 ~~~ String c = new String("chenssy"); ~~~ new关键字一定会产生一个对象chenssy(注意这个chenssy和上面的chenssy不同),同时这个对象是存储在堆中。所以上面应该产生了两个对象:保存在栈中的c和保存堆中chenssy。但是在Java中根本就不存在两个完全一模一样的字符串对象。故堆中的chenssy应该是引用字符串常量池中chenssy。所以c、chenssy、池chenssy的关系应该是:c--->chenssy--->池chenssy。整个关系如下: ![](https://img.kancloud.cn/3f/11/3f11ddb8d3e5ab44da64bd47c5f75c37_560x335.png)  通过上面的图我们可以非常清晰的认识他们之间的关系。所以我们修改内存中的值,他变化的是所有。 **总结:**虽然a、b、c、chenssy是不同的对象,但是从String的内部结构我们是可以理解上面的。String c = new String("chenssy");虽然c的内容是创建在堆中,但是他的内部value还是指向JVM常量池的chenssy的value,它构造chenssy时所用的参数依然是chenssy字符串常量。 ## 总结 ### 1.String类初始化后是不可变的(immutable) **String使用private final char value\[\]来实现字符串的存储,也就是说String对象创建之后,就不能再修改此对象中存储的字符串内容,就是因为如此,才说String类型是不可变的(immutable)**。程序员不能对已有的不可变对象进行修改。我们自己也可以创建不可变对象,只要在接口中不提供修改数据的方法就可以。 然而,String类对象确实有编辑字符串的功能,比如replace()。**这些编辑功能是通过创建一个新的对象来实现的,而不是对原有对象进行修改**。比如: s = s.replace("World", "Universe"); 上面对s.replace()的调用将创建一个新的字符串"Hello Universe!",并返回该对象的引用。通过赋值,引用s将指向该新的字符串。如果没有其他引用指向原有字符串"Hello World!",原字符串对象将被垃圾回收。 ![](https://img.kancloud.cn/a8/5a/a85a652b3860beecb9ce858b05d31032_293x241.png) ### 2.引用变量与对象 A aa; 这个语句声明一个类A的引用变量aa\[我们常常称之为句柄\],而对象一般通过new创建。所以aa仅仅是一个引用变量,它不是对象。 ### 3.创建字符串的方式 创建字符串的方式归纳起来有两类: (1)使用""引号创建字符串; (2)使用new关键字创建字符串。 结合上面例子,总结如下: (1)单独使用""引号创建的字符串都是常量,编译期就已经确定存储到String Pool中; (2)使用new String("")创建的对象会存储到heap中,是运行期新创建的; **new创建字符串时首先查看池中是否有相同值的字符串,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果池中没有,则在堆中创建一份,然后返回堆中的地址(注意,此时不需要从堆中复制到池中,否则,将使得堆中的字符串永远是池中的子集,导致浪费池的空间)!** (3)使用只包含常量的字符串连接符如"aa" + "aa"创建的也是常量,编译期就能确定,已经确定存储到String Pool中; (4)使用包含变量的字符串连接符如"aa" + s1创建的对象是运行期才创建的,存储在heap中; ### 4.使用String不一定创建对象 在执行到双引号包含字符串的语句时,如String a = "123",JVM会先到常量池里查找,如果有的话返回常量池里的这个实例的引用,否则的话创建一个新实例并置入常量池里。所以,当我们在使用诸如String str = "abc";的格式定义对象时,总是想当然地认为,创建了String类的对象str。**担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的对象。**只有通过new()方法才能保证每次都创建一个新的对象。 ### 5.使用new String,一定创建对象 在执行String a = new String("123")的时候,首先走常量池的路线取到一个实例的引用,然后在堆上创建一个新的String实例,走以下构造函数给value属性赋值,然后把实例引用赋值给a: ~~~ public String(String original) { int size = original.count; char[] originalValue = original.value; char[] v; if (originalValue.length > size) { // The array representing the String is bigger than the new // String itself. Perhaps this constructor is being called // in order to trim the baggage, so make a copy of the array. int off = original.offset; v = Arrays.copyOfRange(originalValue, off, off+size); } else { // The array representing the String is the same // size as the String, so no point in making a copy. v = originalValue; } this.offset = 0; this.count = size; this.value = v; } ~~~ 从中我们可以看到,虽然是新创建了一个String的实例,但是value是等于常量池中的实例的value,即是说没有new一个新的字符数组来存放"123"。 ### 6.关于String.intern() intern() 方法返回字符串对象的规范化表示形式。 **intern方法使用:**一个初始为空的字符串池,它由类String独自维护。当调用 intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并返回此String对象的引用。 **它遵循以下规则:对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。** String.intern();  再补充介绍一点:存在于.class文件中的常量池,在运行期间被jvm装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法;当一个String实例str调用intern()方法时,java查找常量池中是否有相同unicode的字符串常量,如果有,则返回其引用,如果没有,则在常量池中增加一个unicode等于str的字符串并返回它的引用。 ~~~ /** * 关于String.intern() */ public void test11(){ String s0 = "kvill"; String s1 = new String("kvill"); String s2 = new String("kvill"); System.out.println("===========test11============"); System.out.println( s0 == s1 ); //false System.out.println( "**********" ); s1.intern(); //虽然执行了s1.intern(),但它的返回值没有赋给s1 s2 = s2.intern(); //把常量池中"kvill"的引用赋给s2 System.out.println( s0 == s1); //flase System.out.println( s0 == s1.intern() ); //true//说明s1.intern()返回的是常量池中"kvill"的引用 System.out.println( s0 == s2 ); //true } ~~~ 运行结果:false、false、true、true。 ### 7.关于equals和== (1)对于==,如果作用于基本数据类型的变量(byte,short,char,int,long,float,double,boolean ),则直接比较其存储的"值"是否相等;如果作用于引用类型的变量(String),则比较的是所指向的对象的地址(即是否指向同一个对象)。 (2)equals方法是基类Object中的方法,因此对于所有的继承于Object的类都会有该方法。在Object类中,equals方法是用来比较两个对象的引用是否相等,即是否指向同一个对象。 (3)对于equals方法,注意:equals方法不能作用于基本数据类型的变量。如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;而String类对equals方法进行了重写,用来比较指向的字符串对象所存储的字符串是否相等。其他的一些类诸如Double,Date,Integer等,都对equals方法进行了重写用来比较指向的对象所存储的内容是否相等。 ~~~ /** * 关于equals和== */ public void test12(){ String s1="hello"; String s2="hello"; String s3=new String("hello"); System.out.println("===========test12============"); System.out.println( s1 == s2); //true,表示s1和s2指向同一对象,它们都指向常量池中的"hello"对象 //flase,表示s1和s3的地址不同,即它们分别指向的是不同的对象,s1指向常量池中的地址,s3指向堆中的地址 System.out.println( s1 == s3); System.out.println( s1.equals(s3)); //true,表示s1和s3所指向对象的内容相同 } ~~~ ### 8.String相关的+ String中的 + 常用于字符串的连接。看下面一个简单的例子: ~~~ /** * String相关的+ */ public void test13(){ String a = "aa"; String b = "bb"; String c = "xx" + "yy " + a + "zz" + "mm" + b; System.out.println("===========test13============"); System.out.println(c); } ~~~ 编译运行后,主要字节码部分如下: ~~~ public static main([Ljava/lang/String;)V L0 LINENUMBER 5 L0 LDC "aa" ASTORE 1 L1 LINENUMBER 6 L1 LDC "bb" ASTORE 2 L2 LINENUMBER 7 L2 NEW java/lang/StringBuilder DUP LDC "xxyy " INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V ALOAD 1 INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; LDC "zz" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; LDC "mm" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; ALOAD 2 INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; ASTORE 3 L3 LINENUMBER 8 L3 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; ALOAD 3 INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V L4 LINENUMBER 9 L4 RETURN L5 LOCALVARIABLE args [Ljava/lang/String; L0 L5 0 LOCALVARIABLE a Ljava/lang/String; L1 L5 1 LOCALVARIABLE b Ljava/lang/String; L2 L5 2 LOCALVARIABLE c Ljava/lang/String; L3 L5 3 MAXSTACK = 3 MAXLOCALS = 4 } ~~~ 显然,通过字节码我们可以得出如下几点结论: (1).String中使用 + 字符串连接符进行字符串连接时,连接操作最开始时如果都是字符串常量,编译后将尽可能多的直接将字符串常量连接起来,形成新的字符串常量参与后续连接(通过反编译工具jd-gui也可以方便的直接看出); (2).接下来的字符串连接是从左向右依次进行,对于不同的字符串,首先以最左边的字符串为参数创建StringBuilder对象,然后依次对右边进行append操作,最后将StringBuilder对象通过toString()方法转换成String对象(注意:中间的多个字符串常量不会自动拼接)。 也就是说**String c = "xx" + "yy " + a + "zz" + "mm" + b; 实质上的实现过程是: String c = new StringBuilder("xxyy ").append(a).append("zz").append("mm").append(b).toString();** **由此得出结论:当使用+进行多个字符串连接时,实际上是产生了一个StringBuilder对象和一个String对象。** ### 9.String的不可变性导致字符串变量使用+号的代价 ~~~ String s = "a" + "b" + "c"; String s1 = "a"; String s2 = "b"; String s3 = "c"; String s4 = s1 + s2 + s3; ~~~ 分析:变量s的创建等价于 String s = "abc"; 由上面例子可知编译器进行了优化,这里只创建了一个对象。由上面的例子也可以知道s4不能在编译期进行优化,其对象创建相当于: StringBuilder temp =newStringBuilder(); temp.append(a).append(b).append(c); String s = temp.toString(); 由上面的分析结果,可就不难推断出String 采用连接运算符(+)效率低下原因分析,形如这样的代码: ~~~ public class Test { public static void main(String args[]) { String s = null; for(int i = 0; i < 100; i++) { s += "a"; } } } ~~~ 每做一次 + 就产生个StringBuilder对象,然后append后就扔掉。下次循环再到达时重新产生个StringBuilder对象,然后 append 字符串,如此循环直至结束。 如果我们直接采用 StringBuilder 对象进行 append 的话,我们可以节省 N - 1 次创建和销毁对象的时间。所以对于在循环中要进行字符串连接的应用,一般都是用StringBuffer或StringBulider对象来进行append操作。 **10.String、StringBuffer、StringBuilder的区别** (1)可变与不可变:String是**不可变字符串对象**,StringBuilder和StringBuffer是**可变字符串对象**(其内部的字符数组长度可变)。 (2)是否多线程安全:String中的对象是不可变的,也就可以理解为常量,显然**线程安全**。StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,只是StringBuffer 中的方法大都采用了synchronized 关键字进行修饰,因此是**线程安全**的,而 StringBuilder 没有这个修饰,可以被认为是**非线程安全**的。 (3)String、StringBuilder、StringBuffer三者的执行效率: StringBuilder > StringBuffer > String 当然这个是相对的,不一定在所有情况下都是这样。比如String str = "hello"+ "world"的效率就比 StringBuilder st  = new StringBuilder().append("hello").append("world")要高。因此,这三个类是各有利弊,应当根据不同的情况来进行选择使用: 当字符串相加操作或者改动较少的情况下,建议使用 String str="hello"这种形式; 当字符串相加操作较多的情况下,建议使用StringBuilder,如果采用了多线程,则使用StringBuffer。 ### 11.String中的final用法和理解 ~~~ final StringBuffer a = new StringBuffer("111"); final StringBuffer b = new StringBuffer("222"); a=b;//此句编译不通过 final StringBuffer a = new StringBuffer("111"); a.append("222");//编译通过 ~~~ 可见,**final只对引用的"值"(即内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。**至于它所指向的对象的变化,final是不负责的。 ### 12.关于String str = new String("abc")创建了多少个对象? 这个问题在很多书籍上都有说到比如《Java程序员面试宝典》,包括很多国内大公司笔试面试题都会遇到,大部分网上流传的以及一些面试书籍上都说是2个对象,这种说法是片面的。 首先必须弄清楚创建对象的含义,创建是什么时候创建的?这段代码在运行期间会创建2个对象么?毫无疑问不可能,用javap -c反编译即可得到JVM执行的字节码内容: ![](https://img.kancloud.cn/34/f8/34f811d279d3db522f17f7f8db2d64fc_646x341.png) 很显然,new只调用了一次,也就是说只创建了一个对象。而这道题目让人混淆的地方就是这里,这段代码在运行期间确实只创建了一个对象,即在堆上创建了"abc"对象。而为什么大家都在说是2个对象呢,这里面要澄清一个概念,该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。 因此,这个问题如果换成 String str = new String("abc")涉及到几个String对象?合理的解释是2个。 个人觉得在面试的时候如果遇到这个问题,可以向面试官询问清楚”是这段代码执行过程中创建了多少个对象还是涉及到多少个对象“再根据具体的来进行回答。 ### 13.字符串池的优缺点 字符串池的优点就是避免了相同内容的字符串的创建,节省了内存,省去了创建相同字符串的时间,同时提升了性能;另一方面,字符串池的缺点就是牺牲了JVM在常量池中遍历对象所需要的时间,不过其时间成本相比而言比较低。 ## 推荐阅读 [深入理解Java中的String](https://www.cnblogs.com/xiaoxi/p/6036701.html)