String是java开发中使用得最频繁的类之一,今天就来学习一下String、StringBuilder和StringBuffer这几个类,分析它们的异同点以及了解各个类适用的场景。
- 了解String类以及常见的方法
- 深入理解String、StringBuffer、StringBuilder
- 不同场景下三个类的性能测试
1、了解String类以及常见的方法
String类实现在java.lang包下面,打开这个源码我们可以发现String类是被final修饰的,所以意味着String类不能被继承,并且它的成员方法都默认为final方法。在Java中,被final修饰的类是不允许被继承的,并且该类中的成员方法都默认为final方法。String类只有两个成员变量 value、hash,都是private修饰,源码中并没有提供设置这两个成员变量的方法,所以这点也说明String无法被修改的。
下面再继续看String类的一些方法实现,以substring方法为例,可以看到不是在原有的字符串上进行操作,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。所以,在这里要永远记住一点:
“对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象”。
在学习中,你可能对于String是不可变对象总是存有疑惑,看下面代码:
String s = "123456";
System.out.println("s = " + s);
s = "hello world";
System.out.println("s = " + s);
打印结果为:
s = 123456
s = hello world
这里就要区分对象和对象的引用,在上面代码中,首先创建一个String对象s,然后让s的值为“123456”, 然后又让s的值为“hello world”。 从打印结果可以看出,s的值确实改变了。那么怎么还说String对象是不可变的呢? 其实这里存在一个误区: s只是一个String对象的引用,并不是对象本身。
2、了解String、StringBuffer、StringBuilder区别
这个String类提供了数值不可改变的字符串,那么必然需要有可以修改字符串的业务,于是StringBuffer、StringBuilder自然就是提供的字符串可以进行修改,StringBuffer、StringBuilder区别在于涉及到线程安全和非线程安全,StringBuffer是线程安全,而StringBuilder是非线程安全,两者提供的方法都差不多,无非StringBuffer类提供的方法都加上synchronized进行控制线程安全而已。
那既然StringBuilder、StringBuffer是字符串可变,我们可以查看一下源码,看看到底是如果改变的,StringBuilder、StringBuffer均继承AbstractStringBuilder类,
修改字符串的方法append()、delete()、insert()具体实现也是在AbstractStringBuilder类进行处理,以append()方法为例,可以发现都是通过System类的arraycopy方法来实现的,即将原数组复制到目标数组,查看源码如下:
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
Arrays.java
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
3、再来说一下执行速度
在执行速度上,String < StringBuffer < Stringbuilder ,可以先来个简单测试,
public class test {
private static int time = 50000;
public static void main(String[] args) {
testString();
testStringBuffer();
testStringBuilder();
test1String();
test2String();
}
public static void testString () {
String s="";
long begin = System.currentTimeMillis();
for(int i=0; i<time; i++){
s += "java";
}
long over = System.currentTimeMillis();
System.out.println("操作"+s.getClass().getName()+"类型使用的时间为:"+(over-begin)+"毫秒");
}
public static void testStringBuffer () {
StringBuffer sb = new StringBuffer();
long begin = System.currentTimeMillis();
for(int i=0; i<time; i++){
sb.append("java");
}
long over = System.currentTimeMillis();
System.out.println("操作"+sb.getClass().getName()+"类型使用的时间为:"+(over-begin)+"毫秒");
}
public static void testStringBuilder () {
StringBuilder sb = new StringBuilder();
long begin = System.currentTimeMillis();
for(int i=0; i<time; i++){
sb.append("java");
}
long over = System.currentTimeMillis();
System.out.println("操作"+sb.getClass().getName()+"类型使用的时间为:"+(over-begin)+"毫秒");
}
public static void test1String () {
long begin = System.currentTimeMillis();
for(int i=0; i<time; i++){
String s = "I"+"love"+"java";
}
long over = System.currentTimeMillis();
System.out.println("字符串直接相加操作:"+(over-begin)+"毫秒");
}
public static void test2String () {
String s1 ="I";
String s2 = "love";
String s3 = "java";
long begin = System.currentTimeMillis();
for(int i=0; i<time; i++){
String s = s1+s2+s3;
}
long over = System.currentTimeMillis();
System.out.println("字符串间接相加操作:"+(over-begin)+"毫秒");
}
}
代码来自http://www.cnblogs.com/dolphin0520/p/3778589.html
执行效果如下:
可见效果非常明显,所以平时如果需要对字符串进行操作修改处理,千万要使用StringBuilder、StringBuffer来代替String。那为何产生如此大的差异尼?
这是因为String类是不可变的,即字符串常量,所以每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象。这就会对程序运行产生很大的影响,因为当内存中的无引用对象多了以后,JVM的GC进程就会进行垃圾回收,这个过程会耗费很长一段时间,因此经常改变内容的字符串最好不要用 String类的对象。而如果是使用 StringBuffer 类则结果就不一样了,每次结果都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,再改变对象引用。所以在一般情况下我们推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。
但是在某些特殊情况下, String 对象的字符串拼接其实是被 JVM 解释成了 StringBuffer 对象的拼接,所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢,而特别是以下的字符串对象生成中, String 运行速度是远要比 StringBuffer 快的:
String str ="hello" +"world";
StringBuffer sbr = new StringBuffer("hello").append("world");
但是如果要拼接的字符串来自于不同的String对象的话,那结果就不一样了:
String str1 ="hello";
String str2="world";
String str3=str1+str2;
这时候使用StringBuffer的运行速度更快,而这是我们编程时的大部分情况。
对String相关的一些判断
String a = "hello2";
String b = "hello" + 2;
System.out.println((a == b));//true
String c = b + 2;
System.out.println((a == c));//false
//对于被final修饰的变量,会在class文件常量池中保存一个副本,
// 也就是说不会通过连接而进行访问,对final变量的访问在编译期间都会直接被替代为真实的值。
// 那么String e = d + 2;在编译期间就会被优化成:String e = "hello" + 2;
final String d = "hello";
String e = d + 2;
System.out.println((a == e)); true
关于String s = new String(“xyz”);创建了几个String对象问题
两个或一个都有可能,”xyz”对应一个对象,这个对象放在字符串常量缓冲区,常量”xyz”不管出现多少遍,都是缓冲区中的那一个。NewString每写一遍,就创建一个新的对象,它使用常量”xyz”对象的内容来创建出一个新String对象。如果以前就用过’xyz’,那么这里就不会创建”xyz”了,直接从缓冲区拿,这时创建了一个StringObject;但如果以前没有用过”xyz”,那么此时就会创建一个对象并放入缓冲区,这种情况它创建两个对象。