李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
字符串--String不可变
Leefs
2019-12-15 PM
4701℃
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
评论已关闭
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
NLP
4
标签云
Http
机器学习
SpringCloudAlibaba
人工智能
Typora
Java阻塞队列
Azkaban
栈
Beego
ajax
链表
VUE
Flink
MySQL
LeetCode刷题
Netty
容器深入研究
线程池
数据结构和算法
Scala
BurpSuite
算法
Jquery
Ubuntu
Python
Golang
JavaWeb
Linux
Jenkins
Livy
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞
评论已关闭