Java
的字符串就是Unicode
字符序列,Java
并没有内置字符串类型,而是在Java
库中提供了预定义类String
,每个用双引号扩起来的字符串都是String
类的一个实例。
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;
........
}
-
String
是一个final
类也就是说String
是不可被继承的,并且它的成员方法默认为final
方法,Java
中被final
修饰的类默认是不可继承的,final
类的成员方法都默认为final
方法。 -
从
String
类的成员属性可以看出String
是通过Char
数组存储字符串。
子串
String
类中substring
方法可以从一个字符串中提取子串
String str = "HaHa";
String str1 = str.substring(0,2);
// str1=“Ha”
- 在
substring
中从0开始计数,直到2为止,但是不包含2. substring
的工作方式有一个优点:容易计算子串的长度。字符串s.substring(a,b)
的长度为b-a
。
- String的substring源码
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);
}
拼接
Java提供了和其他语言一样同样支持+号进行拼接但是效率低稍后会说到。
String a = "Hello";
String b = "World";
String c = a + b;
将一个字符串与一个非字符串进行拼接时,后者被转换成字符串。
int a= 10;
String b = "Hello";
System.out.println(b + a);
int aa = 20;
String bb = "World";
int cc = 30;
System.out.println(aa + cc + bb);
// Hello10
// 50World
不可变字符串
String
类没有提供用于修改字符串的方法,String
使用private final char value[]来实现字符串的储存,也就是说String
对象创建后,就不能在修改此对象中储存的字符串内容,就是因为如此,才说String
类型是不可变的(immutable
)。我们不能对以创建的不可变对象进行修改。我们自己也可以创建不可变对象,只要在接口中不提供修改数据的方法就可以。
String类对象确实有编辑字符串的功能,比如replace()。这些编辑功能是通过创建一个新的对象来实现的,而不是在原有的对象进行修改。
String s = "Hello World";
s = s.replace("World","ShellMing");
上面s.replace()
的调用将创建一个新的字符串“Hello ShellMing”
,并返回该对象的引用。通过赋值,引用s将指向新的字符串。如果没有其他引用指向原有字符串“Hello World”
,原字符串对象将被垃圾回收。
引用变量与对象
String a;
以上语句中String的引用变量是a而对象一般通过new的方式来创建。所以a就是一个引用变量,不是对象。
- 创建字符串的方式
-
使用“”引号来创建字符串对象;
-
使用new关键字来创建字符串对象;
那么以上这两种有什么区别呢!
- 单独使用双引号创建的字符串都是常量,编译期就已经存储到字符串常量池当中
- 使用new关键字来创建的字符串对象会储存在堆内存中,是运行期创建的。
- 但值得注意的是new关键字创建字符串对象时首先查看字符串常量池中是否有相同值的字符串,
- 如果有,则拷贝一份到堆内存中,然后将堆内存的地址返回。
- 如果常量池中没有,则在堆内存中创建一份,然后返回堆内存地址。
- 但是在堆内存中创建的字符串对象不会在复制到字符串常量池中,以避免不必要的常量池空间的浪费。
- 只包含常量的字符串连接符如
“a” + “a”
创建的也是常量,编译期就能确定,已经确定储存到字符串常量池中。 - 使用包含变量的字符串链接符如
“a” + str
创建的对象是运行期才创建的,储存在堆内存中。 - 这就导致了使用String不一定创建对象但是new String 一定创建对象。
关于intern()
方法
- 简介
一个初始化为空的字符串池,它由String
独自维护。当调用intern
方法时,如果池已经包含String
对象的字符串(使用equals
方法确定)则返回池中的字符串,否则将此String
对象添加到池中,并返回此String
对象的引用。
- 执行规则
它遵循以下规则:对于任意两个字符串 s
和 t
,当且仅当 s.equals(t)
为 true
时,s.intern() == t.intern()
才为 true
。
- 扩展
存在于.class
文件中的常量池,在运行期间被jvm
装载,并且可以扩充。String
的intern()
方法就是扩充常量池的一个方法;当一个String
实例str
调用intern()
方法时,Java
查找常量池中是否有相同Unicode
的字符串常量,如果有,则返回其引用,如果没有,则在常量池中增加一个unicode
等于str
的字符串并返回它的引用。
String s0 = "Hello";
String s1 = new String("Hello");
String s2 = new String("Hello");
System.out.println( s0 == s1 ); //false
s1.intern(); //虽然执行了s1.intern(),但它的返回值没有赋给s1
s2 = s2.intern(); //把常量池中"kvill"的引用赋给s2
System.out.println( s0 == s1); //false
System.out.println( s0 == s1.intern() ); //true 说明s1.intern()返回的是常量池中"Hello"的引用
System.out.println( s0 == s2 ); //true
检测字符串是否相等
对于==如果作用于基本类型的变量(byte,short,char,int,long,float,doble,boolen)则直接比较储存的值是否相等注意这里提到的值指的是数值,如果作用于引用类型的变量则比较的是所指向的对象的地址就是判断是否指向同一个对象。
- String中的equals方法
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
equals
方法是基类Object
中的方法,因此对于所由继承于Object
的类都会有该方法。在Object
类中,equals
方法是用来比较两个对象的引用是否相等。即是否指向同一个对象。
对于equals
方法,equals
方法不能用于几本数据类型的变量。如果没有对equals
方法进行重写。则比较的引用类型的变量所指向的地址,而String
类对equals
方法进行了重写,用来比较指向的字符串所储存的字符串是否相等。其他的一写诸如Double
,Date
,Integer
等都对equals
方法进行了重写用来比较指向所储存的内容是否相等。
String s1="hello";
String s2="hello";
String s3=new String("hello");
System.out.println( s1 == s2); //true,表示s1和s2指向同一对象,它们都指向常量池中的"hello"对象
//false,表示s1和s3的地址不同,即它们分别指向的是不同的对象,s1指向常量池中的地址,s3指向堆中的地址
System.out.println( s1 == s3);
System.out.println( s1.equals(s3)); //true,表示s1和s3所指向对象的内容相同
String连接符"+"的详解
String a = "aa";
String b = "bb";
String c = "xx" + "yy " + a + "zz" + "mm" + b;
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
}
-
String
中使用+号进行字符串连接时对不同的字符串,连接操作最开始时如果都是字符串常量,编译后将尽可能多的直接将字符串常量连接起来,形成新的字符串常量参与后续连接。 -
接下来的字符串连接是从左向右依次进行,首先以最左边的字符串参数创建
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
对象。
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 = new StringBuilder();
temp.append(a).append(b).append(c);
String s = temp.toString();
由上面分析的结果,就不难推断出String
采用连接符效率低下的原因的代码分析
String s = null;
for(int i = 0; i < 100; i++) {
s += "a";
}
每做一次 +
就产生了一个StringBuilder
对象,然后append
后就扔掉。下次再循环的时候重新产生个StringBuilder
对象,然后append
字符串,如此循环直至结束。如果我们直接采用StringBuilder
对象进行append
的话我们可以节省N-1
次创建和销毁对象的时间。所以对于循环中要进行字符串连接的应用一般都是使用StringBuilder
或者StringBuffer
对象来进行append
操作。
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是不负责的。
关于String、StringBuffer和StringBuilder
- 可变与不可变:
String
是不可变字符串对象,StringBuilder
和StringBuffer
是可变字符串对象(其内部的字符数组长度可变)。
- 是否多线程安全:
String
中的对象是不可变的,也就可以理解为常量,显然线程安全。StringBuffer
与 StringBuilder
中的方法和功能完全是等价的,只是StringBuffer
中的方法大都采用了synchronized
关键字进行修饰,因此是线程安全的,而 StringBuilder
没有这个修饰,可以被认为是非线程安全的。
- 三者的执行效率:
StringBuilder
> StringBuffer
> String
当然这个是相对的,不一定在所有情况下都是这样。比如String str = "hello"+ "world"
的效率就比 StringBuilder st = new StringBuilder().append("hello").append("world")
要高。因此,这三个类是各有利弊,
- 不同的情况的使用:
当字符串相加操作或者改动较少的情况下,建议使用 String str="hello"
这种形式;当字符串相加操作较多的情况下,建议使用StringBuilder
,如果采用了多线程,则使用StringBuffer
。
字符串池的优缺点
字符串池的优点就是避免了相同内容的字符串的创建,节省了内存,省去了创建相同字符串的时间,同时提升了性能;另一方面,字符串池的缺点就是牺牲了JVM
在常量池中遍历对象所需要的时间,不过其时间成本相比而言比较低。
场景总结
public class StringTest {
public static void main(String[] args) {
/**
* 情景一:字符串池
* JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象;
* 并且可以被共享使用,因此它提高了效率。
* 由于String类是final的,它的值一经创建就不可改变。
* 字符串池由String类维护,我们可以调用intern()方法来访问字符串池。
*/
String s1 = "abc";
//↑ 在字符串池创建了一个对象
String s2 = "abc";
//↑ 字符串pool已经存在对象“abc”(共享),所以创建0个对象,累计创建一个对象
System.out.println("s1 == s2 : "+(s1==s2));
//↑ true 指向同一个对象,
System.out.println("s1.equals(s2) : " + (s1.equals(s2)));
//↑ true 值相等
/**
* 情景二:关于new String("")
*
*/
String s3 = new String("abc");
//↑ 创建了两个对象,一个存放在字符串池中,一个存在与堆区中;
//↑ 还有一个对象引用s3存放在栈中
String s4 = new String("abc");
//↑ 字符串池中已经存在“abc”对象,所以只在堆中创建了一个对象
System.out.println("s3 == s4 : "+(s3==s4));
//↑false s3和s4栈区的地址不同,指向堆区的不同地址;
System.out.println("s3.equals(s4) : "+(s3.equals(s4)));
//↑true s3和s4的值相同
System.out.println("s1 == s3 : "+(s1==s3));
//↑false 存放的地区多不同,一个栈区,一个堆区
System.out.println("s1.equals(s3) : "+(s1.equals(s3)));
//↑true 值相同
/**
* 情景三:
* 由于常量的值在编译的时候就被确定(优化)了。
* 在这里,"ab"和"cd"都是常量,因此变量str3的值在编译时就可以确定。
* 这行代码编译后的效果等同于: String str3 = "abcd";
*/
String str1 = "ab" + "cd"; //1个对象
String str11 = "abcd";
System.out.println("str1 = str11 : "+ (str1 == str11));
/**
* 情景四:
* 局部变量str2,str3存储的是存储两个拘留字符串对象(intern字符串对象)的地址。
*
* 第三行代码原理(str2+str3):
* 运行期JVM首先会在堆中创建一个StringBuilder类,
* 同时用str2指向的拘留字符串对象完成初始化,
* 然后调用append方法完成对str3所指向的拘留字符串的合并,
* 接着调用StringBuilder的toString()方法在堆中创建一个String对象,
* 最后将刚生成的String对象的堆地址存放在局部变量str3中。
*
* 而str5存储的是字符串池中"abcd"所对应的拘留字符串对象的地址。
* str4与str5地址当然不一样了。
*
* 内存中实际上有五个字符串对象:
* 三个拘留字符串对象、一个String对象和一个StringBuilder对象。
*/
String str2 = "ab"; //1个对象
String str3 = "cd"; //1个对象
String str4 = str2+str3;
String str5 = "abcd";
System.out.println("str4 = str5 : " + (str4==str5)); // false
/**
* 情景五:
* JAVA编译器对string + 基本类型/常量 是当成常量表达式直接求值来优化的。
* 运行期的两个string相加,会产生新的对象的,存储在堆(heap)中
*/
String str6 = "b";
String str7 = "a" + str6;
String str67 = "ab";
System.out.println("str7 = str67 : "+ (str7 == str67));
//↑str6为变量,在运行期才会被解析。
final String str8 = "b";
String str9 = "a" + str8;
String str89 = "ab";
System.out.println("str9 = str89 : "+ (str9 == str89));
//↑str8为常量变量,编译期会被优化
}
}