从字符串到常量池,一文看懂String类(1)

简介: 从字符串到常量池,一文看懂String类(1)

从字符串到常量池,一文看懂String类设计


从一道面试题开始


看到这个标题,你肯定以为我又要讲这道面试题了

//  这行代码创建了几个对象?
String s3 = new String("1");

这道题就算你没做过也肯定看到,总所周知,它创建了两个对象,一个位于堆上,一个位于常量池中。


这个答案粗看起来是没有任何问题的,但是仔细思考确经不起推敲。

如果你觉得我说的不对的话,那么可以思考下面这两个问题


1.你说它创建了两个对象,那么这两个对象分别是怎样创建的呢?我们回顾下Java创建对象的方式,一共就这么几种


  • 使用new关键字创建对象
  • 使用反射创建对象(包括Class类的newInstance方法,以及Constructor类的newInstance方法)
  • 使用clone复制一个对象
  • 反序列化得到一个对象

你说它创建了两个对象,那你告诉我除了new出来那个对象外,另外一个对象怎么创建出来的?


2.堆跟常量池到底什么关系?不是说在JDK1.7之后(含1.7版本)常量池已经移到了堆中了吗?如果说常量池本身就位于堆中的话,那么这种一个对象在堆中,一个对象在常量池的说法还准确吗?


如果你也产生过这些疑问的话,那么请耐心看完这篇文章!要解释上面的问题首先我们得对常量池有个准确的认知。


常量池


通常来说,我们提到的常量池分为三种


  • class文件中的常量池
  • 运行时常量池
  • 字符串常量池


对于这三种常量池,我们需要搞懂下面几个问题?

  1. 这个常量池在哪里?
  2. 这个常量池用来干什么呢?
  3. 这三者有什么关系?


接下来,我们带着这些问题往下看


class文件中的常量池


位置在哪?



顾名思义,class文件中的常量池当然是位于class文件中,而class文件又是位于磁盘上。


用来干什么的?


在学习class文件中的常量池前,我们首选需要对class文件的结构有一定了解

Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文

件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数

据,没有空隙存在。


------------《深入理解Java虚拟机》


整个class文件的组成可以用下图来表示

微信图片_20221113182931.png

对本文而言,我们只关注其中的常量池部分,常量池可以理解为class文件中资源仓库,它是class文件结构中与其它项目关联最多的数据类型,主要用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。

字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。

符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。一般包括下面三类常量:


  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符


现在我们知道了class文件中常量池的作用:存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。很多时候知道了一个东西的概念并不能说你会了,对于程序员而言,如果你说你已经会了,那么最好的证明是你能够通过代码将其描述出来,所以,接下来,我想以一种直观的方式让大家感受到常量池的存在。通过分析一段简单代码的字节码,让大家能更好感知常量池的作用。


talk is cheap ,show me code


我们以下面这段代码为例,通过javap来查看class文件中的具体内容,代码如下:


/**
 * @author 程序员DMZ
 * @Date Create in 22:59 2020/6/15
 * @公众号 微信搜索:程序员DMZ
 */
public class Main {
    public static void main(String[] args) {
        String name = "dmz";
    }
}

进入Main.java文件所在目录,执行命令:javac Main.java,那么此时会在当前目录下生成对应的Main.class文件。再执行命令:javap -v -c Main.class,此时会得到如下的解析后的字节码信息

