在Java的学习过程中,字符串的处理是绝对绕不过去的槛。初学时总是被奇怪的String搞的莫名其妙,也曾经对面试中各种String的相等搞到晕头转向。
在学习过JVM的机制和阅读过JDK源码后才算是拨开云雾识得庐山真面目。
String类
我们经常说String类是不可变类,说String对象是不可变对象,具体是因为什么呢?
从String的源码来看,String类被final关键字修饰使得它成为不可变类。不可变类的特性使得我们不能继承String类来实现新的类,并且String类中的方法也默认为final方法,无法被覆写。
String类的底层是使用了字符数组char[]来进行存储,从String对象的操作处理来看,每次当我们试图去改变String对象的时候,实际上都没有修改到原来的对象,而是产生并返回了一个新的对象,包括使用“+”运算符进行操作。
String的相等
你肯定不止一次的在笔试题中遇到让你判断字符串是否相等的题目。其实基于上述String的特性再加一点JVM的知识,再也没有什么能难倒你。
需要明确的包括下面几个关键点:
String对象为不可变对象,对它的修改会生成一个新的对象(新的内存区域)并返回。
在编译期的字面量和符号引用会被直接编译存储在class类的常量池,如:String s = “hello”,在运行期随着类的加载进入运行期常量池。
通过new关键字创建的对象会在堆内存进行分配。
字面量和字面量的“+”操作在编译期即被优化为最终的结果。String s = “hello” + “world”即等同于String s = “hello world”。但引用值和字面量的操作不会被优化。
final关键字修饰的变量会被编译存储到常量池,在进行“+”操作时等同于字面量,会直接被优化。
String类的intern()方法会在常量池创建指定的值,如果已经存在则直接返回。
举例1
String a = “hello world”,String b = “hello” + “world”; System.out.println(a == b);
结果:true
b的定义为字面量直接相加,因此会在编译时进行优化,查看反编译的类可以看到类中b的定义为String b = “hello world”。
举例2
String a = “hello world”, String b = a + “world”; final String c = “hello”, String d = c + “world”; System.out.println(a == b); System.out.println( a == d);
结果:false;true
因b的定义是通过引用和字面量相加得到,所以并不会被优化。从反编译类可以可以看出b的定义并没有发生变化,因此b会在运行期创建和分配。而c因为有final关键字修饰,从而d在编译期会直接被优化为“hello world”
举例3
String a = “hello world”, String b = new String(“hello world”), String c = b.intern(); System.out.println(a == b); System.out.println(a == c)
结果:false, true
因a为字面常量,而b使用new来创建,会在堆内存进行分配,因此a==b结果为false;
C通过String的intern()方法在常量池创建,而”hello world”已经存在,所以直接返回与a相同的引用。
StringBuilder和StringBuffer
首先看下StringBuilder和StringBuffer的区别。从源码可以很清楚看出两个类都是继承自同一个类,所以底层的实现基本相同,唯一的区别在于StringBuffer是设计为线程安全的,所以提供的公共方法都增加synchroinzed关键字来保证同步。也因此在使用上StringBuffer的效率会比StringBuilder要低。所以在不需要考虑线程安全的情况下,我们通常选择StringBuilder。
其次,设计StringBuilder或者StringBuffer的意义何在?
从上面我们知道String对象的不可变性导致当我们对String对象进行修改时总是会创建一个新对象(涉及背后的一系列内存分配操作),因此当需要频繁改变String对象时,比如常见的循环操作对String对象进行修改,会造成大量的内存分配操作导致效率降低。而StringBuilder或StringBuffer实现了对底层存储数组的直接修改来提升效率。
以上就是Java字符串的那些事儿,相信以后它不会再造成困扰了。