1.字符串常量池
1.1 创建对象的思考
public class Test { public static void main(String[] args) { String str1 = "hello"; String str2 = "hello"; String str3 = new String("hello"); String str4 = new String("hello"); System.out.println(str1 == str2); System.out.println(str3 == str4); System.out.println(str1 == str3); } }
运行结果:
大家看到运行结果后,可能会想这样一个问题,为什么 str1 == str2 是 true,而 str3 == str4 却是false 呢?
这就涉及到了常量池的概念,学习了常量池,这个问题也就迎刃而解了
在Java程序中,为了让程序的运行速度更快、更节省空间,Java 为 8 种基本数据类型和 String类都提供了常量池
常量池,肯定是存放常量的,那究竟什么是池了?
给大家举一个例子,大家也就明白了
比如: 小和尚打水
情况一:假设寺庙里面没有水缸,每次小和尚需要水的时候都要下山去打水
情况二:假设寺庙里面有水缸, 水缸里面打满了水,每次小和尚需要水的时候直接去水缸中取
情况二,就是池化技术的一种示例,水放在水缸中,随用随取,效率非常高
为了让程序的运行速度更快、更节省空间,Java中引入了:
Class文件常量池:每个 .Java 源文件编译后生成 .class 文件中都会保存当前类中的字面常量以及符号信息
运行时常量池:在 .class 文件被加载时,.class 文件中的常量池被加载到内存中称为运行时常量池,运行时常量池每个类都有一份
字符串常量池
我们本期讲的字符串所以主要讲的是字符串常量池
1.2 字符串常量池
字符串常量池 在 JVM 中是 StringTable 类,实际是一个固定大小的 HashTable,在不同 JDK 版本下字符串常量池的位置以及默认大小是不同的:
1.3 再谈创建对象
现在大多数人用的 jdk 版本都是 java8,那我们就拿 java8 来详谈
1.直接使用字符串常量进行赋值
public class Test { public static void main(String[] args) { String str1 = "hello"; String str2 = "hello"; System.out.println(str1 == str2); } }
运行结果:
分析图:
"hello" 它是一个字符串常量,所以它需要放入字符串常量池。因为我们用的 jdk 版本都是 java8,所以它字符串常量池的位置在堆中。我们还知道双引号引起来的字符串是字符串对象,并且字符串在内存中是按数组存放的,所以编译器会自动创建一个字符串对象,并指向这个按数组存放的字符串。因为它是字符串常量所以需要放入字符串常量池中,但是不会在字符串常量池中直接存入编译器自动生成的字符串对象的地址,而是借助一个叫哈希桶的工具去存放这个字符串对象的地址,然后把哈希桶的地址存放在字符串常量池中。
当执行 str1="hello" 的时候直接把 "hello" 放进了字符串常量池中。当执行到 str2 = "hello" 的时候会去字符串常量池中看有没有这个字符串常量,如果有直接存放它的字符串对象的地址,如果没有就去把这个字符串常量入池,然后在存放它的字符串对象的地址
注:只会开辟一块堆内存空间,保存在字符串常量池中,然后str共享常量池中的String对象
2.通过new创建String类对象
public class Test { public static void main(String[] args) { String str1 = new String("hello"); String str2 = new String("hello"); System.out.println(str1 == str2); } }
运行结果:
分析图:
编译器会自动为字符串常量生成一个字符串对象然后指向这个存储字符串的数组,然后利用哈希桶存放这个字符串对象的地址,将这个哈希桶的地址放入字符串常量池中。
String str1 = new String("hello") ,new 肯定会创建一个对象,这个对象 vlue 属性中存放的是"hello"这个字符串常量的数组地址,str1里面存放的就是这个new出来的对象地址。
String str2 = new String("hello") ,new 肯定会创建一个对象,这个对象 vlue 属性中存放的是"hello"这个字符串常量的数组地址,str2里面存放的就是这个new出来的对象地址。
注:只要是 new 就会产生一个新的对象 ,直接使用字符串常量进行赋值自动创建String类型对象的效率更高,而且更节省空间
学习完 直接使用字符串常量进行赋值 和 通过new创建String类对象,我们也就知道了上述提到的为什么 str1 == str2 是 true,而 str3 == str4 却是false 呢?
答:因为str1和str2是直接使用字符串常量进行赋值,str1和str2里面存储是编译器自动生成的字符串常量的对象,所以 str1 == str2 是 true。str3和str4是通过new创建String类对象,会分别创建一个对象,然后指向存储字符串的数组,str1和str2里面存储是通过new创建String类对象的地址,所以 str3 == str4 是 false
注:会开辟两块堆内存空间,字符串"hello"保存在字符串常量池中,然后用常量池中的String对象给新开辟 的String对象赋值
3.intern方法
intern 方法的作用是手动将创建的String对象添加到常量池中
①未手动将创建的String对象添加到常量池中
public class Test { public static void main(String[] args) { char[] arr = new char[]{'a','b','c'}; String str1 = new String(arr); String str2 = "abc"; System.out.println(str1==str2); } }
运行结果:
解析:将数组作为参数去实例化一个 String 对象,因为数组不是字符串常量,所以不会放入常量池,那么str1里面存储的就是实例化的那个String对象地址。str2="abc","abc"是个字符串常量所以会入常量池,str2里面存放的是编译器自动生成的字符串常量对象,所以当 str1==str2 返回的是false
②手动将创建的String对象添加到常量池中
public class Test { public static void main(String[] args) { char[] arr = new char[]{'a','b','c'}; String str1 = new String(arr); str1.intern(); String str2 = "abc"; System.out.println(str1==str2); } }
运行结果:
解析:手动将str1对象添加到常量池,我们知道字符串常量是用数组存储的,所以当我们String str2 = "abc"这个代码时,会先去常量池里面看有没有"abc"这个字符串常量,因为字符串常量是用数组存储的,所以常量池中有这个字符串常量那么就不会入池,str2直接存储指向"abc"地址的对象地址
2.字符串的不可变性
String是一种不可变对象,字符串中的内容是不可改变,字符串不可被修改
String类中的字符实际保存在内部维护的value字符数组中:
String类被final修饰,表明该类不能被继承
value被final修饰,表明value自身的值不能改变,即不能引用其它字符数组,但是其引用空间中的内容可以修改。
value被private修饰,表明value只能在本类使用,而且String类没有对外提供 getValue 和 setValue 的方法,所以在外部无法访问和修改value数组的内容
很多人会认为 value 不能被修改的原因是final,final修饰的变量不能改值,那下面我们就来证明value 不能修改跟 final 无关
证明 value 不能被修改跟 final 无关:
通过上面的代码也就证明了被 final 修饰的数组是可以修改的,因为被 final 修饰的不能改变里面存储的内容,arr 里面存储的是 new 一个数组对象的地址,那么 arr 也就不能修改成其他的数组对象地址了,但是数组的内容可以改变,因为 arr 里面存的是地址
注:final修饰类表明该类不想被继承,final修饰引用类型表明该引用变量不能引用其他对象,但是其引用对象中的内容是可以修改的。
为什么 String 要设计成不可变的?
方便实现字符串对象池,如果 String 可变,那么对象池就需要考虑写时拷贝的问题了
不可变对象是线程安全的
不可变对象更方便缓存 hash code, 作为 key 时可以更高效的保存到 HashMap 中.
3.字符串修改
尽量避免直接对String类型对象进行修改,因为String类是不能修改的,所有的修改都会创建新对象,效率非常低
public class Test { public static void main(String[] args) { String str = "abc"; str += "def"; System.out.println(str); } }
如果要对String类型对象修改建议尽量使用StringBuffer或者StringBuilder
3.1 StringBuilder
由于String的不可更改特性,为了方便字符串的修改,Java中又提供StringBuilder和StringBuffer类。这两个类大部分功能是相同的,这里介绍 StringBuilder常用的一些方法
StringBuff append(String str) :在尾部追加,相当于String的+=,可以追加:boolean、char、char[]、 double、float、int、long、Object、String、StringBuff的变量
rcharAt(int index) :cha获取index位置的字符
int length():获取字符串的长度
StringBuffer reverse():反转字符串
StringBuffer deleteCharAt(int index):删除index位置字符
StringBuilder 在修改字符串内容的时候会直接在该对象中修改,不会重新创建新对象:
画图解析:
解析:"abc"是一个字符串常量对象,所以它会入池,当把"abc"作为实例化 StringBuilder 的参数的时候,会自动创建一个比"abc"大一些的数组空间然后把abc拷贝进去,这个拷贝的数组不会入池。所以它可以直接在改变里面的值,也不会创建一个新的对象。当"abc"没有使用的时候编译器会自动回收
StringBuffer和StringBuilder的区别:StringBuffer采用同步处理,属于线程安全操作。StringBuilder没有采用同步处理,属于线程不安全操作。这两个类大部分功能是相似的(更详细的内容会放在多线程部分讲解)