前言
实际上任何语言都没有提供字符串这个概念,而是使用字符数组来描述字符串。Java里面严格来说也是没有字符串的,在所有的开发里面字符串的应用有很多,于是Java为了应对便创建了String类这个字符串类。使用""定义的内容都是字符串,理解Java的String类需要从类的角度和内存关系上分析这个类。
下面将介绍:
- String类对象的两种实例化方式
- 使用"=="和equals比较字符串是否相等
- String常量为匿名对象
- String两种实例化方式的区别
- 字符串一旦定义则不可变
- 字节与字符串
- 字符串中的方法分类
- 重载"+"与StringBuilder
- StringBuffer与StringBuilder
String类对象的两种实例化方式
String name1 = "Sakura"; //直接赋值方式
String name2 = new String("Sakura"); //利用构造方法实例化
使用"=="和equals比较字符串是否相等
![](https://img2018.cnblogs.com/blog/1099419/201811/1099419-20181118181605694-457490419.png)
使用"=="比较的是两个对象在内存中的地址是否一致,也就是比较两个对象是否为同一个对象。
使用equals()方法比较的是对象的值是否相等,name1和name2所指对象的值都是"Sakura"所以输出为true。
String常量为匿名对象
像"Sakura"这样的字符串不属于基本数据类型,而是作为String类的[匿名对象](https://www.cnblogs.com/myworld7/p/9977000.html)而存在。
验证"Sakura"字符串是否为匿名对象:
![](https://img2018.cnblogs.com/blog/1099419/201811/1099419-20181118181653010-1754116119.png)
"Sakura"可以调用String类的方法,由此可见"Sakura"是一个对象。
创建String对象的直接赋值方式相当于将一个匿名对象设置了一个名字。在前篇文章里我们说直接使用"new 类名称();"的方法创建的是一个匿名对象,String类的匿名对象却不是这样的,String类的匿名对象是由系统自动生成不是由用户直接创建。
下面会讲JVM中关于字符串的内存分配管理。
Notice |
图中的代码实际隐含了一个避免出现NullPointerException的小技巧。
若是我们像下面这样写字符串比较代码:
![](https://img2018.cnblogs.com/blog/1099419/201811/1099419-20181118181813068-27980081.png)
未知name1是否指向了一个对象,所以会存在抛出空指针异常的情况。为了避免空指针异常我们就可以将字符串常量写在前面。
两种实例化方式的区别
直接赋值方式
前面讲过直接赋值方式就是将一个字符串的匿名对象设置了一个名字。
![](https://img2018.cnblogs.com/blog/1099419/201811/1099419-20181118181846255-806130668.png)
"=="比较的是两个对象内存地址是否一致,由输出结果可以看出name1和name2指向了同一块堆空间。
为什么不是在堆空间中开辟两个"Sakura"对象而是让name1和name2指向同一个对象呢?
这个需要谈到JVM的共享设计模式。
JVM的底层实现实际上在堆中存在一个对象池(常量池,不一定只保存String对象),当我们使用直接赋值方式定义String类对象,那么JVM会将此字符串对象使用的匿名对象就是如"Sakura"字符串入池保存。如果后面还有其他String对象采用同样方式且设置同样内容时,将不会开辟新的堆空间,而是继续使用相同的空间。
采用构造方法实例化
String name = new String("Sakura");
分析以上语句开辟空间情况:
开辟了一块栈内存存储了对象引用; 开辟了两块堆空间,一块在常量池中存储"Sakura"字符串常量另一块在堆中存储这个对象。
当堆中的对象若是没有引用指向就是垃圾对象会被GC清理掉。所以,这种构造方式会造成一块堆空间的浪费。
若是希望,此方式的对象也可以入池保存也是有方法的,就是利用String类的intern方法。
public class Test {
public static void main(String[] args) {
String name1 = new String("Sakura").intern(); //返回一个匿名对象 name1就指向的是常量池中的"Sakura"
String name2 = "Sakura";
System.out.println(name1==name2);
}
}
/*
output:
true
*/
但是方法真的显得很麻烦!
总结一下两种实例化方法的区别:
- 直接赋值方式:只会开辟一块堆内存空间,并且自动保存在常量池中,以供我们下次重复使用。
- 构造方法:会开辟两块堆内存空间,其中在常量池的会成为垃圾空间。
字符串一旦定义便不可变
String name = "Amy";
String name = "Smith";
String name = "Amy" + "Smith";
Java定义了String内容不能被改变。分析堆内存,是可以知道字符串对象内容的改变是利用了引用关系的变化而实现的。每一次的改变都会产生垃圾空间,所以不要频繁更改定义好的字符串。
字节与字符串
查看API可以看见有许多关于字节的方法。字节使用byte描述,是Java中的基本数据类型之一,使用字节一般主要用于数据的传输或者进行编码转换的时候使用。在String中有许多将字符串转换为字节数组的操作,目的就是为了传输转换。
字符串中的方法分类
在程序开发中对字符串的操作是常事的,那么在Java中对字符串操作方法也是有很多的。主要分为下面几类,关于每种方法的使用可以查看API,但是最好还是几乎要掌握完。
- 比较方法
- 查找方法
- 替换方法
- 截取方法
- 拆分方法
重载"+"与StringBuilder
Java中不允许程序员重载任何操作符,但是Java内部重载了两个用于String类的操作符"+"和"+="。操作符"+"可以用于连接字符串,操作符"+="用于将连接后的字符串再次赋给原字符串引用。
由前面所知,不断使用"+="连接,会产生很多的中间垃圾对象,而且连接的越多也就越浪费空间和时间。垃圾对象占用空间,Java垃圾回收器清理越耗时。
虽然使用这种方式连接字符串,从分析堆栈图来看很费空间和耗时。但是,JVM在运行程序会不会给优化呢。我们反编译下面程序来观察一下:
javap -c StringContact
public class StringContact {
public static void main(String[] args) {
String str1="hello";
String str2="Sakura"+str1+"!";
System.out.println(str2);
}
}
/*
output:
Sakurahello!
*/
可以看出:编译器自动引入了java.lang.StringBuilder类。编译器先自动创建了一个StringBuilder对象,每次字符串连接时调用StringBuilder的append()方法,调用了两次。最后,调用toString()生成最终的字符串,存在str2中使用命令astroe_2。
编译器自主使用StringBuilder类,因为它更高效。StringBuilder对象含有一个缓冲区来处理字符串,所以可以修改删除字符串。在上面代码中创建了一个StringBuilder对象,连接字符串时只是不断调用其append方法,没有创建反复创建对象。
使用下面的例子深入看看编译器的优化程度:
public class CompareStringBuilder {
public String implicit(String[] fields) { //使用String隐式的字符串连接
String result="";
for(int i=0; i<fields.length; i++)
result += fields[i];
return result;
}
public String explicit(String[] fields) { //使用StringBuilder的append方法连接字符串
StringBuilder result=new StringBuilder();
for(int i=0; i<fields.length; i++)
result.append(fields[i]);
return result.toString();
}
}
反编译上述程序:
若是不满足Code 8的大于等于循环次数的话,那么Code 5到Code 35就是一个循环,并且在每一次循环中StringBuilder对象都会被创建。
可以看出这个代码只是在最初创建了一次StringBuilder对象,并且在循环中是一直使用append()方法修改字符串。
以上两段代码可以看出编译器对我们代码的优化程度,字符串较简单时可以直接使用String,若是需要大量连接则需要可考虑StringBuilder类。
StringBuilder与StringBuffer
与String对象比较StringBuffer是一个可变的对象,可以通过其自带的某些方法改变其值的长度和内容。如使用append()方法追加字符串。
同StringBuilder一样,StringBuffer对字符串的修改效率要高于String。
查看JDK文档可知,StringBuilder是在JAVA 5中提出,与StringBuffer拥有的方法几乎相似,可以看成StringBuffer一种“替换”形式。二者的主要区别是,StringBuffer是线程安全的,而StringBuilder是线程不安全的。
查看JDK源码,可以发现StringBuffer在其每个方法前都加了synchronized关键字(用于多线程线程同步)
如append方法
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
因为StringBuilder是线程不安全的,所以一般用在单线程,因为其不需要管理线程同步这些问题所以速度会比StringBuffer快。
小结
本文主要介绍了:
String对象使用equals的比较对象是否相等以及使用"=="判断对象是否同一对象;String对象的两种实例化方式,直接赋值不会产生垃圾空间,并且会存入常量池中,构造方法会产生中间垃圾对象且不会入池;String对象是一个不可变对象,String类对象内容改变是依靠引用关系变化,实际对象并没有发生任何变化;若是经常改变字符串值需要使用StringBuilder或者StringBuffer。
部分内容参考自《Java编程思想》(第四版)