让人头疼的String与字符串常量池

本文详细解析了Java中字符串的工作原理,包括字符串池的位置变化、字符串常量与字符型常量的区别、String类的不可变性原因、String.intern()方法的作用及字符串与数组的对比等内容。

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

首先,本文测试环境为jdk1.8。jdk1.6中字符串池中放的数据与jdk1.8不同。1.6中字符串常量池存在于永久代中,字符串常量池中存放的是字符串实例对象。1.7 ,1.8中字符串常量池存在于堆中,字符串常量池中存放的是字符串实例对象在堆中的地址,字符串实例对象本身存放在堆中(方法区是规范是概念,而永久代和元空间是实现)。

字符型常量和字符串常量的区别

  • 形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符
  • 含义上: 字符常量相当于一个整形值(ASCII值),可以参加表达式运算 字符串常量代表一个地址值(该字符串在内存中存放位置)
  • 占内存大小 字符常量只占一个字节 字符串常量占若干个字节(至少一个字符结束标志)

关于JVM运行区域参考博文:认真学,JVM内存模型(运行时数据区)

只有在运行的时候创建字符串对象,类加载过程中不会创建—只会创建字面量值放在class的常量池中。

运行的时候,字符串池独立于运行时常量池,字符串池是在堆中存放(jdk1.6不是)。

  • jdk1.6下,str.intern()如果发现池中没有该对象,就会复制一份对象放进池中;如果有了,直接返回该对象的引用。

  • jdk1.8下,str.intern()如果发现池中没有该对象,就会在堆中创建对象,把该对象的引用放进池中;如果有了,返回池中的引用。

显示声明的如String str=“abc”;会直接放进pool中。

【1】String为什么是不可变的

简单来说就是String类利用了final修饰的char类型数组存储字符,源码如下:

private final char value[];

① 只有当字符串是不可变的,字符串池才有可能实现

字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。

但如果字符串是可变的,那么String interning将不能实现(译者注:String intern是指对不同的字符串仅仅只保存一个,即不会保存多个相同的字符串。),因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。


② 如果字符串是可变的,那么会引起很严重的安全问题

譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。


③ 因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享

这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。


④ 类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。

譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。


⑤ 因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算

这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。


String中的final用法和理解

