为什么说String是不可变的字符串?
有两方面来保证它的不可变性:
一是将字符数组用final关键字修饰,被final修饰的引用数据类型值是不可变的,但这只能限制符号引用的值不可变,即指向的内存空间不可变;
二是String作为封装类没有对外提供修改字符数组中的值的方法,以此来限制它指向的内存空间中的值也不可改变;
不同于一中是使用语法规则达到不可变性,二中通过代码实现来达到不可变性,可以被反射破坏
String类是日常开发中最常用的非基本数据类型,它作为一个引用数据类型的类却经常被误认为是基本数据类型,我认为给大家带来误解的地方恰恰就是java虚拟机做的好的地方
char ch = 'a';
boolean b = false;
byte t = 1;
short sh = 1;
int n = 2;
float f = 3.0;
long l = 4;
double d = 5.0;
String str1 = "今天又学习了";
HashMap map = new HashMap();
在变量的声明与初始化上,String成功的伪装成了“基本数据类型”。
作为一个引用数据类型,下面的代码应该更能让大家正确认识它
String str2 = new String("今天有学习吗?");
String str3 = new String("今天又学习了");
虽然new String的写法更符合引用数据类型的特征,但是我们平时却不这么写,而且也不推荐这样去写。那这样写的问题是什么呢?或者说是什么原因导致了String类与一般引用数据类型的差异呢?了解了原因,再来看问题吧。
1、字符串的常见相信不用过分证明了,它可以是声明为String类型的字符串变量(或者可称为常量,因为String是不可变的),也可以是你代码中的一个类名、变量名、方法名等任何可自定义的名称。
2、String作为一个引用类型,它核心的属性只有一个,而且不可变
private final char value[];
字符串多、值易重复、属性单一,重复的值可以new成不同的对象,而String又是不可变的,那这种就是完全的浪费内存空间,如果能把重复值的字符串对象都用一个对象来表示,就可以节省大量的内存开销了。而java虚拟机就是这么做的。
java8虚拟机在堆中维护了一个字符串常量池,用于存储字符串常量(有待进一步准确的表述),它能保证在池中所有的字符串值相同的实例对象只有一份(池外的当然管不着)。
有了字符串常量池,当声明字符串常量的时候先去检查字符串常量池中有没有值相同的实例对象,有那就直接返回池中对象的引用,没有则实例化一个对象放入池中,再把引用返回,这样就减少了内存的开销。
有了优化手段后还必须注意不能破坏java原本的规则,new出来的对象是肯定会实例对象
Human man = new Human("小明");
Human man1 = new Human("小明");
就如上面的代码,最终会new出来两个“小明”的。显然如果用new String的方式声明就不合适了,所以String就变成了直接声明的方式,通过直接声明的方式实例的字符串对象都会放在字符串常量池中
String str4 = "学习吧";
String str5 = "学习吧";
String str6 = new String("学习吧");
str4 == str5 true
str4 == str6 false
通过上面的声明方式,最终str4==str5,因为他们都指向常量池中的同一个String对象。
String类定义了一系列的字符串操作方法,其中像replace等需要改变值的方法,返回值都是String,因为String是不可变的,所以都是返回了一个新的字符串对象而已。也就是说,修改后的字符串都是new出来的,而没有改变原来的字符串对象。
此外,String类的字符串拼接符“+”的一些特性也需要注意:
String str22 = "ab" + "cd";
//等同于String str22 = "abcd";
//属于编译阶段优化,程序运行到这后会产生一个字符串常量abcd,并放到字符串常量池中
String str23 = "ab";
String str24 = "cd";
String str25 = str23 + str24;
//str23和str24都指向字符串常量池中的成员
//str25是new出来的,字符串常量池中没有“abcd”
String str26 = "ab";
String str27 = str26 + "cd";
//str26指向字符串常量池中的成员
//str27是new出来的,字符串常量池中没有“abcd”,但是字符串常量池中有“cd”
请问下面的代码产生了几个对象?
String str8 = "xl";//xl作为字符串常量池中的成员
String str9 = new String("nihao" + str8);//nihao作为字符串常量池中的成员,nihaoxl不是字符串常量池中的成员
以下仅代表我个人观点
任何的设计都是基于现实角度出发的,没有String这个封装类,char[]也能表示字符串,例如c++。为了避免重复劳动,也可以设计char[]的工具类来完成对字符串的查找修改等操作。但现实是,程序中会出现大量重复的字符串,而且这些字符串初始化之后就没有任何的改变,但虚拟机却为这些相同的、整个生命周期都没有任何变化的字符串都分配了内存空间。
于是就有了优化的方向,将大量重复的、无变化的字符串使用同一个字符串来表示,使一块内存空间可以被多个地方使用,以此来减少对内存的浪费。于是有了这样的问题:怎么去实现呢?
学过算法的应该都了解时间和空间的概念,优化的两个方向:以时间换空间、以空间换时间。这像是一个哲学问题,又像是物理中的能量守恒定律。
想要节省被浪费的内存空间问题,必然要以时间为代价。即每当声明一个字符串的时候,从直接开辟一个内存空间,转为先扫描内存中有没有相同的字符串,有则直接使用,没有再开辟一个内存空间。但是扫描整个内存空间显然是不合适的,我们希望的是只扫描那些被定义成字符串的空间。
把开辟给字符串的空间给管理起来,这样就缩小了查找的范围。解决了重复字符串的内存空间占用问题,但对于程序中字符串的大体量来说,即便缩小了查找的范围,如果挨个遍历仍然会产生不小的时间开销。用什么数据结构管理字符串空间合适呢?
节省时间开销?那必然是以空间换时间啊!哈希表再合适不过了。
哈希表是以key-value(逻辑结构)为数组元素的数组结构,可以将字符串的值作为key计算出一个唯一的数组下标,当有相同值的字符串被声明时,通过计算得出唯一数组下标,访问这个下标位置就可以得到已经被实例化过的字符串了。
到这里其实池化的概念已经呼之欲出了。不可变字符串String类的出现原因也要浮出水面了。
想要以字符串的值作为哈希表的key值,有个硬性要求——字符串不可变。而这个要求恰恰就是我们优化的对象的特征之一——只在初始化的时候赋值。
哈哈哈,一直强调不可变,巧的是java的语法规则不能直接实现不可变。即便用final来修饰char[],也只能保证指向的字符数组不变,而不能保证字符数组中的元素不变。但可以封装一个不可变字符串类啊,只要保证外部不能修改内部的char[]属性,那封装之后的类就是不可变的字符串类。
相对于不可变字符串的两个不可变,可变字符串让它们都可变
可变意味着它不是字符串常量,无法交给字符串常量池管理
介绍StringBulider之前我们先来看看上面留的问题
请问下面的代码产生了几个对象?
String str8 = "xl";//xl作为字符串常量池中的成员
String str9 = new String("nihao" + str8);//nihao作为字符串常量池中的成员,nihaoxl不是字符串常量池中的成员
其中③④两个对象可以通过反编译查看字节码指令
L1
LINENUMBER 15 L1
LDC "xl"
ASTORE 1
L2
LINENUMBER 16 L2
NEW java/lang/String
DUP
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder. ()V
LDC "nihao"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKESPECIAL java/lang/String. (Ljava/lang/String;)V
ASTORE 2
因为String的不可变性,所以它并不会提供类似StringBuilder.append()的拼接方法,所以String的+操作实际上隐式的交给了StringBuilder来完成。
所以在实际的开发过程中,遇到会频繁修改字符串值的情况,推荐直接使用StringBulider替代String。使用append替代+。
StringBuilder的基础属性
/**
* The value is used for character storage.
*/
char[] value;
未完待续......
1.可变与不可变指的是对象实例的内容,不是符号引用的值。如下面的代码,符号引用的值从指向字符串对象“又来了”改为指向字符串对象“没来啊”,这并不是说String(对象实例)是可变的。相反,正是因为String(对象实例)的不可变性,无法将“又来了”改成“没来啊”,所以只能重新实例了一个String对象。
String aa = "又来了";
aa = "没来啊";
2.本文有涉及池化的概念,这是为了理解与叙述上容易而抽象出的概念,理解了池化的概念已可以解答很多的问题,但要想从根本上解决所有的疑惑,必须再去池化,去了解字符串常量池的实现。推荐一篇博文可以快速地了解字符串常量池的真面目:字符串底层实现
3.c++中在语法规则上使用两个final让引用数据类型不可变,而java中只能用一个final修饰,在语法规则上不能完全实现不可变,但在实现上利用封装的思想,完成了不可变。