String是如何保证不变的?反射为什么可以改变String的值?

1 篇文章 0 订阅

String是如何保证不变的?反射为什么可以改变String的值?

1. String字符串的源码分析

String 字符串到底能不能改变已经是老生常谈的问题了,但是在面试环节中,依然能够难住不少人。

下面我们根据 JDK1.8 版本下的String源码进行分析,一步一步的了解String字符串的不可变性。

image-20230214154817472

String底层是使用final修饰的字符数组 value[] 来存储字符,而数组是引用类型,引用类型的值是内存中的地址,地址在初始化之后不可变,所以String的值不可变。

2. 通过反射改变字符串的值

虽然final修饰的数组地址不可改变,但是地址指向的值(堆内存中)是可以改变的,String没有对外提供相应的方法来更改值,但是可以通过反射实现。

import java.lang.reflect.Field;

public class stringDemo {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String s = "abc";
        Class clz = s.getClass();
        //需要使用getDeclaredField(), getField()只能获取公共成员字段
        Field field = clz.getDeclaredField("value");
        field.setAccessible(true);

        char[] ch =(char[])field.get(s);
        ch[1] = '8';
        System.out.println(s);
    }
}

打印结果:

image-20230216105045200

(在实际开发中,很少需要通过反射来修改String的值。这里只是提供一种思路,在某些情况下可以帮助我们解决一些实际问题。)

上面我们通过案例知道了,字符串的字符数组可以通过反射进行修改,导致字符串的“内容”发生了变化。但即使是内容发生了改变,它的hash值也是不会改变的:

import java.lang.reflect.Field;

public class stringDemo {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String s = "abc";
        System.out.println("str=" + s + "," +  s.hashCode());
        Class clz = s.getClass();
        //需要使用getDeclaredField(), getField()只能获取公共成员字段
        Field field = clz.getDeclaredField("value");
        field.setAccessible(true);

        char[] ch =(char[])field.get(s);
        ch[1] = '8';
        System.out.println("str=" + s + "," + s.hashCode());
    }
}

运行结果:

image-20230216110228618

从上述结果可以知道,String 字符串对象的 value 数组的元素是可以被修改的,但是hash值没有发生改变,也就是说对象没有变。

但是字符串两次打印出来的结果不一样,计算的hash值为什么是一样的呢?

/** Cache the hash code for the string */
    private int hash; // Default to 0
/**
     * Returns a hash code for this string. The hash code for a
     * {@code String} object is computed as
     * <blockquote><pre>
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * </pre></blockquote>
     * using {@code int} arithmetic, where {@code s[i]} is the
     * <i>i</i>th character of the string, {@code n} is the length of
     * the string, and {@code ^} indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    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的计算结果(只要缓存过了就不会再重新计算),因此第二次打印hash值和第一次相同。

3. 如何理解String字符串的不可变性呢?

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

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

image-20230218093416810

image-20230218093514172

。。。。。。

接下来我们来分析下面这段代码,

import java.lang.reflect.Field;

public class stringDemo {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String s = "abc";
        System.out.println("str=" + s + "," +  s.hashCode());
        Class clz = s.getClass();
        //需要使用getDeclaredField(), getField()只能获取公共成员字段
        Field field = clz.getDeclaredField("value");
        field.setAccessible(true);

        char[] ch =(char[])field.get(s);
        ch[1] = '8';
        System.out.println("str=" + s + "," + s.hashCode());
        System.out.println("abc");
    }
}

运行结果:

image-20230218093817638

是不是很神奇,明明打印的是System.out.println(“abc”);但是得到的结果是:a8c。其实道理也很简单,是因为字符串字面量都指向字符串池中的同一个字符串对象(本质是池化的思想,通过复用来减少资源占用来提高性能),通过官方的解释可以明确这一点:

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

image-20230218095010665

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

image-20230218095145221

以上可以分析得到:对象池中存在,则直接指向对象池中的字符串对象,否则创建字符串对象放到对象池中并指向该对象。

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

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

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

很萌の萌新

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值