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

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

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


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


形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符

含义上: 字符常量相当于一个整形值(ASCII值),可以参加表达式运算 字符串常量代表一个地址值(该字符串在内存中存放位置)

占内存大小 字符常量只占一个字节 字符串常量占若干个字节(至少一个字符结束标志)


只有在运行的时候创建字符串对象,类加载过程中不会创建—只会创建字面量值放在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。


【3】字符串与数组

示例一:

publpublic 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】字符串与常量池

实验一:

publicpublic 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(含)之后是在堆内存之中,存储的是字符串对象的引用,字符串实例是在堆中;

目录
相关文章
|
5天前
|
Java 索引
String字符串常用函数以及示例 JAVA基础
String字符串常用函数以及示例 JAVA基础
|
6天前
|
存储 缓存 测试技术
CMake String函数:如何巧妙地在cmake中操作字符串
CMake String函数:如何巧妙地在cmake中操作字符串
169 0
|
6天前
|
存储 XML 缓存
Java字符串内幕:String、StringBuffer和StringBuilder的奥秘
Java字符串内幕:String、StringBuffer和StringBuilder的奥秘
27 0
string(字符串)
在 Lua 中,字符串可以用双引号或单引号定义,如 `string1 = &quot;this is string1&quot;` 和 `string2 = &#39;this is string2&#39;`。多行字符串可由两个方括号包围,例如 `html` 变量所示,它包含了一个 HTML 片段。Lua 会尝试将数字字符串转换为数值进行算术运算,但混合字符串和数字可能导致错误,如 `&quot;error&quot; + 1`。
|
4天前
|
存储 Java
Java基础复习(DayThree):字符串基础与StringBuffer、StringBuilder源码研究
Java基础复习(DayThree):字符串基础与StringBuffer、StringBuilder源码研究
Java基础复习(DayThree):字符串基础与StringBuffer、StringBuilder源码研究
|
6天前
|
缓存 安全 Java
【Java基础】String、StringBuffer和StringBuilder三种字符串对比
【Java基础】String、StringBuffer和StringBuilder三种字符串对比
9 0
|
6天前
|
存储 编译器 Linux
标准库中的string类(中)+仅仅反转字母+字符串中的第一个唯一字符+字符串相加——“C++”“Leetcode每日一题”
标准库中的string类(中)+仅仅反转字母+字符串中的第一个唯一字符+字符串相加——“C++”“Leetcode每日一题”
|
6天前
|
JavaScript
js 字符串String转对象Object
该代码示例展示了如何将一个以逗号分隔的字符串(`&#39;1.2,2,3,4,5&#39;`)转换为对象数组。通过使用`split(&#39;,&#39;)`分割字符串并`map(parseFloat)`处理每个元素,将字符串转换成浮点数数组,最终得到一个对象数组,其类型为`object`。
|
6天前
|
Python
Python中的字符串(String)
【4月更文挑战第6天】Python字符串是不可变的文本数据类型,可使用单引号或双引号创建。支持连接(+)、复制(*)、长度(len())、查找(find()、index()、in)、替换(replace())、分割(split())、大小写转换(lower()、upper())和去除空白(strip()等)操作。字符串可格式化,通过%操作符、`str.format()`或f-string(Python 3.6+)。字符串以Unicode编码,作为对象拥有属性和方法。熟悉这些操作对处理文本数据至关重要。
40 6
Python中的字符串(String)
|
6天前
|
XML 编解码 数据格式
Python标准数据类型-String(字符串)
Python标准数据类型-String(字符串)
27 2