Java-String、StringBuilder、Java的常量池及字节码文件分析案例

文章目录

  • 一、概述
  • 二、String类
    • 1.创建String类对象
    • 2.常量池
      • ①Java常量池
        • 1)内存分布
        • 2)字节码文件分析
          • 声明
          • 类的概述
          • 常量池
        • 3)类加载器函数体
      • ②字符串常量池
        • 概述
        • 案例
    • 3.常用方法
    • 4.String拼接的底层原理
      • ①无变量拼接
      • ②有变量拼接
    • 5.源码分析
  • 三、StringBuilder
    • 1.概述
    • 2.StringBuilder提高效率原理
    • 3.常用方法
    • 4.源码分析


一、概述

java.lang.String类代表字符串,Java程序中所有字符串文字都为此类的对象(这是Java的核心包,故而,在使用时并不需要导入),如:

String name="Mike";

注,字符串的内容是不会发生改变的(后面会分析),它在创建后就不能被更改,譬如,两个字符串进行拼接并不会影响到原字符串,而是产生一个新的字符串对象。
以下的内容并不是改变了原name变量中的字符串,而是创建了新的字符串,并将新字符串中的内容赋值给了name:

String name="Bob";
name="Mike";

此过程产生了两个字符串。


二、String类

字符串广泛应用在Java编程中,在Java中字符串属于对象,Java提供了String类来创建和操作字符串。
String类并非基本数据类型,而是一种引用数据类型。

1.创建String类对象

Java-String、StringBuilder、Java的常量池及字节码文件分析案例_第1张图片
创建String类对象主要有两种方式,分别是直接赋值和使用new来创建,以下是常用的创建方法:
Java-String、StringBuilder、Java的常量池及字节码文件分析案例_第2张图片

package com.example.service;

public class App2 {
    public static void main(String[] args){
        //1.使用直接赋值法进行创建
        String s1="abc";
        //2.使用new方法来获取字符串对象
        //①空白构造:可以获取一个空白的字符串对象
        String s2=new String();
        //②传递一个字符串,根据传递的字符串内容再创建一个新的字符串对象
        String s3=new String("abc");
        //3.传递一个字符数组,根据字符数组创建字符串(字符串的内容是不可改变的,可以通过字符数字来改变)
        char []chars={'a','b','c'};
        String s4=new String(chars);
        //4.可传递一个字节数组(byte)来创建字符串对象(网络中传输的都是字节信息,一般转化为字符串来获取)
        byte []bytes={97,98,99};
        String s5=new String(bytes);
        //得到的仍是"abc"
    }
}

2.常量池

此小结参考以下文章:
https://blog.csdn.net/qq_45737068/article/details/107149922
https://blog.csdn.net/Prior_SX/article/details/123463430
https://blog.csdn.net/Prior_SX/article/details/123510000
https://blog.csdn.net/m764395448/article/details/109407220

①Java常量池

1)内存分布

Java-String、StringBuilder、Java的常量池及字节码文件分析案例_第3张图片

  • 寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制;
  • 堆:存放所有new出来的对象;
  • 栈:存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new出来的对象)或者常量池中(对象可能在常量池里)(字符串常量对象存放在常量池中。);
  • 静态域:存放静态成员(static定义的);
  • 常量池:存放字符串常量对象和基本类型常量(public static final)。有时,在嵌入式系统中,常量本身会和其他部分分割离开(由于版权等其他原因),所以在这种情况下,可以选择将其放在ROM中
  • 非RAM存储:硬盘等永久存储空间。
2)字节码文件分析

什么是字节码文件,在本专栏的《Java基础》已写出,此处不再赘述。
字节码文件中的一些标记:

  • #:后接索引,索引从1开始。
  • =:后接常量的类型,省略前缀CONSTANT_和_info。

Java程序ConstantsTest.java

public class ConstantsTest{
	private static Integer a=10;
	private int b;
	private String c="cc";
	private static String d="dd";
	public int getB(){
		return b;
	}
	public static int getA(){
		return a;
	}
	public static void main(String[] args){
		ConstantsTest constantsTest=new ConstantsTest();
		constantsTest.getB();
		constantsTest.getA();
	}
}

