博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java中的String为什么是不可变的?
阅读量:2492 次
发布时间:2019-05-11

本文共 3879 字,大约阅读时间需要 12 分钟。

什么是不可变对象?

如果一个对象它被创建后,状态不能改变,则这个对象被认为是不可变的。

String是如何实现其对象不可变?

首先需要补充一个容易混淆的知识点:当使用final修饰基本类型变量时,不能对基本类型变量重新赋值因此基本类型变量不能被改变但对于引用类型变量而言,它保存仅仅是一个引用final只保证这个引用变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。例如某个指向数组的final引用,它必须从此至终指向初始化时指向的数组,但是这个数组的内容完全可以改变。

我们来看一下String类的两个主要成员变量,其中value指向的是一个字符数组,字符串中的字符就是用这个value变量存储起来的,并且用final修饰,也就是说value一旦赋予初始值之后,value指向的地址就不能再改变了虽然value指向的数组是可以改变的,但是String没有提供相应的方法让我们去修改value指向的数组的元素然而在StringBuilder中是提供了修改value指向的数组的元素的方法这也是StringBuilder的字符串序列可变的原因

/** The value is used for character storage. */  private final char value[];   /** Cache the hash code for the string */  private int hash; // Default to 0

有一些看起来String对象可变的幻觉?

String中提供了一些看似可以改变String对象的方法,但实际上它们已经是指向了一个新建的对象。

程序例子:

public static void main(String[] args) {        String str1 = "hello";        // 打印str1的内存地址        System.out.println("str1的内存地址:" + System.identityHashCode(str1));        String str2 = "world";        str1 += str2;        // str1的内存地址已经改变了        System.out.println("执行+=后str1的内存地址:" + System.identityHashCode(str1));        System.out.println("拼接之后str1的值:" + str1);         String str3 = "123";        // 创建一个新的对象来保存拼接之后的值        String str4 = str3.concat("456");        // concat操作不会改变原来str3的值        System.out.println("str3的值:" + str3);        System.out.println("str4的值:" + str4);         String str5 = "ABC";        // replace操作不会改变原来str5的值        String str6 = str5.replace("A", "B");        System.out.println("str5的值:" + str5);        System.out.println("str6的值:" + str6);    }

运行结果:

str1的内存地址:1922154895执行+=后str1的内存地址:883049899拼接之后str1的值:helloworldstr3的值:123str4的值:123456str5的值:ABCstr6的值:BBC

程序分析:

str1+=str2实际上是执行了str1=(new StringBuilder()).append(str2).toString();前后实际额外产生了一个StringBuilder与一个helloworld的字符串常量。str1执行+=前后内存的示意图如下所示:
在这里插入图片描述在这里插入图片描述
上面使用了String类的concat与replace方法,执行这两个操作不会对原来的对象产生影响,他们会返回一个全新的对象。我们可以来看一下这两个方法的源码。

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

replace方法源码:

public String replace(char oldChar, char newChar) {        if (oldChar != newChar) {            int len = value.length;            int i = -1;            char[] val = value; /* avoid getfield opcode */             while (++i < len) {                if (val[i] == oldChar) {                    break;                }            }            if (i < len) {                char buf[] = new char[len];                for (int j = 0; j < i; j++) {                    buf[j] = val[j];                }                while (i < len) {                    char c = val[i];                    buf[i] = (c == oldChar) ? newChar : c;                    i++;                }                return new String(buf, true);            }        }        return this;    }

String对象真的不可变吗?

虽然value是final修饰的,只是说明value不能再重新指向其他的引用。但是value指向的数组可以改变,一般情况下我们是没有办法访问到这个value指向的数组的元素。But,反射,对,反射可以,牛逼吧。可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。show you the code!

public static void main(String[] args) throws Exception {        String str = "Hello World";        System.out.println("修改前的str:" + str);        System.out.println("修改前的str的内存地址" + System.identityHashCode(str));        // 获取String类中的value字段        Field valueField = String.class.getDeclaredField("value");        // 改变value属性的访问权限        valueField.setAccessible(true);        // 获取str对象上value属性的值        char[] value = (char[]) valueField.get(str);        // 改变value所引用的数组中的字符        value[3] = '?';        System.out.println("修改后的str:" + str);        System.out.println("修改前的str的内存地址" + System.identityHashCode(str));    }

运行结果:

修改前的str:Hello World修改前的str的内存地址1922154895修改后的str:Hel?o World修改前的str的内存地址1922154895

可以看到str的字符串序列已经被改变了,但是str的内存地址还是没有改变。有疑问?在下方留言哦。

转载地址:http://trbrb.baihongyu.com/

你可能感兴趣的文章
order by 排序原理及性能优化
查看>>
Lock重入锁
查看>>
docker安装 rabbitMq
查看>>
git 常用命令 入门
查看>>
关闭selinx nginx无法使用代理
查看>>
shell 脚本部署项目
查看>>
spring cloud zuul网关上传大文件
查看>>
springboot+mybatis日志显示SQL
查看>>
工作流中文乱码问题解决
查看>>
maven打包本地依赖包
查看>>
spring boot jpa 实现拦截器
查看>>
jenkins + maven+ gitlab 自动化部署
查看>>
Pull Request流程
查看>>
Lambda 表达式
查看>>
函数式数据处理(一)--流
查看>>
java 流使用
查看>>
java 用流收集数据
查看>>
java并行流
查看>>
CompletableFuture 组合式异步编程
查看>>
mysql查询某一个字段是否包含中文字符
查看>>