李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
字符串--String不可变
Leefs
2019-12-15 PM
3285℃
3条
# 字符串--String不可变 ### 前言 **可以证明,字符串操作是计算机程序设计中最常见的行为。** 本篇开始《Java编程思想》第13章,字符串的学习 ### 一、不可变String #### 1. 什么是不可变对象? 如果一个对象,在它创建完成之后,**不能再改变它的状态**,那么这个对象就是不可变的。 不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。 #### 2. 区分对象和对象的引用 许多初学Java的人,对于String是不可变对象总是存在疑惑。 > 代码示例 ```java String s = "ABCabc"; System.out.println("s = " + s); s = "123456"; System.out.println("s = " + s); ``` > 输出结果 ```java s = ABCabc s = 123456 ``` > 结果分析 首先创建一个String对象s,然后让s的值为“ABCabc”, 然后又让s的值为“123456”。 **从打印结果可以看出,s的值确实改变了。那么怎么还说String对象是不可变的呢?** 其实这里存在一个误区: s只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个4字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。 也就是说,S只是一个引用,它指向了一个具体的对象,当`s="123456";`这句代码执行以后,又创建了一个新的对象"123456",而引用s重新指向了这个新的对象,原来的对象"ABCabc"还在内存中存在,并没有改变。 > 内存结构图 ![不可变String01.png][1] Java和C++的一个不同点是, 在Java中不可能直接操作对象本身,所有的对象都由一个引用指向,必须通过这个引用才能访问对象本身,包括获取成员变量的值,改变对象的成员变量,调用对象的方法等。而在C++中存在引用,对象和指针三个东西,这三个东西都可以访问对象。其实,Java中的引用和C++中的指针在概念上是相似的,他们都是存放的对象在内存中的地址值,只是在Java中,引用丧失了部分灵活性,比如Java中的引用不能像C++中的指针那样进行加减运算。 #### 3. 为什么String对象是不可变的? *要理解String的不可变性,首先看一下String类中都有哪些成员变量。 在JDK1.6中,String的成员变量有以下几个:* ```java public final class String implements java.io.Serializable, Comparable
, CharSequence{ /** The value is used for character storage. */ private final char value[]; /** The offset is the first index of the storage that is used. */ private final int offset; /** The count is the number of characters in the String. */ private final int count; /** Cache the hash code for the string */ private int hash; // Default to 0
``` 在JDK1.7中,String类做了一些改动,主要是改变了substring方法执行时的行为,这和本文的主题不相关。JDK1.7中String类的主要成员变量就剩下了两个: ```java public final class String implements java.io.Serializable, Comparable
, 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
``` 由以上的代码可以看出, 在Java中String类其实就是对字符数组的封装。JDK6中, value是String封装的数组,offset是String在这个value数组中的起始位置,count是String所占的字符的个数。在JDK7中,只有一个value变量,也就是value中的所有字符都是属于String这个对象的。 除此之外还有一个hash成员变量,是该String对象的哈希值的缓存,在Java中,数组也是对象, 所以value也只是一个引用,它指向一个真正的数组对象。其实执行了String s = “ABCabc”; 这句代码之后,真正的内存布局应该是这样的: ![不可变String02.png][2] value,offset和count这三个变量都是private的,并且没有提供setValue, setOffset和setCount等公共方法来修改这些值,所以在String类的外部无法修改String。也就是说一旦初始化就不能修改, 并且在String类的外部不能访问这三个成员。此外,value,offset和count这三个变量都是final的, 也就是说在String类内部,一旦这三个值初始化了, 也不能被改变。所以可以认为String对象是不可变的了。 #### 4. String中方法对其值的改变 那么在String中,明明存在一些方法,调用他们可以得到改变后的值。这些方法包括substring, replace, replaceAll, toLowerCase等 > 示例 ```java String a = "ABCabc"; System.out.println("a = " + a); a = a.replace('A', 'a'); System.out.println("a = " + a); ``` > 输出结果 ```java a = ABCabc a = aBCabc ``` 这是因为 a只是一个引用, 不是真正的字符串对象,在调用a.replace('A', 'a')时, 方法内部创建了一个新的String对象,并把这个新的对象重新赋给了引用a。 String中replace方法的源码: ![不可变String03.png][3] #### 5. String不可变总结 > 1. 1.String类是final的,不可被继承。public final class String。 > 2. 2.String类是的本质是字符数组char[], 并且其值不可改变。private final char value[]; > 3. 3.String类对象有个特殊的创建的方式,就是直接指定比如String x = "abc","abc"就表示一个字符串对象。而x是"abc"对象的地址,也叫做"abc"对象的引用。 > 4. 4.String对象可以通过“+”串联 > 5. 5.Java运行时会维护一个String Pool(String池),JavaDoc翻译很模糊“字符串缓冲区”。String池用来存放运行时中产生的各种字符串,并且池中的字符串的内容不重复。而一般对象不存在这个缓冲池,并且创建的对象仅仅存在于方法的堆栈区。 ### 6. String对象的创建原理 > 原理1: 当使用任何方式来创建一个字符串对象s时,Java运行时(运行中JVM)会拿着这个X在String池中找是否存在内容相同的字符串对象,如果不存在,则在池中创建一个字符串s,否则,不在池中添加。 > > 原理2: Java中,只要使用new关键字来创建对象,则一定会(在堆区或栈区)创建一个新的对象。 > > 原理3: 使用直接指定或者使用纯字符串串联来创建String对象,则仅仅会检查维护String池中的字符串,池中没有就在池中创建一个,有则罢了!但绝不会在堆栈区再去创建该String对象。 > > 原理4: 使用包含变量的表达式来创建String对象,则不仅会检查维护String池,而且还会在堆栈区创建一个String对象。 #### 7. String对象真的不可变吗? 从上文可知String的成员变量是private final 的,也就是初始化之后不可改变。那么在这几个成员中, value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗? 比如将数组中的某个位置上的字符变为下划线“_”。 至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个value引用,更不能通过这个引用去修改数组。 那么用什么方式可以访问私有成员呢? 没错,用反射, 可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。 > 示例方法 ```java public static void testReflection() throws Exception { //创建字符串"Hello World", 并赋给引用s String s = "Hello World"; System.out.println("s = " + s); //Hello World //获取String类中的value字段 Field valueFieldOfString = String.class.getDeclaredField("value"); //改变value属性的访问权限 valueFieldOfString.setAccessible(true); //获取s对象上的value属性的值 char[] value = (char[]) valueFieldOfString.get(s); //改变value所引用的数组中的第5个字符 value[5] = '_'; System.out.println("s = " + s); //Hello_World } ``` > 运行结果 ```java s = Hello World s = Hello_World ``` 在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private final 的,但是这个Wheel对象内部的状态可以改变, 那么就不能很好的保证Car对象不可变。 *附:[参考文章链接1](https://www.cnblogs.com/leskang/p/6110631.html),[参考文章链接2](https://www.cnblogs.com/bjut-xiaorun/p/5294811.html)* [1]: https://lilinchao.com/usr/uploads/2019/12/1615182973.png [2]: https://lilinchao.com/usr/uploads/2019/12/4200555997.png [3]: https://lilinchao.com/usr/uploads/2019/12/4097964404.png
标签:
Java
,
Java编程思想
,
JavaSE
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://lilinchao.com/archives/287.html
上一篇
持有对象--总结
下一篇
重载“+”与StringBuilder
取消回复
评论啦~
提交评论
已有 3 条评论
茂林
讲的很透彻哦,厉害厉害
回复
2019-12-17 10:33
茂林
哇哦
秀
回复
2019-12-17 08:38
Leefs
博主
@茂林
哈哈
回复
2019-12-17 10:01
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
标签云
Eclipse
CentOS
二叉树
Golang基础
Hadoop
Zookeeper
Livy
DataWarehouse
Http
Spark SQL
数学
Java阻塞队列
散列
JVM
Elastisearch
机器学习
RSA加解密
随笔
ClickHouse
VUE
Yarn
稀疏数组
递归
Filter
Kafka
JavaWeb
Elasticsearch
高并发
算法
工具
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞
讲的很透彻哦,厉害厉害
哇哦
秀
哈哈