使用javac命令进行编译,并使用javap -v命令操作.class文件得到:

PS C:\Users\lenovo\Desktop> javap -v ConstantsTest
Classfile /C:/Users/lenovo/Desktop/ConstantsTest.class
  Last modified 2023111; size 752 bytes
  SHA-256 checksum 96e7f17dae75ad9d1d1c20ab45882428f50701a5fefb73bbe2ebae5558ad8e08
  Compiled from "ConstantsTest.java"
public class ConstantsTest
  minor version: 0
  major version: 62
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #10                         // ConstantsTest
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 4, methods: 5, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = String             #8             // cc
   #8 = Utf8               cc
   #9 = Fieldref           #10.#11        // ConstantsTest.c:Ljava/lang/String;
  #10 = Class              #12            // ConstantsTest
  #11 = NameAndType        #13:#14        // c:Ljava/lang/String;
  #12 = Utf8               ConstantsTest
  #13 = Utf8               c
  #14 = Utf8               Ljava/lang/String;
  #15 = Fieldref           #10.#16        // ConstantsTest.b:I
  #16 = NameAndType        #17:#18        // b:I
  #17 = Utf8               b
  #18 = Utf8               I
  #19 = Fieldref           #10.#20        // ConstantsTest.a:Ljava/lang/Integer;
  #20 = NameAndType        #21:#22        // a:Ljava/lang/Integer;
  #21 = Utf8               a
  #22 = Utf8               Ljava/lang/Integer;
  #23 = Methodref          #24.#25        // java/lang/Integer.intValue:()I
  #24 = Class              #26            // java/lang/Integer
  #25 = NameAndType        #27:#28        // intValue:()I
  #26 = Utf8               java/lang/Integer
  #27 = Utf8               intValue
  #28 = Utf8               ()I
  #29 = Methodref          #10.#3         // ConstantsTest."":()V
  #30 = Methodref          #10.#31        // ConstantsTest.getB:()I
  #31 = NameAndType        #32:#28        // getB:()I
  #32 = Utf8               getB
  #33 = Methodref          #10.#34        // ConstantsTest.getA:()I
  #34 = NameAndType        #35:#28        // getA:()I
  #35 = Utf8               getA
  #36 = Methodref          #24.#37        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  #37 = NameAndType        #38:#39        // valueOf:(I)Ljava/lang/Integer;
  #38 = Utf8               valueOf
  #39 = Utf8               (I)Ljava/lang/Integer;
  #40 = String             #41            // dd
  #41 = Utf8               dd
  #42 = Fieldref           #10.#43        // ConstantsTest.d:Ljava/lang/String;
  #43 = NameAndType        #44:#14        // d:Ljava/lang/String;
  #44 = Utf8               d
  #45 = Utf8               Code
  #46 = Utf8               LineNumberTable
  #47 = Utf8               main
  #48 = Utf8               ([Ljava/lang/String;)V
  #49 = Utf8               <clinit>
  #50 = Utf8               SourceFile
  #51 = Utf8               ConstantsTest.java
{
  public ConstantsTest();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: aload_0
         5: ldc           #7                  // String cc
         7: putfield      #9                  // Field c:Ljava/lang/String;
        10: return
      LineNumberTable:
        line 1: 0
        line 4: 4

  public int getB();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #15                 // Field b:I
         4: ireturn
      LineNumberTable:
        line 7: 0

  public static int getA();
    descriptor: ()I
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #19                 // Field a:Ljava/lang/Integer;
         3: invokevirtual #23                 // Method java/lang/Integer.intValue:()I
         6: ireturn
      LineNumberTable:
        line 10: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #10                 // class ConstantsTest
         3: dup
         4: invokespecial #29                 // Method "":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #30                 // Method getB:()I
        12: pop
        13: aload_1
        14: pop
        15: invokestatic  #33                 // Method getA:()I
        18: pop
        19: return
      LineNumberTable:
        line 13: 0
        line 14: 8
        line 15: 13
        line 16: 19

  static {};
    descriptor: ()V
    flags: (0x0008) ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        10
         2: invokestatic  #36                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         5: putstatic     #19                 // Field a:Ljava/lang/Integer;
         8: ldc           #40                 // String dd
        10: putstatic     #42                 // Field d:Ljava/lang/String;
        13: return
      LineNumberTable:
        line 2: 0
        line 5: 8
}
SourceFile: "ConstantsTest.java"
声明
Classfile /C:/Users/lenovo/Desktop/ConstantsTest.class
//表示字节码文件的位置
  Last modified 2023111; size 752 bytes
  //表示当前字节码文件的修改日期、文件大小
  SHA-256 checksum 96e7f17dae75ad9d1d1c20ab45882428f50701a5fefb73bbe2ebae5558ad8e08
  //字节码文件的SHA-256值
  Compiled from "ConstantsTest.java"
  //表示当前字节码文件编译于ConstantsTest.java文件