public class com.dmz.jvm.Main
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
// 这里就是常量池了
Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
   #2 = String             #21            // dmz
   #3 = Class              #22            // com/dmz/jvm/Main
   #4 = Class              #23            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               LocalVariableTable
  #10 = Utf8               this
  #11 = Utf8               Lcom/dmz/jvm/Main;
  #12 = Utf8               main
  #13 = Utf8               ([Ljava/lang/String;)V
  #14 = Utf8               args
  #15 = Utf8               [Ljava/lang/String;
  #16 = Utf8               name
  #17 = Utf8               Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               Main.java
  #20 = NameAndType        #5:#6          // "<init>":()V
  #21 = Utf8               dmz
  #22 = Utf8               com/dmz/jvm/Main
  #23 = Utf8               java/lang/Object
 // 下面是方法表                           
{
  public com.dmz.jvm.Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/dmz/jvm/Main;
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         // 可以看到方法表中的指令引用了常量池中的常量,这也是为什么说常量池是资源仓库的原因
         // 因为它会被class文件中的其它结构引用         
         0: ldc           #2                  // String dmz
         2: astore_1
         3: return
      LineNumberTable:
        line 9: 0
        line 10: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  args   [Ljava/lang/String;
            3       1     1  name   Ljava/lang/String;
}
SourceFile: "Main.java"

在上面的字节码中,我们暂且关注常量池中的内容即可。主要看这两行

#2 = String             #14            // dmz
#14 = Utf8               dmz

如果要看懂这两行代码,我们需要对常量池中String类型常量的结构有一定了解,其结构如下:

image.png

对应到我们上面的字节码中,tag=String,index=#14,所以我们可以知道,#2是一个字面量为#14的字符串类型常量。而#14对应的字面量信息(一个Utf8类型的常量)就是dmz。


常量池作为资源仓库,最大的用处在于被class文件中的其它结构所引用,这个时候我们再将注意力放到main方法上来,对应的就是这三条指令

0: ldc           #2                  // String dmz
2: astore_1
3: return

ldc:这个指令的作用是将对应的常量的引用压入操作数栈,在执行ldc指令时会触发对它的符号引用进行解析,在上面例子中对应的符号引用就是#2,也就是常量池中的第二个元素(这里就能看出方法表中就引用了常量池中的资源)


astore_1:将操作数栈底元素弹出,存储到局部变量表中的1号元素

return:方法返回值为void,标志方法执行完成,将方法对应栈帧从栈中弹出


下面我用画图的方式来画出整个流程,主要分为四步


  1. 解析ldc指令的符号引用(#2)
  2. 将#2对应的常量的引用压入到操作数栈顶
  3. 将操作数栈的元素弹出并存储到局部变量表中
  4. 执行return指令,方法执行结束,弹出栈区该方法对应的栈帧


第一步:

微信图片_20221113183350.png

在解析#2这个符号引用时,会先到字符串常量池中查找是否存在对应字符串实例的引用,如果有的话,那么直接返回这个字符串实例的引用,如果没有的话,会创建一个字符串实例,那么将其添加到字符串常量池中(实际上是将其引用放入到一个哈希表中),之后再返回这个字符串实例对象的引用。


到这里也能回答我们之前提出的那个问题了,一个对象是new出来的,另外一个是在解析常量池的时候JVM自动创建的


第二步:

2020040700014127.png

将第一步得到的引用压入到操作数栈,此时这个字符串实例同时被操作数栈以及字符串常量池引用。

第三步:

2020040700014127.png

操作数栈中的引用弹出,并赋值给局部变量表中的1号位置元素,到这一步其实执行完了String name = "dmz"这行代码。此时局部变量表中储存着一个指向堆中字符串实例的引用,并且这个字符串实例同时也被字符串常量池引用。


第四步:

这一步我就不画图了,就是方法执行完成,栈帧弹出,非常简单。

在上文中,我多次提到了字符串常量池,它到底是个什么东西呢?我们还是分为两部分讨论

1.位置在哪?

2.用来干什么的?


目录
打赏
0
0
0
0
45
分享
相关文章
在Java中将String字符串转换为算术表达式并计算
具体的实现逻辑需要填写在 `Tokenizer`和 `ExpressionParser`类中,这里只提供了大概的框架。在实际实现时 `Tokenizer`应该提供分词逻辑,把输入的字符串转换成Token序列。而 `ExpressionParser`应当通过递归下降的方式依次解析
59 14
关于string的‘\0‘与string,vector构造特点,反迭代器与迭代器类等的讨论
你真的了解string的'\0'么?你知道创建一个string a("abcddddddddddddddddddddddddd", 16);这样的string对象要创建多少个对象么?你知道string与vector进行扩容时进行了怎么的操作么?你知道怎么求Vector 最大 最小值 索引 位置么?
37 0
鸿蒙开发:ArkTs字符串string
字符串类型是开发中非常重要的一个数据类型,除了上述的方法概述之外,还有String对象,正则等其他的用处,我们放到以后得篇章中讲述。
206 19
|
4月前
|
《从头开始学java,一天一个知识点》之:字符串处理:String类的核心API
🌱 **《字符串处理:String类的核心API》一分钟速通!** 本文快速介绍Java中String类的3个高频API:`substring`、`indexOf`和`split`,并通过代码示例展示其用法。重点提示:`substring`的结束索引不包含该位置,`split`支持正则表达式。进一步探讨了String不可变性的高效设计原理及企业级编码规范,如避免使用`new String()`、拼接时使用`StringBuilder`等。最后通过互动解密游戏帮助读者巩固知识。 (上一篇:《多维数组与常见操作》 | 下一篇预告:《输入与输出:Scanner与System类》)
118 11
课时45:String对象常量池
本次课程的主要讨论了对象池的概念及其在Java开发中的应用。首先,介绍了静态常量池和运行时常量池的区别。讨论了静态常量池和运行时常量池在实际开发中的作用,以及如何理解和应用这些概念。 1.常量池的分类 2.静态常量池和运行时常量池的区别
课时44:String类对象两种实例化方式比较
本次课程的主要讨论了两种处理模式在Java程序中的应用,直接赋值和构造方法实例化。此外,还讨论了字符串池的概念,指出在Java程序的底层,DOM提供了专门的字符串池,用于存储和查找字符串。 1.直接赋值的对象化模式 2.字符串池的概念 3.构造方法实例化
|
4月前
|
课时14:Java数据类型划分(初见String类)
课时14介绍Java数据类型,重点初见String类。通过三个范例讲解:观察String型变量、&quot;+&quot;操作符的使用问题及转义字符的应用。String不是基本数据类型而是引用类型,但使用方式类似基本类型。课程涵盖字符串连接、数学运算与字符串混合使用时的注意事项以及常用转义字符的用法。
113 9
课时16:String字符串
课时16介绍了Java中的String字符串。在Java中,字符串使用`String`类表示,并用双引号定义。例如:`String str = &quot;Hello world!&quot;;`。字符串支持使用“+”进行连接操作,如`str += &quot;world&quot;;`。需要注意的是,当“+”用于字符串与其他数据类型时,其他类型会先转换为字符串再进行连接。此外,字符串中可以使用转义字符(如`\t`、`\n`)进行特殊字符的处理。掌握这些基本概念对Java编程至关重要。
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
244 2
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问