深入理解系列之JAVA数据结构(5)——String

String类型是Java编程中最为常见的数据结构(没有之一),与之相关联的还有StringBuilder和StringBuffer。其中String类型是不可变的;后者均是可变的字符串,但是StringBuilder是线程不安全的,StringBuffer线程安全;所以三者的效率排名为:StringBuilder>String>StringBuffer。另外,为了优化字符串的使用,Java定义了两种字符串变量,一个是字符串常量,另一个就是字符串对象。

1、字符串的底层实现机制是什么?

不论是字符串常量还是字符串对象,其底层都是String类,而String类存储字符串的方式是通过char型数组存储的:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    /**
     * Class String is special cased within the Serialization Stream Protocol.
     *
     * A String instance is written into an ObjectOutputStream according to
     * <a href="{@docRoot}/../platform/serialization/spec/output.html">
     * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
     */
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];

					…………
}

从这里我们可以看到,value被声明了final不可变,所以正好证实了文章开头所说的String的不可变性!为了进一步证明我们可以用一个简单的例子来看一下:

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

通过debug模式可以发现,当声明好a,b后,实际数值存储的格式和位置如下所示:
这里写图片描述
即很明显的看到,此时a,b其实是包含一个char型数组的对象引用,该数组地址分别被标记为char[5]@457和char[5]@458,我们继续运行至结束:
这里写图片描述
我们可以发现此时a,b中维护的数组对象已经变更了地址,也就是说原地址的数据并没有改变,我们只是重新开辟了一个空间存储新的数值,不变的只是原来的引用罢了!为了探究底层代码到底是怎么实现的,我们对生成的class文件执行:

javap -c StringTest

生成以下字节码文件:

public class StringTest {
  public StringTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/String
       3: dup
       4: ldc           #3                  // String hello
       6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
       9: astore_1
      10: ldc           #5                  // String nihao
      12: astore_2
      13: new           #6                  // class java/lang/StringBuilder
      16: dup
      17: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
      20: aload_1
      21: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      24: ldc           #9                  // String java
      26: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      29: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      32: astore_1
      33: new           #6                  // class java/lang/StringBuilder
      36: dup
      37: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
      40: aload_2
      41: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      44: ldc           #9                  // String java
      46: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      49: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      52: astore_2
      53: return
}

可以看到,不论是字符串常量还是字符串对象当改变存储的值时其实执行的是以下命令:(以字符串常量为例即13~29行,字符串对象同理)
1、第13行new指令,在Java堆上为StringBuilder对象申请内存;
2、第17行invokespecial指令,调用构造方法,初始化StringBuilder对象;
3、第21、26行invokespecial指令,调用append方法,添加原字符串和java字符串;
4、第21行invokespecial指令,调用toString方法,生成String对象。
如果你深入源码观察String.replace等“有修改功能的”方法,你会发现也是同样的道理:创建一个新对象并赋值给自身(如果是取子串的方法则直接使获取原字符串中数组的偏移地址)

2、String字符串常量和String对象的区别是什么?

上面讲到,String字符串常量和String字符串对象底层都是char型数组罢了,但是实际在内存存储还是有区别的!因为字符串是Java中用的最多的一种数据类型,所以JVM专门开辟一个常量池空间针对String类型的数据作了特殊优化:即如果是字符串常量形式的声明首先会查看常量池中是否存在这个常量,如果存在就不会创建新的对象,否则在在常量池中创建该字符串并创建引用,此后不论以此种方式创建多少个相同的字符串都是指向这一个地址的引用,而不再开辟新的地址空间放入相同的数据;但是字符串对象每次new都会在堆区形成一个新的内存区域并填充相应的字符串,不论堆区是否已经存在该字符串!
这里讲一下常量池概念,常量池包含两种:
一种是class文件中的静态常量池,它只是java源码编译后形成的一类数据,不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间,是存在硬盘中的数据类型的命名方式;
另一种是运行时常量池,即上文中一直提到的常量池,它是内存中存储一类数据的内存区域命名方式,那么常见的就是字符串常量池,专门为优化字符创常量而分配的一个空间。在JDK6时,常量池存在方法区的永久代中;JDK7、JDK8都把常量池转移到堆区了,而且到了JDK8的时候已经不存在永久代了,取而代之的是元空间的概念,但是元空间并不占用JVM内存而是直接共享系统内存,他本质上也是方法区的一种实现方式罢了