类的概述
public class ConstantsTest
  minor version: 0
  major version: 62
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #10                         // ConstantsTest
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 4, methods: 5, attributes: 1
  • public class ConstantsTest:声明类的全类名(这里是在Desktop上,就没有很明显)、权限修饰符。
  • minor version、major version:分别表示Java的版本,minor version表示次版本,major version表示主版本,它们与JDK有着对应的关系。
  • flags:表示的是标志,而(0x0021)是所有标志的按位或(我没算过,但推测不同组合的和是不同的),本程序的字节码文件中为ACC_PUBLIC(0x0001)、ACC_SUPER(0x0020),有如下对应关系:
    • ACC_PUBLIC:0x0001,public类型。
    • ACC_FINAL:0x0010,final类型。
    • ACC_SUPER:0x0020,允许使用invokespecial字节码指令,默认为true。
    • ACC_INTERFACE:0x0200,接口类型。
    • ACC_ABSTRACT:0x0400,abstract类型。
    • ACC_SYNTHETIC:0x1000,表示这个类并非用户代码产生。
    • ACC_ANNOTATION:0x2000,注解类型。
    • ACC_ENUM:0x4000,枚举类型。
    • ACC_STRICT:0x0800,限制浮点计算以确保可移植性。
    • ACC_VARARGS:0x0080,是否可接受可变参数。
  • this_class:当前类的索引,指向的常量池中下标为10的常量,其值为Class表示是类类型,且其指向#12,表示类名为ConstantsTest。
  • super_class:当前类父类的索引,可见其默认父类为Object。
  • interfaces: 0, fields: 4, methods: 5, attributes: 1,表示本类并未继承接口,4个字段,5个方法(编写的三个方法+本类默认构造方法+write()方法),1个属性(仅有一个属性是SourceFile(源文件名称),就在整个字节码文件的最后一行)
常量池

当Java文件被编译为class文件后,就会产生class常量池,与JVM无关。
常量池主要存放两大类常量:字面量、符号引用 :

  • 字面量:Integer类型字面量、Float类型字面量、Long类型字面量、Double类型字面量、String类型字面量、UTF-8类型字面量。
  • 符号引用主要包含三类:
    • 类和接口的全名
    • 字段的名称和描述符
    • 方法的名称和描述符

符号引用:符号引用以一组符号来描述所引用的目标,在编译的时候一个每个java类都会被编译成一个class文件, 但在编译的时候虚拟机并不知道所引用类的地址,多以就用符号引用来代替 ,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段(具体可见《操作系统》)。

Java 虚拟机是在加载字节码文件的时候才进行的动态链接(见《操作系统》),也就是说,字段和方法的符号引用只有经过运行期转换后才能获得真正的内存地址。当 Java 虚拟机运行时,需要从常量池获取对应的符号引用,然后在类创建或者运行时解析并翻译到具体的内存地址上。

以上类型在.class文件中的表述为:

类型 标志 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整型字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndTyep_info 12 字段或方法的部分符号引用
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = String             #8             // cc
   #8 = Utf8               cc
   #9 = Fieldref           #10.#11        // ConstantsTest.c:Ljava/lang/String;
  #10 = Class              #12            // ConstantsTest
  #11 = NameAndType        #13:#14        // c:Ljava/lang/String;
  #12 = Utf8               ConstantsTest
  #13 = Utf8               c
  #14 = Utf8               Ljava/lang/String;
  #15 = Fieldref           #10.#16        // ConstantsTest.b:I
  #16 = NameAndType        #17:#18        // b:I
  #17 = Utf8               b
  #18 = Utf8               I
  #19 = Fieldref           #10.#20        // ConstantsTest.a:Ljava/lang/Integer;
  #20 = NameAndType        #21:#22        // a:Ljava/lang/Integer;
  #21 = Utf8               a
  #22 = Utf8               Ljava/lang/Integer;
  #23 = Methodref          #24.#25        // java/lang/Integer.intValue:()I
  #24 = Class              #26            // java/lang/Integer
  #25 = NameAndType        #27:#28        // intValue:()I
  #26 = Utf8               java/lang/Integer
  #27 = Utf8               intValue
  #28 = Utf8               ()I
  #29 = Methodref          #10.#3         // ConstantsTest."":()V
  #30 = Methodref          #10.#31        // ConstantsTest.getB:()I
  #31 = NameAndType        #32:#28        // getB:()I
  #32 = Utf8               getB
  #33 = Methodref          #10.#34        // ConstantsTest.getA:()I
  #34 = NameAndType        #35:#28        // getA:()I
  #35 = Utf8               getA
  #36 = Methodref          #24.#37        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  #37 = NameAndType        #38:#39        // valueOf:(I)Ljava/lang/Integer;
  #38 = Utf8               valueOf
  #39 = Utf8               (I)Ljava/lang/Integer;
  #40 = String             #41            // dd
  #41 = Utf8               dd
  #42 = Fieldref           #10.#43        // ConstantsTest.d:Ljava/lang/String;
  #43 = NameAndType        #44:#14        // d:Ljava/lang/String;
  #44 = Utf8               d
  #45 = Utf8               Code
  #46 = Utf8               LineNumberTable
  #47 = Utf8               main
  #48 = Utf8               ([Ljava/lang/String;)V
  #49 = Utf8               <clinit>
  #50 = Utf8               SourceFile
  #51 = Utf8               ConstantsTest.java

我们按照给出的类型分析几个(懒得全部分析,Integer的部分放在本专栏其他文章):
第一个常量

   #1 = Methodref          #2.#3          // java/lang/Object."":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V

Methodref表示第一个常量定义的是方法,且指向常量池的#2和#3,#2声明是类中的方法,其值#4表示来源于Object类(“=Utf8"表示是UTF-8编码的字符串),而#3表示是字段或方法的部分符号引用包含#5和#6,#5所代表的是方法的符号引用”<\init>“,表明为构造方法,而#6中的”()V"则声明方法的返回值是void。
总的来说,意思就是常量池第一个常量是ConstantsTest类所继承的父类Object的默认构造方法,方法返回值是void,方法名是"<\init>"
注,标识字符的关系如下:
Java-String、StringBuilder、Java的常量池及字节码文件分析案例_第4张图片
第二个常量

   #7 = String             #8             // cc
   #8 = Utf8               cc

表示当前常量是String字面量,值为"cc"。

第三个常量

  #9 = Fieldref           #10.#11        // ConstantsTest.c:Ljava/lang/String;
  #10 = Class              #12            // ConstantsTest
  #11 = NameAndType        #13:#14        // c:Ljava/lang/String;
  #12 = Utf8               ConstantsTest
  #13 = Utf8               c
  #14 = Utf8               Ljava/lang/String;```

Fieldref表示此字段是用来定义字段的,其指向的#10和#11分别表示字段所属的是ConstantsTest类、字段的名称和类别,其中#11所指向的#13和#14表示当前字段字段名为c,类型是java/lang/String类型对象。
总的来说,意思就是ConstantsTest类中声明了一个String类型的字段c,字段属于ConstantsTest类。
第四个常量

  #19 = Fieldref           #10.#20        // ConstantsTest.a:Ljava/lang/Integer;
  #20 = NameAndType        #21:#22        // a:Ljava/lang/Integer;
  #21 = Utf8               a
  #22 = Utf8               Ljava/lang/Integer;