final只对引用的"值"(即内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。

final StringBuffer a = new StringBuffer("111");
final StringBuffer b = new StringBuffer("222");
a=b;//此句编译不通过

final StringBuffer a = new StringBuffer("111");
a.append("222");//编译通过

能否改变String的值?

通过反射是可以修改所谓的“不可变”对象。

String s = "Hello World";
System.out.println("s = " + s); // Hello World
// 获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
// 改变value属性的访问权限
valueFieldOfString.setAccessible(true);
// 获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);
// 改变value所引用的数组中的第5个字符
value[5] = '_';
System.out.println("s = " + s); // Hello_World

结果:

s = Hello World
s = Hello_World

用反射可以访问私有成员, 然后反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。


【2】Some Sick Statement

First

public static void main(String[] args){

     String str1 = "abc";
     String str2 = new String("abc");
     System.out.println(str1==str2);//false
     //一个来源于字符串常量池,一个来源于堆,当然不等

     String str3 = new String("abc") + new String("abc");
     String str4 = "abcabc";
     System.out.println(str3==str4);//false
     //str3为两个对象拼接将会生成一个新的对象,str4来源于字符串常量池,当然不等

     String str7 = str1+str2;
     System.out.println(str3==str7);
     //false 对象拼接生成新的对象,不等
     System.out.println(str4==str7);
     //false 一个字符串常量池,一个堆中对象,当然不等

     String str8 = "abc"+"abc";//编译器优化 str8="abcabc"
     System.out.println(str3==str8);//false
     System.out.println(str4==str8);//true

 }

First-Compile:

public static void main(String[] args) {
       String str1 = "abc";
       String str2 = new String("abc");
       System.out.println(str1 == str2);
       String str3 = new String("abc") + new String("abc");
       String str4 = "abcabc";
       System.out.println(str3 == str4);
       String str7 = str1 + str2;
       System.out.println(str3 == str7);
       System.out.println(str4 == str7);
       //直接在编译阶段就拼接了
       String str8 = "abcabc";
       System.out.println(str3 == str8);
       System.out.println(str4 == str8);
   }

JVM对于字符串常量的"+“号连接,将程序编译期,JVM就将常量字符串的”+“连接优化为连接后的值,拿"abc"+"abc"来说,经编译器优化后在class中就已经是"abcabc”。在编译期其字符串常量的值就确定下来,故str4==str8为true

然后对象引用的拼接,如String str7 = str1+str2;将会新建一个String对象,故str3==str7为false

对于直接做+运算的两个字符串(字面量)常量,并不会放入String常量池中,而是直接把运算后的结果放入常量池中。

对于先声明的字符串字面量常量,会放入常量池。但是若使用字面量的引用进行运算就不会把运算后的结果放入常量池中了。如String str7 = str1 + str2; 常量池中不会有str7。


Second

String a = "ab";  
String b = "b";  
String c = "a" + b;  
System.out.println((a == c)); //result = false  

JVM对于字符串引用,由于在字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a" + b无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给b。所以上面程序的结果也就为false。


Third

String a = "ab";
final String b = "b";
String c = "a" + b;
System.out.println((a == c)); //result = true   

和上面唯一不同的是bb字符串加了final修饰,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。

所以此时的"a" + b和"a" + "b"效果是一样的。故上面程序的结果为true。

其编译后如下所示:

  String a = "ab";
  String b = "b";
  String c = "ab";

可以很明显看到a===c。


Fourth

public static void main(String[] args){

        String str3 = new String("abc") + new String("abc");
        str3.intern();
        String str4=str3.intern();
        String str7= "abcabc";
        System.out.println(str3==str4);//true
        System.out.println(str3==str7);//true
        System.out.println(str4==str7);//true
		
		// 注意,此时str5.intern()时机不同
        String str5 = new String("abcd") + new String("abcd");
        String str6 = "abcdabcd";
        String str8=str5.intern();
        //注意此时str5保存的是堆中的引用,str8保存的是字符串常量池中的引用
        System.out.println(str5==str6);//false
        System.out.println(str5==str8);//false
        System.out.println(str8==str6);//true
    }

String str3 = new String("abc") + new String("abc");会在堆中创建一个对象new String(“abcabc”);假设内存地址为&01;str3.intern();会把"abcabc"对象放到字符串池中(jdk1.6),jdk1.8在则保存该对象的引用。

假设用内存地址hashcode(abcabc hashcode为957324)保存,即957324=>&01。

// 此时str3.intern()发现字符串池中已经有了,就返回,此时str4指向&01
String str4=str3.intern();
//字符串池中已经有了abcabc hashcode,则直接获取,即str7指向&01
String str7= "abcabc";
//故而,如下都为true。
System.out.println(str3==str4);//true
System.out.println(str3==str7);//true
System.out.println(str4==str7);//true

对于str5 str6 str8则是由于str5保存的是堆中string对象的引用,str6和str8保存的都是字符串常量池中的引用。故而str5!=str6,str6==str8


Fifth

public static void main(String[] args){
// 同上解释,str1==str2;
    String str1 = new String("abc") + new String("abc");
    str1.intern();
    //str1指向字符串常量池中的abcabc
    String str2 = "abcabc";
    System.out.println(str1 == str2);//true

// 假设str3内存地址为&03
    String str3 = new String("abcd") + new String("abcd");
    //字符串池中放abcdabcd,此时str4==&04
    String str4 = "abcdabcd";
    //pool中已有,直接返回&04
    str3.intern();
    //&03==&04  false
    System.out.println(str3 == str4);//false
    str3=str3.intern();将str3指向池中的引用
	System.out.println(str3 == str4);//true
}

Sixth

public static void main(String[] args){
//会创建两个对象 一个pool中hello &01;
//一个堆中new String("hello");&02;
//此时str2==&02
    String str2 = new String("hello");
    //池中已有,直接返回
    str2.intern();
    //从池中获取 &01
    String str = "hello";
    // &01==&02  false
    System.out.println(str==str2);// 运行后结果为false

//解释过程同上
    String str3 = new String("world");
    String str4 = "world";
    str3.intern();
    System.out.println(str4==str3);// 运行后结果为false
}

Seventh

public static void main(String[] args) {  
    String s = new String(new char[]{'1','4','7'});    
    s.intern();    
    String s2 = "147";  
    System.out.println(s == s2);    
      
    String s3 = "258";    
    s3.intern();    
    String s4 = "258";  
    System.out.println(s3 == s4);   
}  

上面的程序在jdk1.6下输出false true;在jdk1.8下输出true、true。

更多参考论坛:

字符串对象和pool及堆

String类的intern()方法详解

String类的intern()方法详解


【3】字符串与数组

示例一:

public class A {
    public final String tempString="world";
    //这里可以把final去掉,结果等同!!
    
    public final char[] charArray="Hello".toCharArray();
    
    public char[] getCharArray() {
        return charArray;
    }
    public String getTempString() {
        return tempString;
    }
}

测试类如下:

public class TestA {
    public static void main(String[] args) {
        A a1=new  A();
        A a2=new A();

        System.out.println(a1.charArray==a2.charArray);
        System.out.println(a1.tempString==a2.tempString);
    }
}

输出结果如下:

false
true

① 字符串为什么会输出true

  • 一个Class字节码文件只有一个常量池,常量池被所有线程共享。

  • 在常量池中,字符串被存储为一个字符序列,每个字符序列都对应一个String对象,该对象保存在堆中。所以也就是说为什么String temp=“xxx”;能够当成一个对象使用!!

  • 如果多个线程去访问A类中的String字符串,每次都会到常量区中去找该字符序列的引用。

  • 所以访问A类被创建的两个A类型对象的String字符串对比会输出true。


② 数组为什么是false

声明(不管是通过new还是通过直接写一个数组)一个数组其实在Java中就等同创建了一个对象,即每次创建类的对象都会自动创建一个新的数组空间。

其中要注意的是:常量池中存储字符数组只是存储的是每个字符或者字符串。

为了证明每次获取的final数组地址不一样,并且数组中的字符都会存储在常量池中,我们需要参考另外一个代码。

示例二:

public class A {
    public String tempString="world";
    public final String tempStringArray[]={"Fire","Lang"};
    public final char[] charArray={'h','e','l','l','o'};
    public Character charx='l';

    public char[] getCharArray() {
        return charArray;
    }
    public String getTempString() {
        return tempString;
    }
    public String[] getTempStringArray() {
        return tempStringArray;
    }
    public Character getCharx() {
        return charx;
    }
}

测试类如下:

public class TestA {

    public static void main(String[] args) {
        A a1=new  A();
        A a2=new A();
        System.out.println(a1.tempString==a2.tempString);
        System.out.println(a1.tempStringArray==a2.tempStringArray);//看这里
        System.out.println("#####################");//看这里
        System.out.println(a1.tempStringArray[0]==a2.tempStringArray[0]);
        System.out.println(a1.tempStringArray[0]=="Fire");
        System.out.println("#####################");
        System.out.println(a1.charArray==a2.charArray);
        System.out.println(a1.charx==a2.charx);
    }
}

输出结果如下:

true
false
#####################
true
true
#####################
false
true

【4】字符串与常量池

实验一:

public class D {

    public static void main(String[] args){
        String str = "hello";
        String str2 = new String("MySQL");
        String str3 = new String("hello");

        System.out.println(str==str3);// 运行后结果为false
    }
}

使用javap -v D.class命令查看常量池:

这里写图片描述

new一个对象时,明明是在堆中实例化一个对象,怎么会出现常量池中?

  • 这里的"MySQL"并不是字符串常量出现在常量池中的,而是以字面量出现的,实例化操作(new的过程)是在运行时才执行的,编译时并没有在堆中生成相应的对象。

  • 最后输出的结果之所以是false,就是因为str指向的”hello”是存放在常量池中的,而str3指向的”hello”是存放在堆中的,==比较的是引用(地址),当然是false。


【5】 String intern()究竟是什么

jdk1.8 , jdk1.7 , jdk1.6源码如下:

你没有看错,虽然实现不一样,但是源码确实是一样的。

/**
     * Returns a canonical representation for the string object.
     *  # 返回字符串对象的标准表示
     * <p>
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * # 一个被String类私自维护的String池,初始化是空的。
     * <p>
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method,
     * # 如果intern 方法被调用,如果String池中已经包含一个String,
     * #且两个String通过equals方法判断相等
     *  then the string from the pool is returned. 
     * # 然后这个池子中的String将会被返回。
     * Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * # 否则,这个String对象将会被放入池子中
     * # 并且返回一个该String 对象的引用。
     * <p>
     * It follows that for any two strings {@code s} and {@code t},
     * {@code s.intern() == t.intern()} is {@code true}
     * if and only if {@code s.equals(t)} is {@code true}.
     * <p>
     * All literal strings and string-valued constant expressions are interned. 
     * # 有文字字符串和字符串值常量表达式都被插入(拘留)。
     * String literals are defined in section 3.10.5 of the
     * <cite>The Java&trade; Language Specification</cite>.
     *
     * @return  a string that has the same contents as this string, 
     * but is guaranteed to be from a pool of unique strings.
     * # 返回一个和调用者一样内容的String,
     */
    public native String intern();

Java语言并不要求常量一定只能在编译期产生,运行时也可能将新的常量放入常量池中,这种特性用的最多的就是String.intern()方法。

String的intern()方法就是扩充常量池的一个方法。当一个String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加 一个Unicode等于str的字符串并返回它的引用。

确切的说,jdk1.8下,如果发现字符串池中没有,则将该对象的引用保存到字符串池中并返回。

示例如下:

public static void main(String[] args){

      String s0= "xyz";
      String s1=new String("xyz");
      String s2=new String("xyz");

      System.out.println(s0==s1);//很显然的false
      s1.intern();//我S1想往常量池放xyz
      s2=s2.intern(); //把常量池中“xyz”的引用赋给s2
      System.out.println( s0==s1);
      // false虽然执行了s1.intern(),但它的返回值没有赋给s1 
      System.out.println( s0==s1.intern() );
      //true 说明s1.intern()返回的是常量池中”xyz”的引用 
      System.out.println( s0==s2 );//true
  }

【6】字符串常量池与JVM内存模型

字符串常量池是全局的,JVM 中独此一份,因此也称为全局字符串常量池。运行时常量池中的字符串字面量若是成员的,则在类的加载初始化阶段就使用到了字符串常量池;若是本地的,则在使用到的时候(执行此代码时)才会使用到字符串常量池。

其实,“使用常量池”对应的字节码是一个 ldc 指令,在给 String 类型的引用赋值的时候会先执行这个指令,看常量池中是否存在这个字符串对象的引用,若有就直接返回这个引用,若没有,就在堆里创建这个字符串对象并在字符串常量池中记录下这个引用(jdk1.7)。

String 类的 intern() 方法还可在运行期间把字符串放到字符串常量池中。JVM 中除了字符串常量池,8种基本数据类型中除了两种浮点类型剩余的6种基本数据类型的包装类,都使用了缓冲池技术,但是 Byte、Short、Integer、Long、Character 这5种整型的包装类也只是在对应值在 [-128,127] 时才会使用缓冲池,超出此范围仍然会去创建新的对象。其中:

  • 在 jdk1.6(含)之前也是方法区的一部分,并且其中存放的是字符串的实例;
  • 在 jdk1.7(含)之后是在堆内存之中,存储的是字符串对象的引用,字符串实例是在堆中;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流烟默

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值