• String 的字面量、常量池、构造函数和intern()函数


    一、内存中的 String 对象

    Java 的堆和栈

    • 对于基本数据类型变量和对象的引用,也就是局部变量表属于栈内存
    • 而通过 new 关键字和 constructor 创建的对象存放在堆内存
    • 直接的 "hello" 被称为字面量形式,在JDK1.7之后存放在位于堆内存的独立的常量池中;
      // 比如说:
      String s1="hello";
      Scanner input = new Scanner();
      // 上面的语句中变量名 s1、input 存放在栈内存中,"hello" 为字面量,所以放在常量池,而用构造函数创建的对象放在堆内存中。
      
      

    什么是String常量池

    JVM为了减少字符串对象的重复创建,维护了一个特殊的内存,这段内存被称为字符串常量池或者字符串字面量池。我们所知道的几个String的特点都来源于此。

    • 在这个常量池中,共享所有的String对象 ,因此String对象不可被修改,因为一旦被修改那么同时引用此String对象的变量都会随之改变,所以被设计成不可修改的;
    • 也因此我们常常会听说String拼接字符串的性能较差;
    • 使用双引号声明的String对象会直接存储在常量池中,若已存在,则直接引用已存在的String对象;
    • 每个String对象都是唯一的,这样才能达到节约内存的目的;

    补充说明 “==” 和 “equals()”

    • 在基本数据类型中,只可以使用“ == ”,也就是比较他们的值是否相同;
    • 而对于对象(包括 String )来说,“ == ”表示比较地址是否相同,“ equals() ”才表示比较他们内容是否相同;
    • equals()是object都拥有的一个函数,本身就要求对内部值进行比较;

    二、String 的字面量和构造函数

    1. 两者的不同

    除了"1"这种字面量,还有一种就是使用构造函数 new String() 进行String对象的创建。

    而对于String str1 = "1";String str2 = new String("1");两个语句在执行时,内存中的操作是不同的。

    对于String str1 = "1";来说,和之前介绍的常量池一致,当语句执行时,JVM会首先检查常量池中是否存在该字面量:

    • 若存在,则直接返回此字面量的引用;
    • 若不存在,则在常量池中创建该字面量,返回其引用;

    对于String str2 = new String("1");来说,当语句执行时,JVM同样会先检查常量池中是否存在对应的字面量:

    • 若存在,则在堆中创建String对象,在对象内部引用该字面量;
    • 若不存在,则先在常量池中创建字面量,然后在堆中创建String对象,在对象内部引用该字面量;

    2. 初步结论

    • 无论任何时候,new String() 都会自己另行在堆中开辟空间,创建新的String对象;
    • 而假如常量池中不存在对应的字面量,new String() 会创建两个对象,一个放进常量池中,一个放进堆中;
    • 因为new String() 总是创建新的String 对象,所以当使用"=="将str21比较时,结果一定是false,因为两者的地址是不同的。

    intern()函数和新的问题

    intern()函数

    先介绍一个神奇的函数—— intern(),它是一个native方法,不妨来看一下这个函数的介绍

    1. 返回值是一个标准的字符串形式;
    • 返回值是与此对象具有相同内容的字符串,但保证来自字符串池;
    • 对于两个字符串s、t,当且仅当s.equals(t)为true时,才能说s.intern()==t.intern()为true;
    • 当此方法被调用时,如果常量池中已经包含了一个和该对象内容相同的字符串,那就返回这个字符串;若不包含,如果大家有查看其它资料,他们都会说不存在则新建,但事实上,在接下来的问题之前,根本没有不存在的情况,字面量总是存在的;

    新的问题

    这个函数有什么用,个人认为,可以粗率地认为这个函数可以找到所有的String object在常量池中对应的字面量(存在则返回引用,不存在则创建后返回引用)。但是不难想到,之前的初步结论已经得出new String("")会确保两个对象的存在,那么intern()函数的存在有什么意义呢?为了得到一个String对象中引用的的源对象?
    这时引入下面一段代码:

    String str1 = new String("1");
    System.out.println(str1 == str1.intern());
    System.out.println(str1 == "1");
    
    String str2 = new String("2") + new String("3");
    System.out.println(str2 == str2.intern());
    System.out.println(str2 == "23");
    
    String str4="45";
    String str3 = new String("4") + new String("5");
    System.out.println(str3==str3.intern());
    System.out.println(str3 == "45");
    

    运行结果:
    Java String 运行结果

    对于结果,相信str1的两个输出结果都是可以理解的,str1创建后产生两个对象,保存在堆的 str1 和常量池中的 "1" 地址显然不同,而intern() 则是返回的"1"的地址,所以输出均为false;

    而str2、str3、str4就变得诡异起来了,经过了字符串拼装之后,str2str2.intern()神奇的具有了相同的地址,但同时,因为一个str4,str3str3.intern()相同的地址又变的不同起来;

    所以新的问题就来源于字符串拼接,根据前文已经知道字符串是不可修改的,那么想要进行一次 String str2 = new String("2") + new String("3");这样的字符串拼接消耗就非常大了(相信大家都听过字符串拼接效率差的说法),所以JVM对其进行了优化,具体是如何优化的呢?

    分析 - intern()结论

    • 如果是String str2 = "2" + "3";,则直接将"2"和"3"折叠为"23",然后直接作为字面量放入常量池中,也就是和String str2 = "23";没区别,具体可见String a="a"+"b"+"c"在内存中创建几个对象? - 陈肖恩的回答 - 知乎
    • 如果是 String str2 = new String("2") + new String("3");这种情况,JVM同样会进行优化,目前根据我的调查,会被优化成三个对象的创建——在常量池中创建"2"、"3",在堆中创建内容为"23"的String对象,大家可能会奇怪,不在常量池创建"23"吗?目前看是不会的;
    • 之前我也说到intern() 根本没有不存在的情况,但眼下这种情况是真的不存在了,intern()采取了一种截然不同的处理方案——不是在常量池中建立字面量,而是直接将该对象自身的引用复制到常量池中,所以代码的第二段就不难解释了,此时堆中的str2才是真正的源字符串,而常量池也只是对它的引用。
    • 而使用intern() 场和也变得显而易见,当你需要进行大量可能会重复的字符串的拼接的时候,为了避免内存的浪费进而导致GC清理无用字符串降低性能,那就可以使用intern()了。

    三、其他 String 类相关结论

    构造函数结论

    不难看出,总是new String("")这样的函数在浪费内存,降低性能,所以大家在写程序的时候应该尽量直接使用字面量,而避免构造函数的使用。

    String 是否为空的结论

    String 存在一个方法叫 str.isEmpty(),如果查看源代码就会发现和 str.length()==0 没有任何区别。

    public boolean isEmpty() { return value.length == 0;} //源代码
    
    // 何时出现此种情况:
    String s1 = new String(); 
    String s1 = new String("");
    String s1 = "";
    

    String 是否为null的结论

    null即未指定对象,如果直接使用会出现所谓的空指针错误。值得注意的是第二种情况,字符串数组在创建之后并不会像字符串新建时一样初始化为长度为1的字符串,而是空指针。

    // 何时出现此种情况:
    String s1 = null;
    String[] s1 = new String[n];
    

    String equals() 的结论

    所以根据 equals() 的定义就能发现,此无论任何情况下, equals() 总是比较两个字符串的内容,无论是否开辟内存或别的怎样,假如需求就是简单地进行字符串匹配,使用 equals() 总是没错的。

  • 相关阅读:
    Java实现 LeetCode 136 只出现一次的数字
    Java实现 LeetCode 136 只出现一次的数字
    Java实现 LeetCode 136 只出现一次的数字
    Java实现 LeetCode 135 分发糖果
    Java实现 LeetCode 135 分发糖果
    Java实现 LeetCode 135 分发糖果
    Java实现 LeetCode 134 加油站
    Java实现 LeetCode 134 加油站
    Java实现 LeetCode 134 加油站
    Java实现 LeetCode 133 克隆图
  • 原文地址:https://www.cnblogs.com/imzhizi/p/string-de-zi-mian-liang-chang-liang-chi-gou-zao-ha.html
Copyright © 2020-2023  润新知