• Java杂谈4——Java中的字符串存储


    Java中的String

      Java.Lang.String是Java语言自带的字符串实现,它并不是java的基本类型,但却和几乎每个java程序都密切相关的一个基础java类。

      string类内部实际实现存储的字符数组在定义时用关键字final修饰,意味着这个属性是一个常量,在初始化之后就不能再被修改。这也同时表明所有对String对象的修改操作(包括append,substring,concat,replace,trim等),在具体实现中返回的都是一个全新的string对象副本。总结来说,Java中的String具有不变性、不可继承性

    讨论String对象在内存中的存储

      这里会涉及到三个概念:虚拟机栈、Java堆和运行时常量池,在我的第一篇文章中都有描述。根据JDK源码中的规范,String类的使用方式有如下几种:

        String str1 = new String("abc");
    
        String str2 = "abc";
    
        String str3 = "ab" + new String("c");

      在具体应用中,这里的几种String对象的创建方式是基本没有区别的。但实质上这里有一些微小的差异:第一个字符串的创建方式str1指向的对象被分配到了Java堆中,且创建的时机是在程序运行时。str2指向的字符串对象在编译期就已经确定,存放在运行时常量池中。str3的创建是一个比较复杂的过程,java虚拟机会重新组织对应的字节码,具体的过程在下文会分析。

      在学习的过程中,我也参考了很多前辈的文章,其中包括这篇《Java内存分配和String类型的深度解析》,文中作者在String的定义方法中提出了针对性的几点疑问,结合我自身的思考,我来尝试解答一下。

    • 堆中new出来的实例和常量池中的是什么关系?

      两者都是一个String类型的实例化对象,即使通过方法equals比较返回结果为true,两者在本质上都不会是同一个对象。

    • 常量池中的字符串常量与堆中的String对象有什么区别?

      一个最主要的区别就是内存中的位置不同,当然大部分的常量池中的字符串常量是在编译器确定的,除非很明确的调用string对象的intern方法返回(或创建)一个运行时常量池中的string对象,所有通过new操作符创建的string对象都会被分配到Java堆中。

    • 为什么直接定义的字符串同样可以调用String对象的各种方法呢?

      虽然说字符串常量”abc”是在编译器被确定的字符串常量,被存放在运行时常量池,但是这个常量字符串还是一个String类型的对象(这一点确实有别于c/c++语言中的常量的概念,也看出在java的哲学中万物都是对象),如果是一个标准的java对象,它可以调用String的方法。

     字节码分析

      在分析问题的过程中,通过查看上文中提到的三行java代码对应的字节码(方法javap -c {具体要查看的*.class文件}),来确认我的自己的猜想,在这个过程中也可以看出java编译器对源代码的处置,具体的字节码分析如下:

           //0 ~ 9 对应的是第一行的java语句:String str1 = "abc";
           0: new           #21                 // class java/lang/String
           3: dup           
           /*
           **装载一个常量字符串 ,符号#23代表的字符串对象就是 “abc”,
           **常量字符串在程序运行之前就已经被创建
           */
           4: ldc           #23                 // String abc
           /*str1不指向常量字符串“abc”,
           **而是将这个常量字符串作为构造函数的实参传入
           **在java堆中重新创建了一个全新的对象
           */
           6: invokespecial #25                 // Method java/lang/String."<init>":(Ljava/lang/String;)V
           9: astore_1      
    
        //从这里开始到12 都是第二行的Java代码: String str2 = "abc";
          /*
          ** 直接调用运行时常量池中的对象
          */
          10: ldc           #23                 // String abc
          12: astore_2      
    
        //从此处开始到最后对应第三行的Java代码:String str3 = "ab" + new String("c");
          /*
          **对于这种new 对象与 常量字符串相结合的方式,
          **JAVA编译器在处理过程中创建了一个StringBuilder对象用于处理异构字符串的拼接工作
          ** "ab"对应另一个常量字符串 “c”则运行时动态创建的String对象
          **/
          13: new           #28                 // class java/lang/StringBuffer
          16: dup           
          17: ldc           #30                 // String ab
          19: invokespecial #32                 // Method java/lang/StringBuffer."<init>":(Ljava/lang/String;)V
          22: new           #21                 // class java/lang/String
          25: dup           
          26: ldc           #33                 // String c
          28: invokespecial #25                 // Method java/lang/String."<init>":(Ljava/lang/String;)V
          31: invokevirtual #35                 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
          34: invokevirtual #39                 // Method java/lang/StringBuffer.toString:()Ljava/lang/String;
          37: astore_3      
          38: return            

      由此可以看出,在处理不同方式构建的字符串拼接时,java编译器为我们付出了额外的一些代价,在我们的代码中,尽可能少出现类似第三行那样的代码。如果是确定的字符串常量,也尽可能写成”ab” + “c”这样的形式,在编译器优化时会在常量池中找到现成的对象对象,会在性能上有很大的提升。    

        同时我们能够看到编译器默认用的字符串拼接器是StringBuffuer类,通常情况下我们如果需要对几个动态的string对象做拼接用的都是StringBuilder类。StringBuffer与StringBuilder最本质的区别是:前者是线程安全的。那么如果只是明确的单线程环境下,在效率上编译器自补足的代码又会有更多性能上的欠缺。

  • 相关阅读:
    前台线程与后台线程,AfxGetApp>GetMainWnd()与AfxGetMainWnd的不同 (转)
    详细解说 STL 排序(Sort) (转)
    (转)怎样从一个DLL中导出一个C++类
    HTTP协议之状态码详解(转)
    关于列表(ListCtrl)控件的界面基础知识
    C++序列化(转)
    vs2008修改Menu(菜单)资源ID的方法。
    怎样解决VC中滚动条最大滚动值不能超过32767的问题
    < Photoshop CS 专栏 >颜色(二)
    C#使用BinaryReader类读取二进制文件
  • 原文地址:https://www.cnblogs.com/yahokuma/p/3677576.html
Copyright © 2020-2023  润新知