我所理解的JDK系列·第2篇·String对象是如何创建的

2020/06/14 JDK 共 3784 字,约 11 分钟

本文从一道经典面试题开始介绍了 String 的不可变性、底层结构、String 的创建流程,最后引出了另一道经典的 String、StringBuilder、StringBuffer 比较的面试题,希望能对你有所帮助。

jdk-2-封面

1. 开篇词

相信学习过 Java 的同学,在开始入坑 Java 的第一天都会见到这样一段代码:

public static void main(String[] args) {
 	System.out.println("Hello, World!");   
}

这几乎是每种语言必写的一个示例代码,而在这段 Java 代码中的 Hello, World! 就属于接下来要聊的 String 类,虽然它看上去很简单,但如果使用不当,还是会踩到一些不为人知的深坑。

比如说这道经典的面试题,下面这段代码最终输出的是 true 还是 false?

public static void main(String[] args) {
  String a = "abc";
  String b = new String("abc");
  String c = b.intern();
  System.out.println(a==b);
  System.out.println(a==c);
  System.out.println(b==c);
}

看完本篇关于 String 的介绍后,相信大家会对这个问题了然于胸,废话不多说,直入正题。

2. String 的不可变性

正如 String 类源码文档中所说的那样:String 是常量,它的值在创建后就不能改变。而 String 类的不可变性是由于它被 final 关键字所修饰。

jdk-2-1

2.1 final 关键字

顺便复习一下 Java 基础—— final 关键字的作用:

  • final 关键字可以被修饰于类、方法和变量
  • final 关键字强调的是一种不可变性,对于上述三种情况有不同的具体含义:
    • 当 final 被修饰于类时,表明该类不能被继承
    • 当 final 被修饰于方法时,表明该方法不能被重写
    • 当 final 被修饰于变量时,又分两种情况:
      • 若变量属于基本数据类型,那么它的值在初始化之后就不能再更改
      • 若变量是引用类型,那么在初始化之后它就不能再指向另外的对象

所以在 String 类上使用 final 关键字修饰,保证了 String 类不能被扩展。

2.2 String 底层结构

在 String 类中有一个 char 类型的数组,它是用来存储字符串的。

jdk-2-1

这个 char 类型的变量也是被 final 关键字修饰,而数组变量属于引用类型,它的值其实就是数组的地址,也就是说 value 数组在初始化之后就不能再指向另外的对象,这也是保证 String 类不可变性的方式之一。

但是这里有一点需要额外提到的就是,虽然 value 数组在初始化之后就不能再指向另外的对象,但是它原本指向的地址的内容是可以改变的。

下面是两个直接修改被 final 修饰的变量的示例。

final char[] str = {'1','2','3'};
// 直接赋值将 str 数组的内容修改为{'1','2','4'}
str[2] = '4';
// 通过反射将 str 数组的内容修改为{'1','2','5'}
java.lang.reflect.Array.set(str, 2, '5');

第一个方式是直接对 str 数组变量中元素进行赋值,第二个方式是通过反射修改数组内容。所以,要记得的是:被 final 修饰的引用类型变量只是引用地址不能改变。而为了保证 String 对象的内容,也就是 value 数组的元素不被修改,源码中也没有提供任何修改 value 数组的方法,这也是保证 String 类的不可变性的另一种方式。

但是,强大的 Java 提供了反射能力,这就让我们有了改变 String 内容的机会,比如:

jdk-2-2

这段代码的运行结果是:

原始内容:abc
修改后内容:Abc

即通过反射成功修改了 String 的值。

2.3 使用 final 修饰的原因

在这里需要提一下,为什么 Java 的开发者要使用 final 来修饰 String?

首先第一原因是高效,就拿常量池来说,只有变量是不可修改的,才能够被缓存起来,从而实现常量池的功能。

第二个原因是安全, Java 之父 James Gosling 解释过,迫使 String 类设计成不可变的另一个原因是安全,当你在调用其他方法时,比如调用一些系统级操作指令之前,可能会有一系列校验,如果是可变类的话,可能在你校验过后,它的内部的值又被改变了,这样有可能会引起严重的系统崩溃问题。

2. String 的创建流程

通常 String 类有两种创建方式,直接赋值和通过 new 关键词来创建,如:

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

