一. 字符串基本知识要点java
字符串类型String是Java中最经常使用的引用类型。咱们在使用Java字符串的时候,一般会采用两种初始化的方式:1. String str = "Hello World"; 2. String str = new String("Hello World"); 这两种方式均可以将变量初始化为java字符串类型,经过第一种方式建立的字符串又被称为字符串常量。须要注意的是,Java中的String类是一个final类,str指向的字符串对象存储于堆中,而str自己则是存储在栈中的一个引用罢了。字符串对象一旦被初始化,则不容许再次被修改。从以下String的定义中咱们能够验证以上所述:数据库
1 public final class String implements java.io.Serializable, Comparable<String>, CharSequence{ 2 3 /** The value is used for character storage. */ 4 private final char value[]; 5 6 /** Cache the hash code for the string */ 7 private int hash; // Default to 0 8 9 }
从代码中咱们发现,String前有final修饰,表示是final类;而其中存储的字符数组value[],也是由final修饰,代表一旦被赋值,则不容许再次修改。数组
那么,使用如上两种字符串初始化的方式有什么不一样呢?咱们能够经过以下代码体会:安全
public class EqualTest { public static void main(String[] args) { String s1 = "Hello"; String s2 = new String("Hello"); System.out.println(s1 == s2); System.out.println(s1.equals(s2)); } }
程序输出结果为false和true。从== 和equals的区别上,咱们通常这样来总结:==比较的是两个对象的引用,对象必须如出一辙;equals则比较的是对象的内容,字符串内容一致便返回true。这说明,两种初始化的方式所构造的字符串对象,内容是一致的(能够理解为values数组一致),可是倒是两个不一样的对象,分别存储在内存的不一样位置。其实,这两种初始化方式的最大不一样在于,s1被初始化在字符串常量池中,而s2则存储在堆中。那么,什么是字符串常量池呢?并发
字符串的分配,和其余的对象分配同样,耗费高昂的时间与空间代价。JVM为了提升性能和减小内存开销,在实例化字符串常量的时候进行了一些优化。为了减小在JVM中建立的字符串的数量,字符串类维护了一个字符串池,每当代码建立字符串常量时,JVM会首先检查字符串常量池。若是字符串已经存在池中,就返回池中的实例引用。若是字符串不在池中,就会实例化一个字符串常量并放到池中。Java可以进行这样的优化是由于字符串是不可变的final类型,共享的时候不用担忧数据冲突(读写不冲突,由于不能写,至关于数据库中的S锁,即共享锁)。在常量池中,任何字符串至多维护一个对象。字符串常量老是指向常量池中的一个对象。经过new操做符建立的字符串对象不指向池中的任何对象,可是能够经过使用字符串的intern()方法来指向其中的某一个。java.lang.String.intern()返回一个池字符串,就是一个在全局常量池中有了一个入口。若是该字符串之前没有在全局常量池中,那么它就会被添加到里面。app
Java String类中有不少基本的方法。主要分红如下两个部分:性能
1)和value[]相关的方法:学习
2)和其余字符串相关的方法:优化
另一个值得注意的细节是,String是不可变字符串对象,StringBuilder和StringBuffer是可变字符串对象(其内部的字符数组长度可变),StringBuffer线程安全,StringBuilder非线程安全。关于String的append操做,会在下面结合具体的例子进行解释。ui
二. 几个关于String的程序分析
2.1 intern的程序示例
参看以下程序:
public class StringTest1 { public static void main(String[] args) { // TODO Auto-generated method stub String s1 = "hello world"; String s2 = new String("hello world"); String s3 = s2.intern(); System.out.println(s1 == s2); System.out.println(s1 == s3); } }
程序的输出是false,true。关于==和equals的区别在上面已作了详细的解释,因为s1是分配在字符串常量池中,s2则存储在堆中,所以两个对象并非同一个对象,==操做返回false。而intern在JDK 1.7及如下,都是返回一个池字符串,该池字符串和原来的String对象的内容一致。若池中无该常量则添加,如有,则直接返回该常量的引用。所以,s1和s3是一个对象。说白了,在JVM的字符串常量池中,对于每个字符串,只有一个共享的对象。
2.2 经过字节码进行深刻分析
当状况变得复杂的时候,参看以下程序:
public class StringTest2 { public static void main(String[] args){ String baseStr = "base"; final String baseFinalStr = "base"; //extend String s1 = "baseext"; String s2 = "base" + "ext"; String s3 = baseStr + "ext"; String s4 = baseFinalStr + "ext"; String s5 = new String("baseext").intern(); System.out.println(s1 == s2); System.out.println(s1 == s3); System.out.println(s1 == s4); System.out.println(s1 == s5); } }
这段程序乍一看很是复杂,里面有final String(final是限制在String对象的引用上,即该引用不能再更改所指向的String对象,String对象自己即是final类型的),还有字符串常量,以及字符串对象,和各个对象之间的“+”操做(“+”操做在下面的程序中详细解释)。那么咱们不由会问,在“+”操做的过程当中,JVM究竟是如何进行对象转换和操做呢?要想搞清楚这个问题,咱们须要深刻Bytecode一探究竟。使用javap -v XXX.class命令,能够打印出字节码文件中的符号表和指令等信息,该段程序的字节码输出以下:
这里的constant pool指的是JVM内存结构中的运行时常量池,是方法区的一部分(参见周志明 《深刻理解Java虚拟机》),咱们上文提到的字符串常量池只是constant pool的一部分,除此以外,它还主要用来存储编译期生成的各类字面量和符号引用。javap -v的输出主要分为constant pool和方法体指令两部分,而指令中的操做数则是常量池中的序号。为了方便接下来的描述,咱们先对经常使用的JVM字节码指令作一下说明:
LDC 将int, float或String型常量值从常量池中推送至栈顶;
ASTORE_<N> Store reference into local variable,将栈顶的引用赋值给第N个局部变量;
ALOAD 将指定的引用类型本地变量推送至栈顶
INVOKE VIRTUAL 调用实例方法
INVOKE SPECIAL 调用超类构造方法等初始化方法
INVOKE STATIC 调用静态方法
NEW 建立一个对象,并将其引用值压入栈顶
DUP 复制栈顶数值并将复制值压入栈顶
以上指令是须要仔细理解的。使用javap -v进行字节码的查看和理解可能比较困难,由于你要将 #序号 和 constant pool中的字面量不断照应已方便理解。Eclipse中提供了Bytecode Outline的插件能够很方便的查看和理解bytecode。插件的安装请自行百度,这里再也不赘述。这里贴出本段代码的outline:
// access flags 0x9 public static main([Ljava/lang/String;)V L0 // LINENUMBER 5 L0 LDC "base" //将"base"从常量池推送至栈顶 ASTORE 1 //赋值给baseStr变量 L1 LINENUMBER 6 L1 LDC "base" ASTORE 2 //赋值给baseFinalStr变量 L2 LINENUMBER 8 L2 LDC "baseext" ASTORE 3 L3 LINENUMBER 9 L3 LDC "baseext" //注意,这里直接将"baseext"赋值给了s2,而没有进行"+"操做!!! ASTORE 4 L4 LINENUMBER 10 L4 NEW java/lang/StringBuilder //建立StringBuilder对象 DUP ALOAD 1 //将baseStr推送至栈顶 INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String; //获取baseStr的value INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V //将建立的StringBuilder对象初始化为上一步得到的value LDC "ext" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; //调用StringBuilder对象的append实例方法 INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; //调用StringBuilder对象的toString实例方法 ASTORE 5 //将toString的结果赋值给s3 L5 LINENUMBER 11 L5 LDC "baseext" //s4也是直接赋值 ASTORE 6 L6 LINENUMBER 12 L6 NEW java/lang/String DUP LDC "baseext" INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V INVOKEVIRTUAL java/lang/String.intern ()Ljava/lang/String; //调用intern()方法 ASTORE 7
//===================================如下为 输出部分,能够忽略========================================= .......
....... L19 LINENUMBER 17 L19 RETURN L20 //相似于符号表,对应于local variable和变量编号 LOCALVARIABLE args [Ljava/lang/String; L0 L20 0 LOCALVARIABLE baseStr Ljava/lang/String; L1 L20 1 LOCALVARIABLE baseFinalStr Ljava/lang/String; L2 L20 2 LOCALVARIABLE s1 Ljava/lang/String; L3 L20 3 LOCALVARIABLE s2 Ljava/lang/String; L4 L20 4 LOCALVARIABLE s3 Ljava/lang/String; L5 L20 5 LOCALVARIABLE s4 Ljava/lang/String; L6 L20 6 LOCALVARIABLE s5 Ljava/lang/String; L7 L20 7 MAXSTACK = 3 MAXLOCALS = 8
若是想深刻理解,请逐行理解以上字节码程序。根据程序的分析,咱们不可贵出输出结果:true false true true。
s1和s2,s4,s5都是指向字符串常量池中的同一个字符串常量。s2和s4中的“+”并无起任何做用。String中使用 + 字符串链接符进行字符串链接时,链接操做最开始时若是都是字符串常量,编译后将尽量多的直接将字符串常量链接起来,造成新的字符串常量参与后续链接。而s3中的第一个操做数是String对象类型,所以会首先以最左边的字符串为参数建立StringBuilder对象,而后依次对右边进行append操做,最后将StringBuilder对象经过toString()方法转换成String对象。
这里要注意的一点是,对于final字段修饰的字符串常量,编译期直接进行了常量替换。若是final修饰的不是字符串常量,而是字符串对象,如final String a = new String("baseStr"); 则和没有final修饰的状况是同样的,一样须要用StringBuilder进行append并toString才能够。
咱们再经过一个程序来更深刻的理解字符串常量和“+”操做符。程序以下:
public class AppendTest { public static void main(String[] args) { // TODO Auto-generated method stub String a = "aa"; String b = "bb"; String c = "xx" + "yy " + a + "zz" + "mm" + b; System.out.println(c); } }
程序输出天然不用赘述,咱们经过一样的方法查看Bytecode的outline,输出以下:
// access flags 0x21 public class com/yelbosh/java/str/AppendTest { // compiled from: AppendTest.java // access flags 0x1 public <init>()V L0 LINENUMBER 3 L0 ALOAD 0 INVOKESPECIAL java/lang/Object.<init> ()V RETURN L1 LOCALVARIABLE this Lcom/yelbosh/java/str/AppendTest; L0 L1 0 MAXSTACK = 1 MAXLOCALS = 1 // access flags 0x9 public static main([Ljava/lang/String;)V L0 LINENUMBER 6 L0 LDC "aa" ASTORE 1 L1 LINENUMBER 7 L1 LDC "bb" ASTORE 2 L2 LINENUMBER 8 L2 NEW java/lang/StringBuilder DUP LDC "xxyy " //直接load的是字符串常量“xxyy ” INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V ALOAD 1 INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; //以后都是在调用StringBuilder对象的append实例方法 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 9 L3 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; ALOAD 3 INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V L4 LINENUMBER 10 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 }
经过这个程序,更印证了咱们如上的结论。
经过这个深刻分析,咱们在写代码的时候,也要注意使用StringBuilder对象。若是直接在for循环中使用“+”操做符进行字符串对象(常量无所谓)的拼接,那么实际上在每次循环的时候,都要建立StringBuilder,而后append,再toString出来,所以性能是十分低下的。这个时候,就须要在循环外声明StringBuilder对象,而后在循环内调用append方法进行拼接。另外要注意的是,StringBuilder是线程不安全的,若是涉及到多个线程同时对StringBuilder的append操做,请使用synchronized或lock确保并发访问的安全性,或者转而使用线程安全的StringBuffer。
总结:Java String是很是灵活的一个对象,可是只要把细节搞清楚,问题仍是很简单的。在实际编码的过程当中,必定要考虑字符串操做的性能和线程安全问题,这样才能更好的运用字符串完成本身的业务逻辑。但愿这篇博文能对您的学习有些帮助,若是错误,请不吝赐教。