深入理解String

一、String知识总结

  • String被final修饰,不可变对象,也就是不能被继承
  • String是通过Char数组来保存字符串的
  • String提供的操作字符串方法,比如subString,replace,concat,连接符,都不是在原有字符串操作,而是重新生成了一个新的对象
  • String对象重写了equals和hashCode方法,所以equals比较的是字符的值,并不是内存地址
  • 为了优化String对象分配的性能,JVM使用字符串常量池。每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中

二、常量池

Java中的常量池分为两类:

  • 静态常量池,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有还有一项信息是常量池
  • 运行时常量池,jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,这里需要注意的是字符串常量池在jdk1.7被移到堆中

看段代码:

String a = "text";
String b = "text";
String c = new String("text");
  • a,b,以及字面text都指向字符串常量池的"text"对象,它们在栈上内存地址相同
  • new关键字会在堆上创建对象,但是由于String对象在内存中不可能存在完全相同的两个对象,所以堆上的text对象会引用字符串常量池的text
public class Test {
    public static void main(String[] args) {
        String a = "text";  //1
        String b = "text";  //2
        String c = new String("text"); //3
        System.out.println(a == b);
        System.out.println(a == c);
    }
}

执行结果:
true
false
  1. 执行1行代码时,JVM首先会去字符串池中查找是否存在"text"这个对象
    • 不存在在字符串池中创建"text"这个对象,然后将池中"text"这个对象的引用地址返回给字符串常量a
    • 如果存在,则不创建任何对象,直接将池中"text"这个对象的地址返回,这里执行第2行代码,就是将此值赋值给b,所以a==b结果是ture
  2. 执行第3行代码,new关键字新建一个字符串对象时,JVM首先在字符串池中查找有没有"text"这个字符串对象
    • 如果有,则不在池中再去创建"text"这个对象了,直接在堆中创建一个"text"字符串对象,然后将堆中的这个"text"对象的地址返回赋给引用c,这样,c就指向了堆中创建的这个"text"字符串对象
    • 如果没有,则首先在字符串池中创建一个"text"字符串对象,然后再在堆中创建一个"text"字符串对象,然后将堆中这个"text"字符串对象的地址返回赋给c引用,这样,c指向了堆中创建的这个"aaa"字符串对象,所以a == c为false

上面详细的分析了String两种不同的创建对象的方式,这里不得不提到一个经典问题:

String c = new String(“text”);创建了几个对象

答案:可能是1个也可能是2个,这取决于字符串常量池是否已存在text对象

三、问题分析

问题1:
public void test1(){
    String s0="helloworld";
    String s1="helloworld";
    String s2="hello"+"world";
    System.out.println("===========test3============");
    System.out.println(s0==s1); //true 可以看出s0跟s1是指向同一个对象 
    System.out.println(s0==s2); //true 可以看出s0跟s2是指向同一个对象 
}

分析:当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,在编译期就可以确定。 所以s2也同样在编译期就被解析为一个字符串常量,所以s2也是常量池中"helloworld”的一个引用。所以我们得出s0s1s2

问题2:
public void test2(){
    String s0="helloworld"; 
    String s1=new String("helloworld"); 
    String s2="hello" + new String("world");
    String s3 = "hello";
    String s4 = s3 + "world";
    final String s5 =  "hello";
    String s6 = s5 + "world"; 
    System.out.println("===========test2============");
    System.out.println( s0==s1 ); //false  
    System.out.println( s0==s2 ); //false 
    System.out.println( s1==s2 ); //false
    System.out.println( s0==s4 );//false
    System.out.println( s0==s6 );//true 
}

分析:

  • 用new String() 创建的字符串不是常量,不能在编译期就确定,并且肯定会在堆中创建对象,所以s1,s2都是指向堆中的对象
  • 使用连接符是可以编译器确定,但是引用的值s3在程序编译期是无法确定的,即s3 + "world"无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给s4,所以还是false
  • 对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中,所以s0 == s6
问题3:
public void test3(){
    String str1="abc";   
    String str2="def";   
    String str3=str1+str2;
    System.out.println("===========test3============");
    System.out.println(str3=="abcdef"); //false
}

分析:

  • JVM对String str="abc"对象放在常量池中是在编译时做的,而String str3=str1+str2是在运行时刻才能知道的
  • str1 + str2通过StringBuilder的最后一步toString()方法还原一个新的String对象"abcdef",因此堆中开辟一块空间存放此对象,str3指向堆中的"abcdef"对象,而"abcdef"是字符串池中的对象,所以结果为false
  • 字面量"+“拼接是在编译期间进行的,拼接后的字符串存放在字符串池中;而字符串引用的”+"拼接运算是在运行时进行的,新创建的字符串存放在堆中
问题4:
public void test4(){
    String s0 = "a1"; 
    String s1 = "a" + 1; 
    System.out.println("===========test4============");
    System.out.println((s0 == s1)); //result = true  
    String s2 = "atrue"; 
    String s3= "a" + "true"; 
    System.out.println((s2 == s3)); //result = true  
    String s4 = "a3.4"; 
    String s5 = "a" + 3.4; 
    System.out.println((s4 == s5)); //result = true
}

分析:在程序编译期,JVM就将常量字符串的"+“连接优化为连接后的值,拿"a” + 1来说,经编译器优化后在class中就已经是a1。在编译期其字符串常量的值就确定下来,故上面程序最终的结果都为true

使用+号性能低下原因
String s1  =  "a"; 
String s2  =  "b"; 
String s3  =  "c"; 
String s4  =  s1  +  s2  +  s3;

实际上是使用StringBuilde对象完成的,等价于

String c = new StringBuilder("a").append("b").append("c").toString()

实际上是产生了一个StringBuilder对象和一个String对象,如果在for循环中使用+,那么每次都会产生多个对象,不如直接使用StringBuilder效率高,可以节省 N - 1 次创建和销毁对象的时间,注意这里强调的是循环

四、String.intern()方法

  • 如果字符串未在 Pool 中,那么就往 Pool 中增加一条记录,然后返回 Pool 中的引用
  • 如果已经在 Pool 中,直接返回 Pool 中的引用
  • 对于任意两个字符串 a 和 b,当且仅当 a.equals(b) 为 true 时,a.intern() == b.intern() 才为 true
  • 对于class文件中的常量池即静态常量池来说,在运行期间被jvm装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法

五、String、StringBuffer、StringBuilder的区别

  • 可变与不可变:String是不可变字符串对象,StringBuilder和StringBuffer是可变字符串对象(其内部的字符数组长度可变),StringBuilder底层也是char[]数组,但是没有final,每次调用append方法,就会和ArrayList一样,进行数组复制,但是不会预分配空间,很消耗性能所以最好给定一个较大的初始值
  • 是否多线程安全:String中的对象是不可变的,也就可以理解为常量,显然线程安全。StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,只是StringBuffer 中的方法大都采用了synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是非线程安全的
  • String、StringBuilder、StringBuffer三者的执行效率:
    StringBuilder > StringBuffer > String 当然这个是相对的,不一定在所有情况下都是这样。比如String str = “hello”+ "world"的效率就比 StringBuilder st = new StringBuilder().append(“hello”).append(“world”)要高,这是因为在编译期就可以确定,如果是在运行期,那么+效率较低。因此,这三个类是各有利弊,应当根据不同的情况来进行选择使用:
    • 当字符串相加操作或者改动较少的情况下,建议使用 String str="hello"这种形式;
    • 当字符串相加操作较多的情况下,建议使用StringBuilder,如果采用了多线程,则使用StringBuffer。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值