String、StringBuilder和StringBuffer的那些事儿

引言

先来看一段代码

public class TestMain {
	public static void main(String[] args) {
		String str0 = "123";
                String str1 = "123";
                System.out.println(str0 == str1);
        
                String str2 = new String("234");
                String str3 = new String("234");
                System.out.println(str2 == str3);
	}
}

运行结果:
true
false

为什么String=String,而new String!=new String

在JVM中有一块区域叫做常量池。 常量池中的数据是那些在编译期间被确定,并被保存在已编译的.class文件中的一些数据。除了包含所有的8种基本数据类型(boolean、byte、char、short、int、float、double、long)外,还有String及其数组的常量值,另外还有一些以文本形式出现的符号引用。

Java栈的特点是存取速度快(比堆块),但是空间小,数据生命周期固定,只能生存到方法结束。
定义的boolean b=true、char c='c'、String str="123",分析如下:

(1)true、c、123,等号右边的指的是编译期间可以被确定的内容,都被维护在常量池中
(2)b、c、str,紧挨着等号左边的是引用,引用的内容是等号右边数据在常量池中的地址
(3)boolean、char、String,引用的类型

栈有一个特点,就是数据共享。回到引言的例子,第三行String str0 = "123",编译时在常量池中创建了一个常量"123"。执行第四行String str1 = "123"时,先去常量池中找有没有"123",发现有,str1也指向常量池中的"123"。因此第五行str0 == str1的执行结果是true,因为str0和str1指向的都是常量池中"123"这个字符串的地址。分析了String, 基本数据类型也是如此:先看常量池中有没有要创建的数据,有就返回数据的地址,没有就创建一个。

Java虚拟机的解释器每遇到一个new关键字,都会在堆内存中开辟一块内存来存放一个对象,所以str2、str3指向的是两块不同的堆内存,因此str2 == str3的执行结果是false。

为什么使用StringBuilder和StringBuffer拼接字符串?

看下面一段代码

public class TestMain {
	public static void main(String[] args) {
		String str = "111";
                str += "222";
                str += "333";
                System.out.println(str);
	}
}

这段代码编译器是怎么处理的呢?编译器每次碰到“+”的时候,都会new一个StringBuilder出来,接着调用append方法,最后调用toString方法,生成新字符串。如果代码中有很多的“+”,每个“+”都会new一个StringBuilder,这种方式对内存是一种浪费,效率很不好。

看下面一段代码

public class TestMain {
	public static void main(String[] args) {
		StringBuilder sb = new StringBuilder("111");
		sb.append("222");
		sb.append("333");
		System.out.println(sb.toString());
	}
}

StringBuilder在底层维护了一个char数组,每次append的时候就往char数组里面放字符,在最终sb.toString()的时候,用一个new String()方法把char数组里面的内容都转成String。这样,整个过程中只产生了一个StringBuilder对象与一个String对象,非常节省空间。

StringBuilder唯一的性能损耗点在于char数组不够的时候需要进行扩容,扩容需要进行数组拷贝,一定程度上降低了效率。如果可以估计到要拼接的字符串的长度的话,尽量利用构造函数指定他们的长度,可以一定程度的避免扩容带来的性能上的损耗。

StringBuffer和StringBuilder原理一样,唯一的区别是StringBuffer是线程安全的,它对所有方法都做了同步。StringBuilder是线程非安全的。所以在不涉及线程安全的场景,比如方法内部,尽量使用StringBuilder,避免同步带来的消耗。

看下面一段代码

public class TestMain {
	public static void main(String[] args) {
		String str = "111" + "222" + "333" + "444";
		System.out.println(str);
	}
}

对于连续+的情况(存在变量引用的情况下),实际上编译时JVM会只产生一个StringBuilder,并连续append等号后面的字符串。因为“111″、“222″、“333″、“444″都是编译期间就可以确定的常量,因此第三行的代码JVM在编译的时候并不会生成一个StringBuilder,而是直接生成字符串“111222333444″,相当于String str = "111222333444"


但是这么写得很少,主要原因有两点:
(1)例子比较简单,但实际上大量的“+”会导致代码的可读性非常差;

(2)待拼接的内容可能从各种地方获取,比如调用接口、从.properties文件中、从.xml文件中,这样的场景下尽管用多个“+”的方式也不是不可以,但会让代码维护性不太好。


一些知识点总结

(1)String类是final类,意味着String类不能被继承,并且它的成员方法都默认为final方法。在Java中,被final修饰的类是不允许被继承的,并且该类中的成员方法都默认为final方法。