3、常见的字符串比较相等该如何理解?

笔试面试中问的最多的就是两个字符串是否相等的问题,这里通过例子来阐述一下:
场景1:

 String a = new String("helloJava");
 String b = "helloJava";

结论:
a !=b
解释:
这个上面已经说明,一个在堆区,一个在常量池,他们是不同的两个对象!

--------------------------------------------------------------------------------------------------------
场景2:

String b = "helloJava";
String c = "hello" + "Java";

结论:
b = c
解释
JVM在编译期间会自动把字符串常量相加操作计算完毕后再执行比较,所以计算完成后其实c=“helloJava”,因为字符串存在这样的常量,所以直接引用即可,使用的是同一内存地址的数据。

---------------------------------------------------------------------------------------------------------
场景3:

String b = "helloJava";
String d1 = "hello";
String d2 = "Java";
String d = d1 + d2;
String e = d1 + new String("Java");
String f = "hello" + new String("Java");

结论:
b!=d != e !=f
解释
字符串变量的连接动作,在编译阶段会被转化成StringBuilder的append操作,变量d最终指向Java堆上新建String对象,变量b指向常量池的”helloJava”字符串,所以 b != d,其实这也就是文章开头讲String不可变的机制,为什么JVM要这样处理?
这是因为d1和d2在编译阶段最终指向的对象是不可知的,所以不能当做常量来看待。但是如果添加final关键字的话,那么就是相等的了,因为final关键字强制性的使d1和d2无法再变更,所以可以当做常量看待!其他不等的情况同理

--------------------------------------------------------------------------------------------------------------
场景4:

    String a = new String("helloJava");
    String a1 = a.intern();
    System.out.println(a == a1);//false


    String b = new String("String")+new String("Tests");
    String b1 = b.intern();
    System.out.println(b == b1);//true

    String c = "helloWorld";
    String c1 = c.intern();
    System.out.println(c == c1);//true

结论
false true true
解释
解释这一现象首先得理解两点:
1、JDK7/8的常量池区域都放在了堆区(JDK6的情况就不考虑了)
2、intern()方法的作用在于:
·········**····>**首先查看常量池是否有字符串,如果有直接返回字符串的引用;否则在常量池添加该字符串在堆区的引用,并返回该引用;
对于第一个例子,执行第一句后,a指向是堆区字符串的引用,同时在常量池也创建了一份字符串,所以a1指向常量池字符串引用,所以引用地址不同自然不等;
对于第二个例子,只在堆区创建了字符串对象,常量池没有!所以执行intern()后实际在常量池中创建了一份该字符串在堆区的引用,b1实际指向的还是堆区的引用,所以相等!
对于第三个例子,通过前面的分析已经很简单了,直接返回常量池的引用就行了,所以是相等的!
这里对于第二个例子稍微做一下变形:

String b = new String("String")+new String("Tests");
b0 = "StringTests"//添加此句
String b1 = b.intern();
System.out.println(b == b1);//false

结论:
false
解释
由上述分析可知:如果常量池存在该字符串,则直接返回常量池的引用,这里在intern()之前在常量池中创建了该字符串,所以最终返回的不是堆区的引用,而是字符串的引用!
这里同样引用网上的一幅图来说明intern()方法的实现原理:
这里写图片描述
图中所述就是:常量池没有StringTest,则s1.intern()指向的是ref,而ref是执行intern()后在常量池中创建的指向堆区StringTest的引用副本,所以s1.intern() = ref = new String(“stringTest”);常量池中存在“java”,则直接返回常量池的引用即可!

最近手残,搞了个公众号,主要闲暇时间随便聊一些程序圈的一些事,也会分享一些技术面试的资料,感兴趣的可以关注一波。关注后,后台发送 面试指南,可以获取2021最新JAVA面试总结,基本看完后,JAVA八股文这些应该不在话下了。

在这里插入图片描述

  • 7
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值