分析同上,表示ConstantsTest类中声明了一个Integer类型的字段a。

第五个常量

  #23 = Methodref          #24.#25        // java/lang/Integer.intValue:()I
  #24 = Class              #26            // java/lang/Integer
  #25 = NameAndType        #27:#28        // intValue:()I
  #26 = Utf8               java/lang/Integer
  #27 = Utf8               intValue
  #28 = Utf8               ()I

此处涉及到Java中基本数据类型int和其包装类Integer的自动装箱,即,调用intValue方法,将Integer类型自动转化为int类型,这些内容,会在本专栏单独列出文章。
总的来说,意思就是本常量是属于ConstantsTest类中调用的一个方法,来源于java/lang/Integer类中,方法名为intValue,返回值是int基本数据类型。

第六个常量

#29 = Methodref          #10.#3         // ConstantsTest."":()V
	#10 = Class              #12            // ConstantsTest
	#3 = NameAndType        #5:#6          // "":()V
		#5 = Utf8               <init>
      	#6 = Utf8               ()V
        #12 = Utf8               ConstantsTest

总的来说,意思就是本常量是ConstantsTest类的默认构造方法。

第七、八、九个常量

  #30 = Methodref          #10.#31        // ConstantsTest.getB:()I
  #31 = NameAndType        #32:#28        // getB:()I
  #32 = Utf8               getB
  	  #28 = Utf8               ()I
  #33 = Methodref          #10.#34        // ConstantsTest.getA:()I
  #34 = NameAndType        #35:#28        // getA:()I
  #35 = Utf8               getA
  #36 = Methodref          #24.#37        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  #37 = NameAndType        #38:#39        // valueOf:(I)Ljava/lang/Integer;
  #38 = Utf8               valueOf
  #39 = Utf8               (I)Ljava/lang/Integer
  	#24 = Class              #26            // java/lang/Integer
  	#26 = Utf8               java/lang/Integer

分别表示ConstantsTest类中的getA()、getB()及java/lang/Integer类中的valueOf方法,后者用于Integer类和int基本数据类型的自动装/拆箱。
第十个常量

  #40 = String             #41            // dd
  #41 = Utf8               dd

表示String类型的字面量"dd"。

第十一个常量

  #42 = Fieldref           #10.#43        // ConstantsTest.d:Ljava/lang/String;
  #43 = NameAndType        #44:#14        // d:Ljava/lang/String;
  #44 = Utf8               d

