灵魂拷问:为什么 Java 字符串是不可变的?

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

1. 图文分析

来看下面这行代码。

String alita = "阿丽塔";
复制代码

这行代码在字符串常量池中创建了一个内容为“阿丽塔”的对象,并将其赋值给了字符串变量 alita(存储的是字符串对象"阿丽塔"的引用)。如下图所示。 在这里插入图片描述 再看下面这段代码:

String wanger = alita;
复制代码

这行代码将字符串变量 alita 赋值给了字符串变量 wanger。这时候,wanger 和 alita 存储的是同一个字符串对象的引用。如下图所示。 在这里插入图片描述 再来看下面这行代码。

alita = "战斗天使".concat(alita);
复制代码

这行代码将字符串“战斗天使”拼接在字符串变量 alita 的前面,并重新赋值给 alita。这个过程就比之前的复杂了。我们需要先来看看 concat() 方法做了什么,源码如下所示。

public String concat(String str) {
  int otherLen = str.length();
  if (otherLen == 0) {
  	return this;
  }
  int len = value.length;
  char buf[] = Arrays.copyOf(value, len + otherLen);
  str.getChars(buf, len);
  return new String(buf, true);
}
复制代码

可以看得出,"战斗天使".concat(alita) 这行代码会先在字符串常量池中创建一个新的字符串对象,内容为“战斗天使”,然后 concat() 方法会将其对应的字符数组和“阿丽塔”对应的字符数组复制到一个新的字符数组 buf 中,最后,再通过 new 关键字创建了一个新的字符串对象,并返回。如下图所示。 在这里插入图片描述

从上图中可以得出结论,alita 此时引用的是在堆中新创建的字符串对象。

2. 对象和对象引用

可能有些读者看完上面的图文分析没有理解反而更疑惑了:alita 不是变了吗?从“阿丽塔”变为“战斗天使阿丽塔”?怎么还说字符串是不可变的呢?

这里需要给大家解释一下,什么是对象,什么是对象引用。

在 Java 中,由于不能直接操作对象本身,所以就有了对象引用这个概念,对象引用存储的是对象在内存中的地址。

PS:Java 虚拟机在执行程序的过程中会把内存区域划分为若干个不同的数据区域,如下图所示。 在这里插入图片描述

对象存储在堆(heap)中,而对象的引用存储在栈(stack)中。

我们通常所说的“字符串是不可变的”是指“字符串对象是不可变的”。alita 是字符串对象“阿丽塔”或者“战斗天使阿丽塔”的引用。这下应该明白了吧?

3. 源码分析

我们来看一下 String 类的部分源码。

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

可以看得出, String 类其实是通过操作字符数组 value 实现的。而 value 是 private 的,也没有提供 serValue() 这样的方法进行修改;况且 value 还是 final 的,意味着 value 一旦被初始化,就无法进行改变。

另外呢,String 类提供的方法,比如说 substring():

public String substring(int beginIndex) {
int subLen = value.length - beginIndex;
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
toLowerCase():

public String toLowerCase(Locale locale) {
return new String(result, 0, len + resultOffset);
}
复制代码

还有之前提到的 concat(),看似都能改变字符串的内容,但其实都是在方法内部使用 new 关键字重新创建的新字符串对象。

4. 为什么要不可变

String 类的源码中还有一个重要的字段 hash,用来保存字符串对象的 hashCode。

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

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

  public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
    	char val[] = value;

    	for (int i = 0; i < value.length; i++) {
      	h = 31 * h + val[i];
      }
    	hash = h;
    }
    return h;
  }
}
复制代码

因为字符串是不可变的,所以一旦被创建,它的 hash 值就不会再改变了。由此字符串非常适合作为 HashMap 的 key 值,这样可以==极大地提高效率==。

另外呢,不可变对象天生是==线程安全==的,因此字符串可以在多个线程之间共享。

举个反面的例子,假如字符串是可变的,那么数据库的用户名和密码(字符串形式获得数据库连接)将不再安全,一些高手可以随意篡改,从而导致严重的安全问题。

5. 最后

总结一下,字符串一旦在内存中被创建,就无法被更改。String 类的所有方法都不会改变字符串本身,而是返回一个新的字符串对象。如果需要一个可修改的字符序列,建议使用 StringBuffer 或 StringBuilder 类代替 String 类,否则每次创建的字新符串对象会导致 Java 虚拟机花费大量的时间进行垃圾回收。

猜你喜欢

转载自juejin.im/post/7032078285467189262