字符串就是一连串字符序列,Java提供了String和StringBuffer两个类来封装字符串,并提供一系列方法来操作字符串对象。
String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符串是不可改变的,知道这个对象被销毁。
StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,StringBuffer提供的append()、insert()、reverse()、setChatAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。
JDK1.5又新增了一个StringBuilder类,它也代表字符串对象。实际上StringBuilder和StringBuffer基本相似,两个类的构造器也基本相同。不同的是,StringBuffershi1线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高。
提示:String、StringBuilder、StringBuffer都实现了CharSequence接口,因此CharSequence可认为是一个字符串的协议接口。
先看String类源码:
public final class String
implements java.io.Serializable, Comparable, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
...
1.可以看出,String是由char[] 来实现的。
2.String是final类,也就意味着String类不能被继承。
注意:String类不可变主要是因为char[]在是private的,并且String类没有提供setter方法,导致无法改变这个String对象。但是我们常说的String s = “123456”中的s是对象的引用,对象的引用指向对象。对象是不可变的,但是对象的引用是可变的。
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
上面的subString、concat、replace操作都是不是在原字符串进行的,而是创建了一个新的字符串。也就是说,进行了上述操作后,本身的字符串并没有改变。只是返回了一个新的对象的引用。
也就是说:对String操作的任何改变都不会改变原对象,而任何改变String对象的操作都会产生新对象。
比如:
String str1 = "java";
str1 = str1 + "struts";
str1 = str1 + "spring";
上面程序出了使用3个字符串直接量之外,还会产生额外两个字符串直接量“javastruts”和“javastrutsspring”。程序中的str1依次指向3个不同的字符串对象。
因为String是不可变的,所以会产生很多的临时变量,使用StringBuffer或者StringBuilder就可以避免这个问题。
StringBuilder提供了一系列插入、追加,改变该字符序列的方法。而StringBuffer与其用法完全相同,只是StringBuffer是线程安全的。
StringBuffer、StringBuilder有两个属性:length和capacity。
其中length属性表示其包含的字符序列的长度。与String对象length不同的是,StringBuffer、StringBuilder的length是可以改变的,可以通过length()、setLength(int len)方法来访问和修改其字符串序列的长度,capacity属性表示StringBuilder容量,capacity通常比length大,程序通常无须任何关心capacity
属性。
既然已经存在了String类,那为什么要存在StringBuffer、StringBuilder类呢?
看如下代码:
public class Main
{
public static void main(String[] args)
{
String str = "";
for(int i = 0;i < 10000;i++)
str += "Hello";
}
}
反编译字节码之后,可以看出,每次循环都会new一个StringBuilder对象,然后进行append操作,最后用toString方法返回String对象。试想一下如果这些对象没有被JVM回收,则会造成多大的资源浪费。实际上上述操作会被JVM优化成:
StringBuilder sb = new StringBuilder(string);
sb.append("Hello");
str.toString();
再看下面的代码:
public class Main
{
public static void main(String[] args)
{
StringBuilder sb = new StringBuilder();
for(int i = 0;i < 10000;i++)
sb.append("Hello");
}
}
反编译其字节码文件,可以看出new操作只进行了一次,也就是说只生成一个对象,append操作是在原有对象上进行的,因此在循环10000次之后,资源消耗要小得多。
而StringBuilder呢,比StringBuffer多了一个关键字: synchronize。这个关键字在多线程操作的时起到安全保护的作用。
总之,如果在对字符串修改较少的情况下,建议使用String str = “Hello”;这种形式;如果在对字符串修改较多,则用StringBuilder;涉及到多线程,则用StringBuffer。
一些常见的面试题:
1.
String a = "hello2";
String b = "hello" + 2;
System.out.println(a == b);
a == b输出true。很明显b在编译时就被优化成“hello2”,因此在运行期间,a和b指向的是统一对象
2.
String a = "hello2";
String b = "hello";
String c = b + 2;
System.out.println(a == c);
a==c输出false,因为b是变量,不会在编译期间被优化,不会把 b + 2当成字符常量来处理的。这种情况生成的对象实际上保存在堆上。
3.
String a = "hello2";
final String b = "hello";
String c = b + 2;
System.out.println(a == c);
输出:true。被final修饰的变量,会在class文件常量池中保存一个副本。那么b + 2在编译期间会被优化成”hello” + 2。也可以把final修饰的变量看做常量。
4.
public class Main {
public static void main(String[] args) {
String a = "hello2";
final String b = getHello();
String c = b + 2;
System.out.println((a == c));
}
public static String getHello() {
return "hello";
}
}
输出:false。因为b虽然是final变量,但是由于其赋值只能在运行期间确定。
5.
public class Main {
public static void main(String[] args) {
String a = "hello";
String b = new String("hello");
String c = new String("hello");
String d = b.intern();
System.out.println(a==b);
System.out.println(b==c);
System.out.println(b==d);
System.out.println(a==d);
}
}
输出
false
false
false
true
intern方法是一个本地方法,会在运行时常量池中查找是否存在内容相同的字符串,如果有则返回该对象的引用;如果没有,则将该字符串入池,并且返回该对象的引用,所以是true。