Java中的String类型不属于八大基本数据类型,而是一个引用数据类型,所以在定义一个String对象的时候如果不直接赋值给这个对象,它的默认值就是null。我们要怎么理解String类型的不可变,在JDK源码中String这个类的value方法被final关键字修饰,导致String里的值不可以被修改。
也就是说双引号括起来的String对象,从出生到死亡,都无法发生变化。
对于任意一个String对象,都由value[]和hash组成,例如:String str = “hello”;
当我们直接使用双引号括起来的字符串初始化String对象时,该字符串会被存储在“方法区”的“字符串常量池”当中。而上图中的hash值就是用于查找字符串在常量池中的位置。
字符串常量池主要用于存储字符串常量,本质是一个哈希表(StringTable)。从JDK1.8开始,这个哈希表就存放在了堆中
为什么会将字符串放在常量区?
因为字符串在实际的开发中使用太频繁。为了执行效率,所以把字符串放到了方法区的字符串常量池当中。
从这里开始,将从内存的角度分析问题
例1:
这里输出false的原因大家应该都知道,因为str1和str2的引用指向不,那底层是如何实现的呢?
通过内存图可以清晰明了看起问题本质:
String str1 = “hello”,首先会创建一个字符数组,用来存放"hello",然后再开辟一块空间(假如为ptr1),指向这个数组,最后会在栈上用一个引用去指向ptr1。
String str2 = new String(“hello”),首先会查看StringTable是否存在"hello",这里已经存在,会开辟一块空间(假如为ptr2),将ptr1中的内容拷贝到ptr2中,那么ptr2就指向了"hello"数组,最后在栈上用一个引用去指向ptr2
JDK源码:
JDK源码的大致含义:当我们用一个字符串去实例化一个String对象时,会将这个字符串通过hash函数得到一个hash值,然后在StringTable中找,如果存在就不需要重新再创建,如果不存在就需要创建这个字符串
例2:
public class Test6 {
public static void main(String[] args) {
String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2);
}
}
因为hello在SringTable中已经存在,所以在栈上引用str2直接指向了0x3344。而例1中str2是new出来的,所以需要在堆上开辟空间,然后这块空间再指向0x3344
例3:
public class Test6 {
public static void main(String[] args) {
String str1 = "hello";
String str2 = "he" + "llo";
System.out.println(str1 == str2);
}
}
这里的结果依然是true
因为“he”和“llo”都是常量,代码在编译的时候就已经确定了最终结果是常量字符串“hello”
如果“he”或者“llo”有一个是变量,最终结果为false,例如:
例4:
public class Test6 {
public static void main(String[] args) {
String str1 = "11";
String str2 = new String("1") + new String("1");
System.out.println(str1 == str2);
}
}
还是画图分析:
当new string(“1”)时,产生的匿名对象,也会将"1"放入到字符串常量池中,当两个"1"相加,就会形成一个StringBuilder对象, 里面存放"11",当这个StringBuilder对象要转换成String对象时,需要调用to_string()方法,这个方法并不会判断"11"是否在字符串常量池中,而是直接创建新的字符串。也就是说to_string并不会入池,所以最终的结果为false
例5:
public class Test6 {
public static void main(String[] args) {
String str2 = new String("1") + new String("1");
String str1 = "11";
System.out.println(str1 == str2);
}
}
这里的结果也为false,跟例4的原因是一样的。这里就不画图了。
但是只要对这个代码稍作修改,结果就为true
intern()这个方法叫做手动入池(将String放在字符串常量池中)
本来str2(“11”)是没有入池,但是调用了intern()方法后,“11"就被放进了常量池中。当定义str1时,发现常量池中有"11”,就不需要再创建字符串"11",直接赋值,就和例2一样,最终str1和str2引用的是同一块空间,所以为true
注意:使用intern()方法时,如果常量池中没有才会入池,有的话就不会入池
equals()方法进行比较String
以上是equals的源码,是按照逐一比较字符的方式。
如果String没有重新equals方法的话,默认是比较地址
任何一个引用去调用方法,都要预防空指针异常,equals方法也不例外,例如:
除此之外,下面两种字符串引用有着本质的区别
String str1 = null;
String str2 = "";
str1这个引用,不指向任何对象
str2这个引用,指向的字符串是空的
可以使用length方法验证一下:
String作为参数,在使用之前也需要进行判断:
public static void func(String str) {
//正确写法
if (str == null || str.length() == 0) {
return;
}
//错误写法
if(str.length() == 0 || str == null) {
return;
}
}
第二种写法,先判断str的长度可能会存在空指针异常
尽量少用String去拼接字符串
public class Test7 {
public static void main(String[] args) {
String str = "abcde";
for(int i = 0; i < 10; ++i) {
str += i;
}
System.out.println(str);
}
}
这段代码,大家可能认为平平无奇,就是一个单纯的字符串拼接。但是每一次拼接都会产生临时对象,可以看一下java的汇编代码:
从汇编代码可以看出,每一次的for循环,都会创建一个StringBuilder对象,最后再调用String方法。这样是非常浪费内存和时间的,虽说循环10次影响比较小,但是循环10w次呢?那开销就很大了,所以,由于String是不可变的,所以应当少用String去拼接字符串。
StringBuffer和StringBuilder
首先来回顾下String类的特点:
任何的字符串常量都是String对象,而且String的常量一旦声明不可改变,如果改变对象内容,改变的是其引用的指向而已。
通常来讲String的操作比较简单,但是由于String的不可更改特性,为了方便字符串的修改,提供StringBuffer和StringBuilder类。
StringBuffer 和 StringBuilder 大部分功能是相同的,主要介绍 StringBuffer
public class Test8 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("fff");
System.out.println(sb);
sb.append("lll");
System.out.println(sb);
}
}
String是无法更改的,而StringBuilder对象时可以更改的,这里调用了两次append方法,都是在原有对象进行添加操作,并且有重新创建新的对象。
前面也了解了,在循环中用String去拼接字符串,会产生临时对象StringBuilder,而StringBuilder对象又是可以更改的,所以优先使用StringBuilder对象去拼接字符串
StringBuffer和StringBuilder的区别
StringBuilder中的部分append:
StringBuffer中的部分append:
通过对比,发现StringBuffer中的append方法是多了synchronized,也就是说StringBuffer是线程安全的,而StringBuilder不是线程安全的。
因此StringBuilder用于单线程,而StringBuffer用于多线程。
String、StringBuffer、StringBuilder的区别