表示字段d,其是String类型。
其他常量

  #45 = Utf8               Code
  #46 = Utf8               LineNumberTable
  #47 = Utf8               main
  #48 = Utf8               ([Ljava/lang/String;)V
  #49 = Utf8               <clinit>
  #50 = Utf8               SourceFile
  #51 = Utf8               ConstantsTest.java
  • Code:标识符,声明代码段。

  • LineNumberTable:描述源码行号与字节码行号(字节码偏移量)之间的对应关系。

  • main:声明为主方法,是程序的入口。

  • ([Ljava/lang/String;)V:字段描述符,是一种对函数返回值和参数的编码,表示的是其代表的是void function(String param)方法,其中,Ljava/lang/String表示的就是String param参数的声明。
    Java-String、StringBuilder、Java的常量池及字节码文件分析案例_第5张图片

  • < clinit >:用于类加载(加载->链接->初始化,关于类加载可见本专栏《Java基础》),是由JVM生成的初始化方法,就是所谓的类构造器,类的初始化也就是它的执行过程。

    • 类加载器的函数体是由编译器自动收集编译器中的所有static成员的操作组成,它们会被类加载器优先执行,而非静态部分则被包含在静态部分的代码当中,只有程序运行时才会动态生成。事实上,本程序中它的函数体就是字节码下面用花括号"{}"中的内容。
    • 而< init >,则是构造方法,用于类对象的初始化,二者一个是在类加载时执行,一个是在实例初始化时执行,骑着用于执行静态变量和静态代码块,后者用于执行实例变量和实例代码块。
  • sourceFile:标识符,声明源文件。

  • ConstantsTest.java:代表源文件名称。

3)类加载器函数体
{
  public ConstantsTest();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: aload_0
         5: ldc           #7                  // String cc
         7: putfield      #9                  // Field c:Ljava/lang/String;
        10: return
      LineNumberTable:
        line 1: 0
        line 4: 4

  public int getB();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #15                 // Field b:I
         4: ireturn
      LineNumberTable:
        line 7: 0

  public static int getA();
    descriptor: ()I
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #19                 // Field a:Ljava/lang/Integer;
         3: invokevirtual #23                 // Method java/lang/Integer.intValue:()I
         6: ireturn
      LineNumberTable:
        line 10: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #10                 // class ConstantsTest
         3: dup
         4: invokespecial #29                 // Method "":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #30                 // Method getB:()I
        12: pop
        13: aload_1
        14: pop
        15: invokestatic  #33                 // Method getA:()I
        18: pop
        19: return
      LineNumberTable:
        line 13: 0
        line 14: 8
        line 15: 13
        line 16: 19

  static {};
    descriptor: ()V
    flags: (0x0008) ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        10
         2: invokestatic  #36                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         5: putstatic     #19                 // Field a:Ljava/lang/Integer;
         8: ldc           #40                 // String dd
        10: putstatic     #42                 // Field d:Ljava/lang/String;
        13: return
      LineNumberTable:
        line 2: 0
        line 5: 8
}

具体基本上都能看懂,其中的一些是汇编语言?反正是更底层的语言,不再具体分析,唯一需要注意的是,我们从第一个函数体(ConstantsTest自己的无参构造方法)中可以看到先执行的是父类Object的无参构造方法,也恰恰说明了创建一个实例时会先创建其父类对象,完成父类的初始化(即为,super())。
然后就是一些基本属性:

  • stack:最大操作栈数,JVM会根据这个值来分配栈帧及栈深度。
  • locals:表示局部变量所需要的存储空间,单位为槽(slot),方法的参数变量和局部变量都在其中。
  • args_size:方法的参数个数。
    注意,只要不是static函数,局部变量都会包含一个隐藏的this变量,故而locals通常比表面上的过一个。
  • LocalVariableTable,该属性的作用是描述帧栈中的局部变量与源码中定义的变量之间的关系。可以通过这个来找到this(本例中全是static方法,算是一个败笔)。

②字符串常量池

概述

实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。在JDK7.0中,StringTable的长度可以通过参数指定:

-XX:StringTableSize=66666

全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。且,只有直接赋值、String的intern方法产生的字符串才会被存入常量池当中,而通过new得到的字符串并不会存放在常量池当中,而常量池处于堆内存当中。

案例

①使用直接赋值语句
Java-String、StringBuilder、Java的常量池及字节码文件分析案例_第6张图片
程序开始执行,main进入栈内存,执行第一条赋值语句(只要是赋值语句就都会去查看),此时,会查看StringTable(串池)当中是否有"abc",因为没有,故而创建,并将地址值传递给s1,然后再执行第二条赋值语句。第二条赋值语句进入串池进行查找并找到,就不会创建新的String对象,而是复用原先的"abc"对象。
即,当使用直接赋值时,系统会检查该字符串在串池中是否存在,若不存在,则创建新的String对象,若存在则会复用。
②使用new方法创建
Java-String、StringBuilder、Java的常量池及字节码文件分析案例_第7张图片
代码测试

package org.example.SimpleCode;

import org.junit.Test;

public class Demo {
    @Test
    public void test1() {
        String s1="abc";
        String s2="abc";
        String s3=new String("abc");
        String s4=new String("abc");
        System.out.println("s1与s2均为常量池中的对象地址,故,结果为:"+(s1==s2));
        System.out.println("s1为常量池中的对象地址,而s3为记录堆里的地址,故,结果为:"+(s1==s3));
        System.out.println("s3与s4均为记录堆中的对象地址,故,结果为:"+(s3==s4));
    }
}

