Java中字符串驻留(String Interning)

本文深入探讨了Java中String、StringBuilder和StringBuffer的区别,通过代码示例详细分析了字符串常量池的工作原理,以及字符串拼接操作的性能比较。同时,文章还讨论了字符串驻留的实现方式和应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前提知识:

String、StringBuffer以及StringBuilder的区别

先看一个简单程序:

public class Main {
         
    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");
         
        System.out.println(str1==str2);
        System.out.println(str1==str3);
        System.out.println(str2==str4);
    }
}

运行结果:

false
true
false

为什么会出现这样的结果?下面解释一下原因:

  在class文件中有一部分 来存储编译期间生成的 字面常量以及符号引用,这部分叫做class文件常量池,在运行期间对应着方法区的运行时常量池。

  1. 因此在上述代码中,String str1 = "hello world";和String str3 = "hello world"; 都在编译期间生成了 字面常量和符号引用,运行期间字面常量"hello world"被存储在运行时常量池(当然只保存了一份)。
  2. 通过这种方式来将String对象跟引用绑定的话,JVM执行引擎会先在运行时常量池查找是否存在相同的字面常量,如果存在,则直接将引用指向已经存在的字面常量;
  3. 否则在运行时常量池开辟一个空间来存储该字面常量,并将引用指向该字面常量。
  4. 总所周知,通过new关键字来生成对象是在堆区进行的,而在堆区进行对象生成的过程是不会去检测该对象是否已经存在的。因此通过new来创建对象,创建出的一定是不同的对象,即使字符串的内容是相同的。

后来我修改str2的值,str之前是new出来的,我添加  Str2=“world” ;看他是如何操作的?

 public  static void main(String[] args) {
            String str1 = "hello world";
            String str2 = new String("hello world");
            String str3 = "world";
            String str4 = new String("hello world");
            String str5="hello world";
            
            System.out.println(str2==str3);
            str2= "world";
            System.out.println(str2==str3);
            

            System.out.println(str1==str2);     
            System.out.println(str1==str5);     
            System.out.println(str2==str4);   
            
        }

输出结果:

false
true
false
true
false

可以看出第一个为false,而第二个为true。 

  分析:没有使用new,而是先判断常量池是否有,若有的话, str2指向该引用。若没有的话,会在常量池里保留一份下来。但原先new出来的在堆上的一块空间还存在。

那么问题来了,如果这些对象有很多,且没有被回收,会造成多大的内存资源浪费。如下程序:

public class Lianxi {
         
    public static void main(String[] args) {
        String string = "";
        for(int i=0;i<10000;i++){
            string += "hello";
        }
    }
}

反汇编可知:

        string+="hello"的操作事实上会自动被JVM优化成: (每次都要new,然后改变指向,GC还要回收,速度较慢)

  StringBuilder str = new StringBuilder(string);

  str.append("hello");

  str.toString();

那么通过以下程序来比较以下:

 public  static void main(String[] args) {
            //花费时间://new StringBuider.append,
            // 每次都new
            //String
            long begin=System.currentTimeMillis();
            String str=" ";
            for(int i=0;i<50000;i++) {
                str +=" ";
            }
            long end=System.currentTimeMillis();
            System.out.println(end-begin);

            //StringBuilder 所花费的时间
            long begin1 = System.currentTimeMillis();
            StringBuilder str1 = new StringBuilder();    //只用new一次,
            str1.append(" ");
             for(int i=0;i<50000;i++) {
                 str1.append(" ");
             }
             long end1 =  System.currentTimeMillis();
             System.out.println(end1-begin1);

             //StringBuffer 所花费的时间  
            long begin2 = System.currentTimeMillis();
            StringBuffer str2 = new StringBuffer();   //只new一次
            str2.append(" ");
            for(int i=0;i<50000;i++) {
                str2.append(" ");
            }
            long end2 =  System.currentTimeMillis();
            System.out.println(end1-begin1);

        }

输出结果:(每次结果可能不太一样,取决于那时电脑的执行速度)

但结果很明显,Str速度最慢,StringBuilder和StringBuffer相差不大。

2300
1
1

但通过查询java的源代码:

StringBuilder和StringBuffer类拥有的成员属性以及成员方法基本相同,区别是StringBuffer类的成员方法前面多了一个关键字:synchronized,不用多说,这个关键字是在多线程访问时起到安全保护作用的,也就是说StringBuffer是线程安全的。

总结:

String:适用于少量的字符串操作的情况。

StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况。

StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况。

注意:(面试题的考点)


 String a = "hello2"; 
  String b = "hello";    
  String c = b + 2;       
  System.out.println((a == c));

输出: flase

原因:

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


 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; 


public class Main {
    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指向的不是同一个对象。

 


字符串驻留:

String s0= “kvill”; 
String s1=new String(”kvill”); 
String s2=new String(“kvill”); 
System.out.println( s0==s1 ); 
System.out.println( “**********” ); 

s1.intern(); 
s2=s2.intern(); //把常量池中“kvill”的引用赋给s2 

System.out.println( s0==s1); 
System.out.println( s0==s1.intern() ); 
System.out.println( s0==s2 );

输出结果:

false 
********** 
false //虽然执行了s1.intern(),但它的返回值没有赋给s1 
true //说明s1.intern()返回的是常量池中”kvill”的引用 
true 

还有一点要注意:

String s1=new String("kvill"); 
String s2=s1.intern(); 
System.out.println( s1==s1.intern() ); 
System.out.println( s1+" "+s2 ); 
System.out.println( s2==s1.intern() ); 

输出结果:

false     //s1==s1.intern()为false说明原来的“kvill”仍然存在;
kvill kvill 
true       //s2现在为常量池中“kvill”的地址,所以有s2==s1.intern()为true。

  在这个类中我们没有声名一个”kvill”常量,所以常量池中一开始是没有”kvill”的,当我们调用s1.intern()后就在常量池中新添加 了一个”kvill”常量,原来的不在常量池中的”kvill”仍然存在,也就不是“将自己的地址注册到常量池中”了。

总结:

     .intern()方法的时候,会将共享池中的字符串与外部的字符串(s)进行比较,如果共享池中有与之相等 的字符串,则不会将外部的字符串放到共享池中的,返回的只是共享池中的字符串,如果不同则将外部字符串放入共享池中,并返回其字符串的句柄(引用)-- 这样做的好处就是能够节约空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值