你真的理解Java 字符串的不可变性吗?

简介: 你真的理解Java 字符串的不可变性吗?

一、背景

字符串的不可变性可以说是面试中的一个常见的“简单的” 问题。

常见的回答如:

字符串创建后不可改变。

字符串的不可变性是指字符串的字符不可变。

String 的 value 字符数组声明为 final 保证不可变。

真的是这样吗?

下面我们再思考两个问题:

  • 那么字符串的不可变究竟是指什么?
  • 是如何保证的呢?

下面看一个奇怪的现象:在程序一段程序的最后执行下面的语句居然打印了 "aw" 为什么?

// 前面代码省略
System.out.println("ab");

建议大家先思考,然后再看下文。

二、案例

你认为下面的示例代码的输出结果是啥?

import java.lang.reflect.Field;

public class Test {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String str = "ab";
        System.out.println("str=" + str);
        Class stringClass = str.getClass();
        Field field = stringClass.getDeclaredField("value");
        field.setAccessible(true);
        char[] value = (char[]) field.get(str);
        value[1] = 'w';
        System.out.println("str=" + str );
    }
}

输出结果为:

str=ab
str=aw

是不是和有些同学想的有些不一样呢?

字符串的字符数组可以通过反射进行修改,导致字符串的“内容”发生了变化。

我们再多打印一些:

import java.lang.reflect.Field;

public class Test {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String str = "ab";
        System.out.println("str=" + str + "," + System.identityHashCode(str)+","+ str.hashCode());
        Class stringClass = str.getClass();
        Field field = stringClass.getDeclaredField("value");
        field.setAccessible(true);
        char[] value = (char[]) field.get(str);
        value[1] = 'w';
        System.out.println("str=" + str + "," + System.identityHashCode(str)+","+ str.hashCode());
    }
}

输出结果为:

str=ab,1638215613,3105
str=aw,1638215613,3105

通过这个例子我们可以看出,String 字符串对象的 value 数组的元素是可以被修改的。

简单看下 java.lang.System#identityHashCode 的源码:

    /**
     * Returns the same hash code for the given object as
     * would be returned by the default method hashCode(),
     * whether or not the given object's class overrides
     * hashCode().
     * The hash code for the null reference is zero.
     *
     * @param x object for which the hashCode is to be calculated
     * @return  the hashCode
     * @since   JDK1.1
     */
    public static native int identityHashCode(Object x);

native 方法,该函数给出对象唯一的哈希值(不管是否重写了 hashCode 方法)。

可知,对象没有变。

那么,我们知道 String 的哈希值是通过字符串的字符数组计算得来的(JDK8),那为啥两次 hashCode 函数返回值一样呢?

我们再仔细看下 java.lang.String#hashCode 源码:

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 

    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;
    }
  //省略其他
}

发现在第一次调用 hashCode 函数之后,字符串对象内通过 hash 这个属性缓存了 hashCode的计算结果(只要缓存过了就不会再重新计算),因此第二次和第一次相同。

那么如何保证不可变性的呢?

首先将 String 类声明为 fianl 保证不可继承。

然后,所有修改的方法都返回新的字符串对象,保证修改时不会改变原始对象的引用。

    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);
    }

其次,字符串字面量都会指向同一个对象。

  public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    // 字符串字面量
        String str = "ab";
        System.out.println("str=" + str + "," + System.identityHashCode(str)+","+ str.hashCode());
        Class stringClass = str.getClass();
        Field field = stringClass.getDeclaredField("value");
        field.setAccessible(true);
        char[] value = (char[]) field.get(str);
        value[1] = 'w';
        System.out.println("str=" + str + "," + System.identityHashCode(str)+","+ str.hashCode());
    // 字符串字面量
        System.out.println("ab");
    }

可以看到打印结果为:

str=ab,1638215613,3105
str=aw,1638215613,3105
aw

很多人不理解,为啥 System.out.println("ab"); 打印 aw ?

是因为字符串字面量都指向字符串池中的同一个字符串对象(本质是池化的思想,通过复用来减少资源占用来提高性能)。

https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.10.5

A string literal is a reference to an instance of class String ( §4.3.1, §4.3.3).

字符串字面量是指向字符串实例的一个引用。

Moreover, a string literal always refers to the same instance of class String. This is because string literals - or, more generally, strings that are the values of constant expressions (§15.28) - are "interned" so as to share unique instances, using the method String.intern.

