前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >你真的理解Java 字符串的不可变性吗?

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

作者头像
明明如月学长
发布2021-08-31 11:26:13
3850
发布2021-08-31 11:26:13
举报
文章被收录于专栏:明明如月的技术专栏

一、背景

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

常见的回答如:

字符串创建后不可改变。

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

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

真的是这样吗?

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

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

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

代码语言:javascript
复制
// 前面代码省略
System.out.println("ab");

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

二、案例

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

代码语言:javascript
复制
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 );
    }
}

输出结果为:

代码语言:javascript
复制
str=ab
str=aw

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

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

我们再多打印一些:

代码语言:javascript
复制
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());
    }
}

输出结果为:

代码语言:javascript
复制
str=ab,1638215613,3105
str=aw,1638215613,3105

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

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

代码语言:javascript
复制
    /**
     * 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 源码:

代码语言:javascript
复制
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 保证不可继承。

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

代码语言:javascript
复制
    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);
    }

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

代码语言:javascript
复制
  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");
    }

可以看到打印结果为:

代码语言:javascript
复制
str=ab,1638215613,3105
str=aw,1638215613,3105
aw

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

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

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共享唯一实例。

代码语言:javascript
复制
    /**
     * Returns a canonical representation for the string object.
     * 
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * 
     * 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.
     * 
     * 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}.
     * 
     * All literal strings and string-valued constant expressions are
     * interned. String literals are defined in section 3.10.5 of the
     * The Java™ Language Specification.
     *
     * @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 属于字面量,指向同一个字符串池中的同一个对象,因此对象的属性修改,两个地方打印都会受到影响。

三、思考

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

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

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/08/23 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、背景
  • 二、案例
  • 三、思考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档