(2)String类其实是通过char数组来保存字符串的。

(3)String无论是substring、concat还是replace操作,都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象

(4)对于直接相加字符串,效率很高,因为在编译期间便确定了它的值,也就是说形如"I"+"love"+"java"的字符串相加,在编译期间便被优化成了"Ilovejava"。对于间接相加(即包含字符串引用),形如s1+s2+s3效率要比直接相加低,因为在编译期间不会对引用变量进行优化。

(5)String、StringBuilder、StringBuffer三者的执行效率:StringBuilder > StringBuffer > String
当然这个是相对的,不一定在所有情况下都是这样。比如String str = "hello" + "world"的效率就比 StringBuilder st = new StringBuilder().append("hello").append("world")要高。
因此,这三个类是各有利弊,应当根据不同的情况来进行选择使用:
当字符串相加操作或者改动较少的情况下,建议使用 String str="hello"这种形式;

当字符串相加操作较多的情况下,如不考虑线程安全,建议使用StringBuilder;如果采用了多线程,要考虑线程安全,则使用StringBuffer。


常见面试题

(1)下面这段代码的输出结果是什么?

public class TestMain {
	public static void main(String[] args) {
		String a = "hello2";
		String b = "hello" + 2;
		System.out.println((a == b));
	}
}
输出结果为:true。原因很简单,"hello" + 2在编译期间就已经被优化成 "hello2",因此在运行期间,变量a和变量b指向的是同一个对象。

(2)下面这段代码的输出结果是什么?

public class TestMain {
	public static void main(String[] args) {
		String a = "hello2";
		String b = "hello";
		String c = b + 2;
		System.out.println((a == c));
	}
}

输出结果为:false。由于有符号引用的存在,所以String c = b + 2;不会在编译期间被优化,不会把b+2当做常量来处理,因此这种方式生成的对象其实是保存在堆上的。因此a和c指向的并不是同一个对象。

(3)下面这段代码的输出结果是什么?

public class TestMain {
	public static void main(String[] args) {
		String a = "hello2";
		final String b = "hello";
		String c = b + 2;
		System.out.println((a == c));
	}
}

输出结果为:true。对于被final修饰的变量,会在class文件常量池中保存一个副本,对final变量的访问在编译期间都会直接被替代为真实的值。那么String c = b + 2;在编译期间就会被优化成:String c = "hello" + 2;

(4)下面这段代码的输出结果是什么?

public class TestMain {
	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修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定,因此a和c指向的不是同一个对象。

(5)下面这段代码的输出结果是什么?

public class TestMain {
	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。这里涉及到String.intern方法的使用。在String类中,intern方法是一个本地方法,在运行时查找常量池中是否存在内容相同的字符串,如果存在则返回指向该字符串的引用;如果不存在,则会将该字符串入池,并返回一个指向该字符串的引用。因此,a和d指向的是同一个对象。

(6)下面这段代码1)和2)的区别是什么?

public class TestMain {
	public static void main(String[] args) {
		String str1 = "I";
		String str2 = "I";
		str1 += "love" + "java"; 		// 1)
		str2 = str2 + "love" + "java"; 	        // 2)
	}
}

1)的效率比2)的效率要高,1)中的"love" + "java"在编译期间会被优化成"lovejava",而2)中的不会被优化。在1)中只进行了一次append操作,而在2)中进行了两次append操作。

(7)看下面问题

下面代码
String str = new String("abc");
创建了几个String对象?

首先必须弄清楚创建对象的含义,创建是什么时候创建的?这段代码在运行期间会创建两个对象么?这道题目让人混淆的地方就是这里,这段代码在运行期间确实只创建了一个对象,即在堆上创建了"abc"对象。而为什么大家都在说是两个对象呢?这里面要澄清一个概念,该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。

换成另一种问法:

下面代码
String str = new String("abc");
在执行(运行)时涉及到几个String对象?

答案:两个。一个是字符串常量"xyz"所对应的、驻留(intern)在一个全局共享的字符串常量池中的实例,另一个是通过new String(String)创建并初始化的、内容与"xyz"相同的实例。

在面试的时候如果遇到这个问题,可以向面试官询问清楚“是这段代码执行过程中创建了多少个对象还是涉及到多少个对象“再根据具体的来进行回答。

再换一个问题来问:

下面代码
String str = new String("abc");
涉及几个用户声明的String类型的变量?

答案:一个。就是String str。

把问题换成下面这个版本,答案也一样:

下面代码
String str = null;
涉及几个用户声明的String类型的变量?


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值