字符串字面量都指向同一个字符串实例。
因为字面量字符串都是常量表达式的值,都通过String.intern共享唯一实例。

    /**
     * Returns a canonical representation for the string object.
     * <p>
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * <p>
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * <p>
     * It follows that for any two strings {@code s} and {@code t},
     * {@code s.intern() == t.intern()} is {@code true}
     * if and only if {@code s.equals(t)} is {@code true}.
     * <p>
     * All literal strings and string-valued constant expressions are
     * interned. String literals are defined in section 3.10.5 of the
     * <cite>The Java&trade; Language Specification</cite>.
     *
     * @return  a string that has the same contents as this string, but is
     *          guaranteed to be from a pool of unique strings.
     */
    public native String intern();

对象池中存在,则直接指向对象池中的字符串对象,否则创建字符串对象放到对象池中并指向该对象。

因此可以看出,字符串的不可变性是指引用的不可变。

虽然 String 中的 value 字符数组声明为 final,但是这个 final 仅仅是让 value的引用不可变,而不是为了让字符数组的字符不可替换。

由于开始的 ab 和最后的 ab 属于字面量,指向同一个字符串池中的同一个对象,因此对象的属性修改,两个地方打印都会受到影响。

三、思考

很多简单的问题并没有看起来那么简单。

大家在看技术博客,在读源码的时候,一定要有自己的思考,多问几个为什么,有机会多动手实践。

大家在学习某个技术时要养成本质思维,即思考问题的本质是什么。

面试的时候,简单的问题要回答全面又有深度,不会的问题要回答出自己的思路,这样才会有更多的机会。

相关文章
|
10天前
|
SQL JSON Java
告别字符串拼接:用Java文本块优雅处理多行字符串
告别字符串拼接:用Java文本块优雅处理多行字符串
213 108
|
3月前
|
SQL JSON Java
告别拼接噩梦:Java文本块让多行字符串更优雅
告别拼接噩梦:Java文本块让多行字符串更优雅
391 82
|
3月前
|
自然语言处理 Java Apache
在Java中将String字符串转换为算术表达式并计算
具体的实现逻辑需要填写在 `Tokenizer`和 `ExpressionParser`类中,这里只提供了大概的框架。在实际实现时 `Tokenizer`应该提供分词逻辑,把输入的字符串转换成Token序列。而 `ExpressionParser`应当通过递归下降的方式依次解析
237 14
|
7月前
|
存储 缓存 安全
Java 字符串详解
本文介绍了 Java 中的三种字符串类型:String、StringBuffer 和 StringBuilder,详细讲解了它们的区别与使用场景。String 是不可变的字符串常量,线程安全但操作效率较低;StringBuffer 是可变的字符串缓冲区,线程安全但性能稍逊;StringBuilder 同样是可变的字符串缓冲区,但非线程安全,性能更高。文章还列举了三者的常用方法,并总结了它们在不同环境下的适用情况及执行速度对比。
169 17
|
7月前
|
存储 缓存 安全
Java字符串缓冲区
字符串缓冲区是用于处理可变字符串的容器,Java中提供了`StringBuffer`和`StringBuilder`两种实现。由于`String`类不可变,当需要频繁修改字符串时,使用缓冲区更高效。`StringBuffer`是一个线程安全的容器,支持动态扩展、任意类型数据转为字符串存储,并提供多种操作方法(如`append`、`insert`、`delete`等)。通过这些方法,可以方便地对字符串进行添加、插入、删除等操作,最终将结果转换为字符串。示例代码展示了如何创建缓冲区对象并调用相关方法完成字符串操作。
146 13
|
11月前
|
SQL Java 索引
java小工具util系列2:字符串工具
java小工具util系列2:字符串工具
251 83
|
11月前
|
Java 数据库
java小工具util系列1:日期和字符串转换工具
java小工具util系列1:日期和字符串转换工具
193 26
|
11月前
|
存储 缓存 安全
java 中操作字符串都有哪些类,它们之间有什么区别
Java中操作字符串的类主要有String、StringBuilder和StringBuffer。String是不可变的,每次操作都会生成新对象;StringBuilder和StringBuffer都是可变的,但StringBuilder是非线程安全的,而StringBuffer是线程安全的,因此性能略低。
292 8
|
11月前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
175 6

热门文章

最新文章