字符串(String)广泛应用于 Java 编程中,在 Java 中字符串不属于基础类型,属于对象,Java 提供了 String 类来创建和操作字符串。
Java 的可变类(Mutable)与不可变类(Immutable)
不可变类,是指这个类的实例一旦创建完成后,就不能改变其成员变量值。如JDK内部自带的很多不可变类:Interger、Long 等。
可变类,相对于不可变类,类的实例创建后可以改变其成员变量值,开发中创建的大部分类都属于可变类。
参考 String 和 StringBuilder,String 是不可变的,每次对于 String 对象的修改都将产生一个新的 String 对象,而原来的对象保持不变。StringBuilder 是可变的,每次对于 StringBuilder 对象的修改都作用于该对象本身,并没有产生新的对象。
如何创建不可变对象:
- 类添加
final
修饰符,保证类不被继承,或者所有方法都添加final
修饰,保证不可被重写。 - 保证所有成员变量必须私有(private),并且添加
final
修饰符。 - 只提供访问器方法(getter),不提供修改器方法(setter)
- 通过构造器初始化所有成员,如果包含可变成员变量,进行深拷贝作为初始化的值。
- 如果成员变量是可变的,添加
final
修饰符,值依然会被修改。在 getter 方法返回该成员变量时,返回对象的一个深拷贝,这也是 Java 的最佳实践之一。
String 类的设计细节
首先看一下 String 类的源码
public final class String
implements java.io.Serializable, Comparable<String>, 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
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
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);
}
........
}
可以看到以下几点:
- String 类满足以上不可变类的几条设计原则。
- String 类内部是通过 char[] 来保存字符串的。
- subString、concat 和 replace 等操作都重新生成了一个新的字符串对象进行操作,最原始的字符串并没有被改变。
字符串常量池
Java 中字符串的使用是非常高频的,而字符串和其他对象一样,创建需要消耗时间和空间,JVM为了提高性能和减少内存的开销,在实例化字符串的通过使用==字符串常量池==进行优化。
创建字符串常量时,JVM会优先检查字符串常量池,如果该字符串已经存在,那么返回常量池中的实例引用。如果字符串不存在,就会实例化该字符串并且将其放到常量池中。
字符串不可变的特性能够很好的支持这一优化,可以保证常量池中一定不存在两个相同的字符串。如果字符串是可变的,此时相同内容的字符串指向常量池的同一个内存空间,当某个变量改变了该内存的值时,其他变量的值也会发生改变。
Java 中的常量池有两种:静态常量池和运行时常量池。
静态常量池,即 *.class 文件中的常量池,class 文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用 class 文件绝大部分空间。
运行时常量池,是JVM虚拟机在完成类装载操作后,将 class 文件中的常量池载入到内存中,并保存在方法区中,通常说的常量池,就是指方法区中的运行时常量池。
字符串不可变的其他好处
同一个字符串实例可以被多个线程共享,由于不可变的特性可以不用担心线程安全。
类加载器要用到字符串(根据类的完整路径名字加载),不可变性提供了安全性,以便正确的类被加载。
能够很好支持 hash 映射,在它创建的时候 hashcode 就被缓存了(不可变),不需要重新计算,使得字符串很适合作为 Map 中的键,字符串的处理速度要快过其它的键对象。
String 类并不是所有情况下都不可变,可以通过反射机制的手段改变其值。
String 类方法
创建字符串
创建字符串的方式有多种方式,总结有两种处理方式:
(1)使用 ""
引号创建字符串,String str = "hello";
(2)使用 new
关键字创建字符串,String str = new String("hello");
(3)使用只包含常量的字符串连接符,String str = "hello " + "world";
(4)使用包含变量的字符串连接符,String str = "hello " + s1;
方式(1)和(3)创建的字符串都是常量,编译期就已经确定存储到 String Pool 中,方式(2)和(4)创建的对象会在运行时创建,存储到堆中。
使用
new
关键字创建字符串时,首先查看池中是否存在,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果没有,则在堆中创建一份,然后返回堆中的地址。注意,此时不需要从堆中复制到池中,浪费池的空间。如果要将对象放入常量池,需要调用String.intern()
方法。
注意,在使用方式(1)或(3)创建字符串时,对象并不一定创建,可能只是指向一个先前已经创建的对象。只有通过 new
关键字的方法才能保证每次都创建一个新的对象。
String.intern()
当调用 intern()
方法时,如果常量池中已经包含一个等于此 String 对象的字符串,则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。String.intern()
方法就是扩充常量池的一个方法
对于任意两个字符串 s 和 t,当且仅当 s.equals(t) == true
时,s.intern() == t.intern()
才为 true。
String s0 = "aaa"; // s0 在常量池中
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s0 == s1); // false
s1.intern(); // 虽然执行了 s1.intern(),但它的返回值没有赋给 s1
s2 = s2.intern(); //把常量池中 "aaa" 的引用赋给 s2
System.out.println(s0 == s1); // false,s1 引用的是 new 的字符串 "aaa"
System.out.println(s0 == s2); // true,s2 引用的是常量池中的 "aaa"
== 和 equal()
对于 ==
操作符,如果作用于基本数据类型的变量(byte,short,char,int,long,float,double,boolean),则比较的是其存储的"值"是否相等;如果作用于引用类型的变量(包括 String),则比较的是所指向的对象的地址(即是否指向同一个对象)。
对于 equal()
方法,在基类 Object 类中,equals()
方法默认是用来比较两个对象的引用是否相等(即是否指向同一个对象)。equals()
方法不能作用于基本数据类型的变量。
String 类对 equals()
方法进行了重写,用来比较指向的字符串对象所存储的字符串是否相等。其他的一些类诸如 Double,Date,Integer 等,也对 equals()
方法进行了重写用来比较指向的对象所存储的内容是否相等。
+
参考 http://www.cnblogs.com/xiaoxi/p/6036701.html
对以下代码段编译运行
public void test() {
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 中使用
+
进行字符串连接时,编译后将尽可能多的直接将字符串常量连接起来,形成新的字符串常量参与后续连接(限于开头,中间的多个字符串常量不会自动拼接),"xx" + "yy " 转变为 "xxyy ","zz" + "mm" 并没有变化。 -
字符串连接是从左向右依次进行,对于不同的字符串,首先以最左边的字符串为参数创建 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();
代码分析:
String s = "a" + "b" + "c";
String s1 = "a";
String s2 = "b";
String s3 = "c";
String s4 = s1 + s2 + s3;
变量 s 的创建等价于 String s = "abc";
,而变量 s4 不能在编译期进行优化,其对象创建相当于:new StringBuilder(s1).append(s2).append(s3);
。因此使用 +
时容易产生低效的代码,看下面的例子:
String s = null;
for(int i = 0; i < 100; i++) {
s += "a";
}
每做一次 +
连接就产生一个 StringBuilder 对象,append()
一次后就不再使用。下次循环再到达时重新产生 StringBuilder 对象,重复执行以上步骤直至循环结束。 如果直接采用 StringBuilder 对象进行 append()
的话,可以节省 N - 1 次创建和销毁对象的时间。
所以对于在循环中要进行字符串连接的应用,一般都是用 StringBuffer 或 StringBulider 对象来进行操作。
String、StringBuilder、StringBuffer 的对比
-
可变与不可变
String 是==不可变==字符串对象。
StringBuilder 和 StringBuffer 是==可变字符串==对象。 -
线程安全性
String 是不可变的,==线程安全==。
StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,
StringBuffer 中的方法大都采用了synchronized
关键字修饰,因此是==线程安全==的,StringBuilder 没有这个修饰,可以被认为是==非线程安全==的。 -
效率
一般来说,StringBuilder > StringBuffer > String。而在某些情况下,String 的操作会比 StringBuffer 操作快。例如,String s1 = "hello " + "world";
会明显好于StringBuffer sb = new StringBuffer("hello ").append("world");
,
在JVM眼里,前者等效于String s1 = "hello world";
总结:
- 当字符串相加操作或者改动较少的情况下,建议直接使用 String;
- 当字符串相加操作较多的情况下,建议使用 StringBuilder;
- 如果在多线程环境中,建议使用 StringBuffer。
FAQ
(a). String str = new String("abc")
创建了多少个对象?
在运行期间确实只创建了一个对象,即在堆上创建了 "abc" 对象。在类加载的过程中,在运行时常量池中创建了一个 "abc" 对象。
网友评论