运行结果:

s1与s2均为常量池中的对象地址,,结果为:true
s1为常量池中的对象地址,而s3为记录堆里的地址,,结果为:false
s3与s4均为记录堆中的对象地址,,结果为:false

事实上,通过字节码文件也可看出不同,这里不再赘述。

3.常用方法

equals方法与"=="的区别:
equals方法继承于父类Object,在Object当中的源码为:

    public boolean equals(Object obj) {
        return (this == obj);
    }

由前置知识可知,在Java中,对于引用数据类型变量而言,其存储的是对象的引用值,而this存储的是当前对象的引用值,故在Object中的equals方法是对引用值进行比较。
而在String类中进行了重载:

    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        return (anObject instanceof String aString)
                && (!COMPACT_STRINGS || this.coder == aString.coder)
                && StringLatin1.equals(value, aString.value);
    }

表示当引用相同时返回true,或是直接对引用数据类型所指向的String类型字面量进行比较,若相等则返回true。
本方法的意义在于,由于Java中的 " == " 在比较引用数据类型时比较的是对象的引用,使得当两个String类型的变量在比较时,即使所指向的String字面量是相等的,也可能因为引用不同而返回false,这在使用直接赋值和new创建的字符串在比较时经常发生。
常见方法:

  • char charAt(int index) 返回指定位置的字符
  • int compareTo(String anotherString) 比较两个字符串。相等返回0;前大后小返回1;前小后大返回-1
  • boolean contains(CharSequence s) 判断字符串是否包含s
  • boolean endsWith(String suffix) 判断字符串是否以suffix结尾
  • boolean equals(Object anObject) 判断两个串是否相等
  • boolean equalsIgnoreCase(String anotherString) 忽略大小写判断两个串是否相等
  • byte[] getBytes() 将字符串串变成字节数组返回
  • int indexOf(String str) 返回str在字符串第一次出现的位置
  • boolean isEmpty() 字符串是否为空
  • int length() 字符串长度
  • int lastIndexOf(String str) 返回str最后一次出现的位置
  • String replace(CharSequence target, CharSequence replacement) 用replacement替换字符串target的字符
  • String[] split(String regex) 将字符串以regex分割
  • boolean startsWith(String prefix) 判断字符串是否以prefix开始
  • String substring(int beginIndex) 从beginIndex开始截取字串
  • String substring(int beginIndex, int endIndex) 截取beginIndex到endIndex - 1的字符串
  • char[] toCharArray() 将字符串转换乘char数组
  • String toLowerCase() 字符串转小写
  • String toUpperCase() 字符串转大写
  • String trim() 去除字符串两边空格
  • static String valueOf(int i) 将 i 转换成字符串

4.String拼接的底层原理

①无变量拼接

当拼接的时候没有参与时,如:

public class StringTest{
    public static void main(String[] args){
        String a="a"+"b"+"c";
    }
}

查看字节码文件:

   #7 = String             #8             // abc
   #8 = Utf8               abc

可见,此时的直接赋值操作的常量池中只有最后的"abc"串,而无"a"、“b”、“c”,事实上,此处触发了字符串的优化机制,使得在编译时就已经是最终的结果了。使用以下代码同理:

    @Test
    public void test1() {
        String Str1="a"+"b"+"c";
        String Str2="abc";
        System.out.println("Str1==Str2的结果为"+(Str1==Str2));
    }

输出:

Str1==Str2的结果为true

进程已结束,退出代码0

②有变量拼接

package com.example.service;
public class App2 {
    public static void main(String[] args){
            String s1="a";
            String s2=s1+"b";
            String s3=s2+"c";
            System.out.println(s3);
    }
}

JDK8以前底层会使用StringBuilder,其内存图为:
Java-String、StringBuilder、Java的常量池及字节码文件分析案例_第8张图片
s2拼接的实质是底层创建了StringBuilder对象,并执行了append方法和toString方法。
查看源码:
Java-String、StringBuilder、Java的常量池及字节码文件分析案例_第9张图片
故,至少产生了一个StringBuilder对象和一个String对象,二者均处于堆内存当中,这也是"+"进行字符串拼接时效率不高的原因。
JDK8之后对以上的拼接方式进行了优化,例:

package com.example.service;

public class App2 {
    public static void main(String[] args){
        String s1="a";
        String s2="b";
        String s3="c";
        String s4=s1+s2+s3;
        System.out.println(s3);
    }
}

在执行s4的操作时,底层会对最终的字符串大小进行预估而创建字符串数组,并将字符串依次填入,最后再合并(若是JDK7版本,则最少需要创建4个对象),但是,即使是这样也是浪费内存且效率不高。
结论:当有很多字符串变量拼接时,不要直接使用"+",因为会在底层创建多个对象,浪费时间,浪费性能,建议使用StringBuilder、StringBuffer。

5.源码分析

算是留一个坑,考研+技术同时抓的话还是要以推进度为主,这里的源码分析在我完成后端技术的复习之后再来填这个坑。


三、StringBuilder

1.概述

对于String类,它是一个不可变类,即,一旦一个String对象被创建后,包含在这个对象中的字符序列是不可变的,只能改变变量存储的引用来指向其他String对象,这是因为String对象的底层通过字符数组来存储字符串序列,而这个数组通过final修饰,是不可变的:

    @Stable
    private final byte[] value;

StringBuilder类不同,它继承于AbstractStringBuilder抽象类,而这个抽象类使用:

    byte[] value;

来存储字符串,故而,StringBuilder对象代表的是一个字符序列可变的字符串对象,创建该对象后,可通过其各种方法直接在这个字符数组上进行操作。例:

    @Test
    public void test1() {
        StringBuilder stringBuilder1=new StringBuilder("aaa");
        StringBuilder stringBuilder2=stringBuilder1;
        stringBuilder1.append("bbb");
        System.out.println("stringBuilder1==stringBuilder2的结果为:"+(stringBuilder1==stringBuilder2));
        String string1="aaa";
        String string2=string1;
        string1+="bbb";
        System.out.println("string1==string2的结果为:"+(string1==string2));
    }

输出结果为:

stringBuilder1==stringBuilder2的结果为:true
string1==string2的结果为:false

进程已结束,退出代码0

因为String类对象引用指向的字符串量并不可变,事实上,string1执行完与"bbb"的操作后是指向了一个新的String对象,而StringBuilder是对对象自身进行操作,故而引用不变。

2.StringBuilder提高效率原理

public class Test{
    public static void main(String[] args){
        StringBuilder sb=new StringBuilder();
        sb.append("a");
        sb.append("b");
        sb.append("c");
    }
}

Java-String、StringBuilder、Java的常量池及字节码文件分析案例_第10张图片
StringBuilder是一个内容可变的容器,故而效率很高。

3.常用方法

①构造方法
常规创建

StringBuilder sb=new StringBuilder();

在创建时添加初始化字符串

StringBuilder sb=new StringBuilder("abc");

在创建时添加初始化长度

StringBuilder sb=new StringBuilder(初始长度);

②常规方法
1.builder.append()
作用:追加数据

		builder.append("just");

在加入新字符串时,不会在内存中新开辟字符串空间,只是给原有的字符串尾部加入新字符串

2.builder.insert()
作用:向指定位置插入数据

builder.insert(0, "you");

每次加入新字符串之后都会改变字符串中每个字符的地址,插入后原始指定位置的数据向后移
3.builder.deleteCharAt()
作用:删除指定位置的数据

builder.deleteCharAt(index);

4.builder.delete( )
作用:删除指定范围的数据左闭右开

builder.delete(beginIndex, endIndex);

范围:从开始位置到结束位置的前一个
5.builder.toString()
作用:将对象中的数据以字符串的形式返回

builder.toString();

6.builder.reverse()
作用:将对象中的数据反转

builder.reverse();

4.源码分析

算是留一个坑,考研+技术同时抓的话还是要以推进度为主,这里的源码分析在我完成后端技术的复习之后再来填这个坑。

你可能感兴趣的:(后端开发,java,jvm,开发语言)