【Java】String
前言
几乎所有语言都没有提供字符串这个概念,而是使用字符数组来描述字符串。Java语言严格来说也是没有字符串的。然而在开发时会经常使用到字符串,于是Java为了应对便创建了String类这个字符串类。使用双引号""
定义的内容都为字符串。理解Java的String需要从类和内存关系上分析这个类。
String类对象的两种实例化方式
String name1 = "Sakura"; //直接赋值方式
String name2 = new String("Sakura"); //利用构造方法实例化
使用"=="和equals()比较字符串是否相等
使用==
比较的是两个对象在内存中的地址是否一致,也就是比较两个对象是否为同一个对象。
使用equals()
方法比较的是对象的内容是否相等,name1和name2所指对象的值都是"Sakura"所以输出为true。equals()
方法源自Object类,Java为很多自带类都重写了该方法。自定义的类如果要用该方法来判断对象是否相等则需要自己重写该方法以保证判断行为的正确性,否则使用equals()
的效果只会等同于==
。
String常量为匿名对象
像"Sakura"这样的字符串不属于基本数据类型,而是作为String类的匿名对象而存在。
验证"Sakura"字符串是否为匿名对象:
"Sakura"可以调用String类的方法,由此可见"Sakura"是一个对象。
创建String对象的直接赋值方式相当于为一个匿名对象设置了一个名字。在前篇文章里我们说直接使用new 类名称();
的方法创建的是一个匿名对象。在这里我们可以看到,没有使用new关键字就创建了String的匿名对象。String类的匿名对象是由Java自动生成不是由用户直接创建。
上图中的代码实际隐含了一个避免出现NullPointerException
的小技巧。
若是我们像下面这样写字符串比较代码:
我们不知道name1是否指向了一个对象,所以这将存在抛出空指针异常的情况。为了避免空指针异常,我们就可以将字符串常量写在前面。
两种实例化方式的区别
直接赋值方式
前面讲过直接赋值方式就是将一个字符串的匿名对象设置了一个名字。
==
比较的是两个对象内存地址是否一致,由输出结果可以看出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垃圾回收器清理越耗时。
虽然使用这种方式连接字符串,从分析堆栈图来看很费空间和耗时。但是“聪明”的编译器会不会优化呢?
我们反编译下面程序来观察一下: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是线程不安全的。
查看源码,可发现StringBuffer在其每个方法前都加了synchronized关键字(Java内置同步机制)。如append()
方法:
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
因为StringBuilder是线程不安全的,所以一般用在单线程,因为其不需要管理线程同步这些问题所以速度会比StringBuffer快。
小结
本文介绍了String的各种相关问题,概括如下:
String对象使用equals()的比较对象是否相等以及使用"=="判断对象是否同一对象。String对象的两种实例化方式,直接赋值不会产生垃圾空间,并且会存入常量池中,构造方法会产生中间垃圾对象且不会入池。String对象是一个不可变对象,String类对象内容改变是依靠引用关系变化,实际对象并没有发生任何变化。若是经常改变字符串值需要使用StringBuilder或者StringBuffer。
参考:
[1] Eckel B. Java编程思想(第四版)[M]. 北京: 机械工业出版社, 2007