String详解
注意区分对象和对象的引用
首先来看一下我在jdk中找到的String源代码,这里只截取开头的小小一部分
public final class String
implements java.io.Serializable, Comparable, CharSequence {
/** The value is used for character storage. */
private final char value[];
从这里可以看出,String类是被final所修饰的,因此String类对象不可变,也不可继承。这里要注意一个误区,字符串对象不可变,但字符串变量所指的值是可变的,即引用地址可变。String变量存储的是对String对象的引用,String对象里存储的才是字符串的值【注意区分对象和对象的引用】。看下面的例子
String str = "abc"; //str是个对象引用
System.out.println(str); //输出abc
str = "abcde"; //不会出错
System.out.println(str); //输出abcde
当给str第二次赋值的时候,对象"abc"并没有被销毁,仍存放在常量池中(String自带),只是让str指向了"abcde"的内存地址,而字符串对象"abcde"是新生成的,即str第二次赋值时并不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。记住,对String对象的任何改变都不影响到原对象,相关的任何改变的操作都会生成新的对象。
String对象是否真的不可变?!
答案并不是的,那么该如何做呢?那就是使用反射(反射不太懂的可以点击链接学习——
从上面的源码可知String的成员变量是private final的,也就是初始化之后不可改变。那么在这几个成员中,value比较特殊,因为它是一个引用变量,而不是真正的对象。value是final修饰的,即final不能再指向其他数组对象,那么我想改变value指向的数组时,比如将数组中的某个位置上的字符变为下划线"_"。因为不能直接通过这个引用去修改数组,那么为了访问到至value这个引用,我们可以使用反射来访问私有成员,反射出String对象中的value属性,进而改变value数组的内容。
public static void testReflection() throws Exception {
// 创建字符串"Hello World" 并赋给引用s
String s = "Hello World";
System.out.println("s = " + s); // 打印出Hello World
// 获取String类中的value字段————Field类获取成员变量
Field valueFieldOfString = String.class.getDeclaredField("value");
valueFieldOfString.setAccessible(true); // 改变value属性的访问权限
value[5] = '_'; // 获取s对象上的value属性的值 改变value所引用的数组中的第5个字符
System.out.println("s = " + s); // 打印出Hello_World
}
下面这个例子将引出两个问题
String str="hello world" 和 String str=new String("hello world") 有什么不同
为什么使用 "==" 和 "equals" 会有不同的结果public static void main(String[] args) {
String str1 = "hello world";
String str2 = new String("hello world");
String str3 = "hello world";
String str4 = new String("hello world");
String str5 = "Hello" + " World";
String str6 = "Hello" + new String(" World");
String str7 = str2.intern();
String ex1 = "Hello";
String ex2 = " World";
String str8 = ex1 + ex2;
System.out.println(str1 == str3); //true
System.out.println(str1 == str2); //false
System.out.println(str2 == str4); //false
System.out.println(str1.equals(str2)); //true
System.out.println(str2.equals(str4)); //true
System.out.println(str1 == str5); //true
System.out.println(str1 == str6); //false
System.out.println(str1 == str7); //true
System.out.println(str1 == str8); //false
}
【运行结果】是 true、false、false、true、true、true、false、true、false
先把下面四大点看懂了,我会在最后写出【解析】
String的两种赋值方式
※ 区分【String str="HW"】和【String str=new String("HW")】
(1)字面量赋值方式 eg:String str = "Hello";
该种直接赋值的方法,JVM会去字符串常量池(String对象不可变)中寻找是否有equals("Hello")的String对象,如果有,就把该对象在字符串常量池中"Hello"的引用复制给字符串变量str,如若没有,就在堆中新建一个对象,同时把引用驻留在字符串常量池中,再把引用赋给字符串变量str。
用该方法创建字符串时,无论创建多少次,只要字符串的值(内容)相同,那么它们所指向的都是堆中的同一个对象。
该方法直接赋值给变量的字符串存放在常量池里
(2)new关键字创建新对象 eg:String str = new String("Hello");
利用new来创建字符串时,无论字符串常量池中是否有与当前值相同的对象引用,都会在堆中新开辟一块内存,创建一个新的对象。
注意:对字符串进行拼接操作,即做"+"运算的时候,分2种情况:
表达式右边是纯字符串常量,那么存放在常量池里面。eg:String str = "Hello" + "World";
表达式右边如果存在字符串引用,也就是字符串对象的句柄,那么就存放在堆里面。eg:String str = str1 + str2;
总结:常量池是方法区的一部分,而方法区是线程共享的,所以常量池也是线程共享的,且它是线程安全的,它让有相同值的引用指向同一个位置,如果引用值变化了,但是常量池中没有新的值,那么就会新建一个常量结果来交给新的引用,对于同一个对象,new出来的字符串存放在堆中,而直接赋值给变量的字符串存放在常量池里。
字符串比较——区分"=="和"equals"
"==":比较引用变量的地址,即两个对象是否引用同一地址的内容,用"=="时会检测是否指向同一个对象
"equals":比较对象的内容,即两个对象内容上是否相同
字符串用这两种比较方式都是可行的,具体看想要比较什么,总体来看,"=="稍微强大些,因为它既要求内容相同,也要求引用对象相同
intern() 方法
当使用 intern()方法时,会先查询字符串常量池是否存在当前字符串,如果存在,则返回常量池中的引用,若不存在,则将字符串添加到字符串常量池中,并返回字符串常量池中的引用。
【代码解析】
str1 == str3——str1与str3指向常量池中同一个对象,引用对象相同,因此用"=="比较时结果为true
str1 == str2——new会创建一个新的对象放在堆中,str1所指对象在常量池,即使str1与str2内容相同,但并不是同一个对象
str2 == str4——new会创建一个新的对象放在堆中,str2与str4指向不同的对象,即使内容相同
str1.equals(str2)——str1于str2各自引用对象不同,但内容相同,因此用"equals"比较时结果为true
str2.equals(str4)——str2于str4各自引用对象不同,但内容相同,因此用"equals"比较时结果为true
str1 == str5——对字符串进行拼接操作("+"运算)时,表达式右边是纯字符串常量,那么存放在常量池里面,若常量池中有该字符串,则返回该引用
str1 == str6——对字符串进行拼接操作("+"运算)时,表达式右边如果存在字符串引用,也就是字符串对象的句柄,那么就存放在堆里面
str1 == str7——intern()方法会去常量池中找是否存在当前字符,存在则返回引用,该对象引用刚好是str1所指向的
str1 == str8——str8由两个变量拼接,编译期不知道它们的具体位置,所以不会做出优化,必须要等到运行时才能确定,因此新对象的地址和前面的不同。
StringBuilder & StringBuffer
StringBuilder和StringBuffer类类似于String类,但区别在于String创建的对象是不可改变的,而StringBuilder和StringBuffer这两个类创建对象后都是可以对对象进行修改的。即String为字符串常量,而StringBuilder和StringBuffer均为字符串变量
运行速度(快到慢):StringBuilder > StringBuffer > String
StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行像String对象那样子进行创建和回收的操作,所以速度要比String快很多。
在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的
如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。
总结
String:适用于少量的字符串操作的情况
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
字符串拼接五种方法
使用 +
使用 concat
使用 StringBuilder
使用 StringBuffer
使用 StringUtils.join
注:先看完上面对String、StringBuilder和StringBuffer的详解后再看下面的文章会好理解很多的
由于String是Java中一个不可变的类,所以他一旦被实例化就无法被修改,因此所有的所谓字符串拼接,都是重新生成了一个新的字符串。
效率(用时短到长):StringBuilder
(1)"+"是Java提供的一个语法糖,而使用+拼接的字符串,它将String转成了StringBuilder后,再使用StringBuilder.append进行处理。如果不是在循环体中进行字符串拼接的话,直接使用+就好了。
(2)concat方法,其实是new了一个新的String
(3)StringUtils.join也是通过StringBuilder来实现的
(4)StringBuffer在StringBuilder的基础上,做了同步处理,所以在耗时上会相对多一些。
(5)如果在并发场景中进行字符串拼接的话,要使用StringBuffer来替代StringBuilder。因为StringBuilder是线程不安全的,而StringBuffer是线程安全的
其他参考文章: