String
这个类是我们在写 Java 代码中用得最多的一个类,没有之一,今天我们就讲讲它。
简介
String
并不是基本数据类型,而是一个对象,并且是不可变的对象。
查看源码就会发现 String
类为final
型的(当然也不可被继承),而且通过查看 JDK 文档会发现几乎每一个修改 String
对象的操作,实际上都是创建了一个全新的 String
对象。
String
类实现了 Serializable
,Comparable
,CharSequence
接口,被 final
修饰。内部维护了一个字符数组。
常用方法
字符串比较
1 | == // 判断内容与地址是否相同 |
字符串查找
1 | char charAt(int index) // 返回索引上的字符,索引从0开始 |
字符串截取与替换
1 | String substring(int beginIndex) // 返回一个新的字符串,是从beginIndex开始到length-1的串。 |
字符串其他常用方法
1 | int length() // 获取字符串长度 |
创建方式
第一种方式是在常量池中直接拿对象,第二种是在堆内存空间创建一个新的对象。只要使用new方法,就需要创建新的对象。
1 | String str = new String("Freya"); // 这句话创建了两个对象。 |
首先,字符串“Freya”放入常量池,然后 new 了一个字符串“Freya”放入 Java 堆。字符串常量”Freya”在编译期就已经确定放入常量池,而 Java 堆上的”Freya”是在运行期初始化阶段才确定。然后 Java 栈中的 str 指向了 Java 堆中的 “Freya”。
字符串常量池
字符串常量池是 JVM 实例全局共享的,全局只有一个。字符串常量池及到一个设计模式,叫“享元模式”,顾名思义 就是共享元素模式。也就是说:一个系统中如果有多处用到了相同的一个元素,那么我们应该只存储一份此元素,而让所有地方都引用这一个元素。
- 直接使用双引号声明的 String 对象直接存储在字符串常量池。
- 不是双引号声明的,可以使用
String.intern()
方法,这是一个Native方法。如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用。
1 | public static void main(String[] args) { |
注意:尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变,可以使用StringBuilder或者StringBuffer。
String 对象的不可变性
打开 String
类源码,可以看到一句话:
Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings. Because String objects are immutable they can be shared.
意思是 String
是个常量,创建之后就是不可变的。不可变的意思是不能改变对象内的成员变量,包括基本数据类型变量的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。
String 对象不可变性是如何实现的?
从JDK 9之后,value的类型由 char[] 数组变成了 byte[] 数组。目的是为了节省空间,提高String的性能。
1 | public final class String |
从源码可以看出:
- String 类被 final 修饰,不可继承。
- String内部所有成员都设置为私有变量。
- 不存在value的setter方法。
- 将value和offset设置为final。
- 当传入可变数组value[]时,进行copy而不是直接将value[]复制给内部变量。
- 获取value时不是直接返回对象引用,而是返回对象的copy。
String 对象不可变性的优缺点
优点
字符串常量池的需要
字符串常量池可以将一些字符常量放在常量池中重复使用,避免每次都重新创建相同的对象、节省存储空间。但如果字符串是可变的,此时相同内容的String还指向常量池的同一个内存空间,当某个变量改变了该内存的值时,其他遍历的值也会发生改变。所以不符合常量池设计的初衷。线程安全考虑
同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载
譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。
支持hash映射和缓存
因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
缺点
如果有对String对象值改变的需求,那么会创建大量的String对象。
String对象是否真的不可变
虽然 String 对象将 value 设置为 final,并且还通过各种机制保证其成员变量不可改变。但是还是可以通过反射机制的手段改变其值。例如:
1 | public static void main(String[] args) { |
打印结果为:
1 | s = Hello World |
其他相关内容
substring 方法
在 JDK 1.6中,当调用 substring 方法的时候,会创建一个新的 String 对象,但是这个 String 的值仍然指向堆中的同一个字符数组。这两个对象中只有 count 和offset 的值是不同的。如果你有一个很长很长的字符串,但是当你使用 substring 进行切割的时候你只需要很短的一段。这可能导致性能问题,因为你需要的只是一小段字符序列,但是你却引用了整个字符串(因为这个非常长的字符数组一直在被引用,所以无法被回收,就可能导致内存泄露)。在JDK 1.6中,一般用以下方式来解决该问题,原理其实就是生成一个新的字符串并引用他。
1 | x = x.substring(x, y) + "" |
以上问题在jdk 7+中得到解决。在jdk 7+ 中,substring方法会在堆内存中创建一个新的数组。其使用new String
创建了一个新字符串,避免对老字符串的引用。从而解决了内存泄露问题。
String.valueOf和Integer.toString的区别
我们有三种方式将一个int类型的变量变成呢过String类型,那么他们有什么区别?
1 | int i = 5; |
replaceFirst、replaceAll、replace 的区别
1 | public static void main(String[] args) { |
- replace(CharSequence target, CharSequence replacement) ,用 replacement 替换所有的 target ,两个参数都是字符串。
- replaceAll(String regex, String replacement) ,用 replacement 替换所有的 regex 匹配项,regex 很明显是个正则表达式,replacement 是字符串。
- replaceFirst(String regex, String replacement) ,基本和 replaceAll 相同,区别是只替换第一个匹配项。
注意
replace 替换的只能是字符或字符串形式。
replaceAll() 和 replaceFirst() 是基于正则表达式的替换。
replaceAll() 和 replace() 是替换所有的,而 replaceFirst() 仅替换第一次出现的。
如果 replaceAll() 和 replaceFirst() 所用的参数据不是基于正则表达式的,则与 replace() 替换字符串的效果是一样的。
执行了替换操作后,源字符串的内容是没有发生改变的。
switch 对字符串的支持
switch 中只能使用整型,其他数据类型都是转换成整型之后在使用switch的。比如byte
、short
、char
(ASCII码是整型)以及int
。
字符串进行switch
的实际是哈希值,然后通过使用 equals 方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行 switch 或者使用纯整数常量,但这也不是很差。因为Java编译器只增加了一个equals
方法,如果你比较的是字符串字面量的话会非常快。例如:
1 | public class switchDemoString { |
对代码进行反编译:
1 | public class switchDemoString |