转载

为什么Java里面的String对象是不可变的

这篇文章是 Java里面的String对象到底神奇在什么地方 的姊妹篇。

先来说说java对象不可变。

所谓的对象不可变,可以简单的理解为,对象一旦创建后,其状态不可修改。所谓不可修改就是对象里面所有的状态属性都不能修改。

举个例子:

public class Book {
    //书价
    private final double price;
    //书名
    private final String name;

    public Book(double price, String name) {
        this.price = price;
        this.name = name;
    }

    public double getPrice() {
        return price;
    }

    public String getName() {
        return name;
    }
}

这个对象一旦被创建,就没有办法修改内部状态。因为没有对外提供任何方法修改属性值。而且 pricename 属性被声明为 final

在来看看String类是如何定义的。以下源码摘自JDK1.8

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];

    /**
     * Initializes a newly created {@code String} object so that it represents
     * an empty character sequence.  Note that use of this constructor is
     * unnecessary since Strings are immutable.
     */
    public String() {
        this.value = "".value;
    }
}

这段代码最关键的两个部分就是, String 类用 final 关键字进行了修饰,还有就是字符数组 valuefinal 修饰,并且访问权限是 private 的。如果不清楚Java访问权限的,可以看我这篇关于 Java访问权限 的介绍。

String 类用 final 关键字修饰,证明该类是不可继承的。

String 类是用字符数组表示字符串对象的。属性 valuefinal 修饰,证明 value 是不可变的。等等,这里有个小问题, value 是个字符数组,数组是引用类型, value 不可变只能说明字符数组的引用不可变,字符数组里面的内容是可变的。

举个例子 String str = “java”; value 里面的值是这样的 [‘j’,’a’,’v’,’a’] ,我们虽然不能改变 value 到数组 ['j','a','v','a'] 的引用,但是我们很容易改变数组内的值,比如 value[2]=’s’ ;

那为什么我们还说 String 是不可变的呢。关键就在于 private 关键字。用 private 关键字修饰之后,在java的访问权限规则里面,除了在类内部可以修改 value 的值以外,没有任何地方可以修改它了。而我们的JDK工程师,又很小心的设计了该类,在 String 类内部也没有任何一处可以修改 value 的值,真是连自己人都不信任啊。所以 String 对象一旦被创建,就再也不能被修改了。

有人说貌似不是,比如你看下面的代码

String str = “java”;
str = str.substring(1,2);

要想知道str有没有被改变我们可以看一眼substring的源码。

public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > value.length) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    int subLen = endIndex - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return ((beginIndex == 0) && (endIndex == value.length)) ? this
            : new String(value, beginIndex, subLen);
}

关健的代码就是最后一行, new String(value, beginIndex, subLen) 看到了吗,不是在原来的字符上进行修改,而是创建了一个新的对象,然后把变量str指向新的对象。 原来的字符串就被回收了。

费了这么大力气,把 String 类搞成不可变的有什么好处吗?当然有,主要体现在两个方面。

安全:

  1. 在加载数据库用户名密码,以及类加载的时候,都是以String类型传递参数的。如果字符串是可变的,这些信息很容易被篡改,那么将会造成很大的安全问题。
  2. 因为字符串是不可变的,同一个字符串实例可以被多个线程共享。所以天生就是线程安全的。

性能:

  1. 只有当字符串是不可变的,字符串常量池才有可能实现。字符串常量池可以节省JVM的内存空间(多份相同的String字符实际只占同一个空间)
  2. 因为字符串不可变,所以在它创建的时候可以把 hashcode值缓存起来。当使用的时候就不用再重新计算了,这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。
原文  https://hellofrank.github.io/2019/10/17/为什么Java里面的String对象是不可变的/
正文到此结束
Loading...