[TOC]
字符串就是一连串的字符序列, Java 提供了 String,StringBuilder,StringBuffer 三个类来封装字符串
String
String 类是不可变类, String 对象被创建以后, 对象中的字符序列是不可改变的, 直到这个对象被销毁
为什么是不可变的
- jdk1.8
- public final class String
- implements java.io.Serializable, Comparable<String>, CharSequence {
- /** The value is used for character storage. */
- private final char value[];
- //jdk1.9 中将 char 数组替换为 byte 数组, 紧凑字符串带来的优势: 更小的内存占用, 更快的操作速度.
- // 构造函数
- public String(String original) {
- this.value = original.value;
- this.hash = original.hash;
- }
- // 构造函数
- public String(char value[]) {
- this.value = Arrays.copyOf(value, value.length);
- }
- // 返回一个新的 char[]
- public char[] toCharArray() {
- // Cannot use Arrays.copyOf because of class initialization order issues
- char result[] = new char[value.length];
- System.arraycopy(value, 0, result, 0, value.length);
- return result;
- }
- }
根据上面的代码, 我们看看 String 究竟是怎么保证不可变的.
String 类被 final 修饰, 不可被继承
string 内部所有成员都设置为私有变量, 外部无法访问
没有向外暴露修改 value 的接口
value 被 final 修饰, 所以变量的引用不可变.
char[]. 为引用类型仍可以通过引用修改实例对象, 为此
String(char value[])
构造函数内部使用的 copyOf 而不是直接将 value[]复制给内部变量 `.
在获取 value 时, 并没有将 value 的引用直接返回, 而是采用了 arraycopy()的方式返回一个新的 char[]
String 类中的函数也处处透露着不可变的味道, 比如: replace()
- public String replace(char oldChar, char newChar) {
- if (oldChar != newChar) {
- int len = value.length;
- int i = -1;
- char[] val = value; /* avoid getfield opcode */
- while (++i < len) {
- if (val[i] == oldChar) {
- break;
- }
- }
- if (i < len) {
- // 重新创建新的 char[], 不改变原有对象中的值
- char buf[] = new char[len];
- for (int j = 0; j < i; j++) {
- buf[j] = val[j];
- }
- while (i < len) {
- char c = val[i];
- buf[i] = (c == oldChar) ? newChar : c;
- i++;
- }
- // 最后返回新创建的 String 对象
- return new String(buf, true);
- }
- }
- return this;
- }
当然不可变也不是绝对的, 还是可以通过反射获取到变 value 引用, 然后通过 value[]修改数组的方式改变 value 对象实例
- String a = "Hello World!";
- String b = new String("Hello World!");
- String c = "Hello World!";
- // 通过反射修改字符串引用的 value 数组
- Field field = a.getClass().getDeclaredField("value");
- field.setAccessible(true);
- char[] value = (char[]) field.get(a);
- System.out.println(value);//Hello World!
- value[5] = '&';
- System.out.println(value);//Hello&World!
- // 验证 b,c 是否被改变
- System.out.println(b);//Hello&World!
- System.out.println(c);//Hello&World!
写到这里该如何引出不可变的好处呢? 忘记反射吧, 我们聊聊不可变的好处吧
不可变的优点
保证了线程安全
同一个字符串实例可以被多个线程共享.
保证了基本的信息安全
比如, 网络通信的 IP 地址, 类加载器会根据一个类的完全限定名来读取此类诸如此类, 不可变性提供了安全性.
字符串缓存 (常量池) 的需要
具统计, 常见应用使用的字符串中有大约一半是重复的, 为了避免创建重复字符串, 降低内存消耗和对象创建时的开销. JVM 提供了字符串缓存的功能 -- 字符串常量池. 如果字符串是可变的, 我们就可以通过引用改变常量池总的同一个内存空间的值, 其他指向此空间的引用也会发生改变.
支持 hash 映射和缓存.
因为字符串是不可变的, 所以在它创建的时候 hashcode 就被缓存了, 不需要重新计算. 这就使得字符串很适合作为 Map 中的键, 字符串的处理速度要快过其它的键对象. 这就是 HashMap 中的键往往都使用字符串.
不可变的缺点
由于它的不可变性, 像字符串拼接, 裁剪等普遍性的操作, 往往对应用性能有明显影响.
为了解决这个问题, java 为我们提供了两种解决方案
字符串常量池
StringBuilder,StringBuffer 是可变的
字符串常量池
还是刚才反射的示例
- String a = "Hello World!";
- String b = new String("Hello World!");
- String c = "Hello World!";
- // 判断字符串变量是否指向同一块内存
- System.out.println(a == b);
- System.out.println(a == c);
- System.out.println(b == c);
- // 通过反射观察 a, b, c 三者中变量 value 数组的真实位置
- Field a_field = a.getClass().getDeclaredField("value");
- a_field.setAccessible(true);
- System.out.println(a_field.get(a));
- Field b_field = b.getClass().getDeclaredField("value");
- b_field.setAccessible(true);
- System.out.println(b_field.get(b));
- Field c_field = c.getClass().getDeclaredField("value");
- c_field.setAccessible(true);
- System.out.println(c_field.get(c));
- // 通过反射发现 String 对象中变量 value 指向了同一块内存
输出
- false
- true
- false
- [C@6f94fa3e
- [C@6f94fa3e
- [C@6f94fa3e
字符串常量的创建过程:
判断常量池中是否存在 "Hello World!" 常量, 如果有直接返回该常量在池中的引用地址
如果没有, 先创建一个
char["Hello World!".length()]
数组对象, 然后在常量池中创建一个字符串对象并用数组对象初始化字符串对象的成员变量 value, 然后将这个字符串的引用返回, 比如赋值给 a
由此可见, a 和 b 对象指向常量池中相同的内存空间不言自明.
而 b 对象的创建是建立在以上的创建过程的基础之上的. "Hello World!" 常量创建完成时返回的引用, 会经过 String 的构造函数.
- public String(String original) {
- this.value = original.value;
- this.hash = original.hash;
- }
构造函数内部将引用的对象成员变量 value 赋值给了内部成员变量 value, 然后将新创建的字符创对象引用赋值给了 b, 这个过程发生在堆中.
再来感受下下面这两行代码有什么区别
- String b = new String(a);
- String b = new String("Hello World!");
StringBuilder 和 StringBuffer
二者都是可变的
为了弥补 String 的缺陷, Java 先后提供了 StringBuffer 和 StringBuilder 可变字符串类.
二者都继承至 AbstractStringBuilder,AbstractStringBuilder 使用了 char[] value 字符数组
- abstract class AbstractStringBuilder implements Appendable, CharSequence {
- /**
- * The value is used for character storage.
- */
- char[] value;
- AbstractStringBuilder(int capacity) {
- value = new char[capacity];
- }
- }
可以看出 AbstractStringBuilder 类和其成员变量 value 都没有使用 final 关键字.
value 数组的默认长度
StringBuilder 和 StringBuffer 的 value 数组默认初始长度是 16
- public StringBuilder() {
- super(16);
- }
- public StringBuffer() {
- super(16);
- }
如果我们拼接的字符串长度大概是可以预计的, 那么最好指定合适的 capacity, 避免多次扩容的开销.
扩容产生多重开销: 抛弃原有数组, 创建新的数组, 进行 arrycopy.
二者的区别
StringBuilder 是非线程安全的, StringBuffer 是线程安全的.
StringBuffer 类中的方法使用了 synchronized 同步锁来保证线程安全. 关于锁的话题非常大, 会单独成文来说明, 这里推荐一篇不错的博客, 有兴趣的可以看看
JVM 源码分析之 synchronized 实现 https://www.jianshu.com/p/c5058b6fe8e5
来源: https://juejin.im/post/5c2759d3e51d45176760e22c