我所理解的String in Java8

Planeswalker23 2020年06月14日 共4,120字 208次浏览

0. 前言

本文旨在介绍 String 类的相关特性以及源码分析,同时还列出了常见的面试题以及示例回答。

相信学习过 Java 的同学都见过这样一段代码:

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

这几乎是每种语言必写的一个示例代码,而在这段 Java 代码中的 Hello, World! 就属于接下来要聊的 String 类。

本文 JDK 源代码版本为 1.8.0_221

1. String 的不可变性

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

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
}

1.1 final 关键字

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

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

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

1.2 String 底层结构

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

    /** The value is used for character storage. */
    private final char value[];

这个 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 类的不可变性的一种方式。

1.3 使用 final 修饰的原因

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

第二个原因是安全, 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 String()

对于使用 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);
}

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

示例代码中对象之间的关系

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

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

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

3. 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。

4. String、StringBuilder 和 StringBuffer 的区别

这个问题真的被问吐了,网上资料也非常多了,这里就贴几个链接好了。

参考资料