2.1 直接赋值

先来说直接赋值的方式:首先会去常量池中寻找 abc 字符串是否存在,若已存在会将 a 变量直接指向常量池中的值。如果不存在,会在常量池中先创建一个 abc 字符串,然后把 str 指向刚刚创建出来的 abc 字符串。

2.2 new 关键字

对于使用 new 关键词来创建一个 String 对象的情况,首先虚拟机会在 Java 堆中创建一个 String 类型的对象,然后再去常量池中寻找 abc 字符串是否存在,如果存在,将创建的对象的值指向常量池中已存在的字符串;如果不存在会在常量池中创建一个 abc 字符串,然后把 Java 堆中的对象引用的值指向在常量池中创建的 abc 字符串。

2.3 代码示例

我们写个例子验证一下上面的结论。

public static void main(String[] args) {
    String a = "abc";
    String b = new String("abc");
    System.out.println(a==b);
}

下面这张图描述了示例代码中对象之间的关系。

jdk-2-3

第1行代码中使用直接赋值的方式,由于常量池中 abc 字符串已经存在(虚拟机在类加载期间在常量池中创建该字符串),所以 a 变量会直接指向常量池中的 abc 字符串。

第2行代码中使用 new String("abc") 创建了一个对象,所以会在堆中创建一个 value 数组对象,而此时常量池已存在 abc 字符串,所以 b 对象的 value 数组的引用值会指向常量池中的 abc 字符串。

最后,由于 a 对象的地址相当于常量池中 abc 字符串的地址,而 b 对象的地址相当于 value 对象的地址,这两者地址不相同,所以 a 与 b 两个对象不相等。

2.4 String#intern

其实除了上述的两种创建方式之外,还有一种方式可以创建 String 对象,那就是 String#intern 方法。

intern 是一个本地方法,它返回的是一个字符串对象,如果常量池已经包含了字符串,那么直接返回该字符串;否则,会将该字符串添加到常量池,并返回该字符串。

回到本文开篇词中提到的抛出的问题:

public static void main(String[] args) {
    String a = "abc";
    String b = new String("abc");
    String c = b.intern();
    System.out.println(a==b);
    System.out.println(a==c);
    System.out.println(b==c);
}

a 和 b 的关系我们已经知道了,那么对于使用 intern 方法返回的 c 字符串,会是怎样的情况呢?执行这个方法,我们会发现输出结果是:

false   // a!=b
true    // a==c
false   // b!=c

对照 intern 方法的作用,其实也能够知道这个结果:当调用 intern 方法时,如果常量池中存在 abc 字符串,那么直接返回。

在示例代码的场景中,此时 abc 字符串肯定已经存在于常量池中,而且这个字符串是在类加载期间创建的,同时又被赋值给 a 变量,所以 a==c 的判断结果是 true。

3. String、StringBuilder、StringBuffer

相信对于初级程序员来说,在面试中经常会被问到的问题就是:说一说 String、StringBuilder、StringBuffer 这三个类的区别?

简单来说,String 是线程不安全的,StringBuilder 也是线程不安全的,StringBuffer 是线程安全的(通过将方法加 synchronized 锁修饰)。且StringBuilder 和 StringBuffer 的速度比 String 要快,所以一般在进行字符串拼接时,如果不考虑线程安全性问题,推荐使用 StringBuilder。

说实话这个问题真的被问吐了,网上资料也非常多了,这里推荐下面这篇博客,里面还有实际的案例来比较 String、StringBuilder、StringBuffer 的速度:StringBuilder 比 String 快?空嘴白牙的,证据呢!

4. 小结

本文从一道经典面试题开始介绍了 String 的不可变性、底层结构、String 的创建流程,最后引出了另一道经典的 String、StringBuilder、StringBuffer 比较的面试题,希望能对你有所帮助。

5. 参考资料

  • On Java8
  • [Java性能调优实战第3讲字符串](https://time.geekbang.org/column/article/4a182f7bfe2e5f9397e6188099edf4b0/share?code=VCKLPjeXdS0XDO%2FWFcwkbB1I3fpf0EgS-zEXpNP0u8k%3D)

最后,本文收录于个人语雀知识库: 我所理解的后端技术,欢迎来访。

文档信息

Search

    Table of Contents