5、String、StringBuffer、StringBuilder

String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的 Immutable 类(不可变类),被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。

StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,它是 Java 1.5 中新增的,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是 StringBuilder。

StringBuilder 在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选线程不安钱

要知道String 是 Immutable 的,字符串操作不当可能会产生大量临时字符串,以及线程安全方面的区别。

还要关注几个地方:

1.通过 String 和相关类,考察基本的线程安全设计与实现,各种基础编程实践。

2.考察 JVM 对象缓存机制的理解以及如何良好地使用。

3.考察 JVM 优化 Java 代码的一些技巧。

4.String 相关类的演进,比如 Java 9 中实现的巨大变化。

1. 字符串设计和实现考量

String 是 Immutable 类的典型实现,原生的保证了基础线程安全,因为你无法对它内部数据进行任何修改,这种便利甚至体现在拷贝构造函数中,由于不可变,Immutable 对象在拷贝时不需要额外复制数据。如调用函数时的值传递

StringBuffer,它的线程安全是通过把各种修改数据的方法都加上 synchronized 关键字实现的,非常直白。其实,这种简单粗暴的实现方式,非常适合我们常见的线程安全类实现,不必纠结于 synchronized 性能之类的,有人说“过早优化是万恶之源”,考虑可靠性、正确性和代码可读性才是大多数应用开发最重要的因素。

为了实现修改字符序列的目的,StringBuffer 和 StringBuilder 底层都是利用可修改的数组(char数组,JDK 9 以后是 byte数组),二者都继承了 AbstractStringBuilder,里面包含了基本操作,区别仅在于最终的方法是否加了 synchronized。

(byte数组:字节数组,如int类型可以表示为byte[4],int类型为4个字节,byte数组的一项为一个字节,可以表示8bit位,也就是最大是-128-127范围的有符号整数)

(char占两个字节,大小为0-65535,为16bit位的无符号类型,不能表示负数)

(char可以表示中文字符,byte不可以)

这个内部数组应该创建成多大的呢?如果太小,拼接的时候可能要重新创建足够大的数组;如果太大,又会浪费空间。目前的实现是,构建时初始字符串长度加 16(这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是 16)。我们如果确定拼接会发生非常多次,而且大概是可预计的,那么就可以指定合适的大小,避免很多次扩容的开销。扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行 arraycopy。

如:

String strByConcat = "aa" + "bb" + "cc" + "dd";

反编译后回发现,在JDK8中,字符串拼接操作会自动被 javac 转换为 StringBuilder 操作。而在 JDK 9 里面则是因为 Java 9 为了更加统一字符串操作优化,提供了 StringConcatFactory,作为一个统一的入口。javac 自动生成的代码,虽然未必是最优化的,但普通场景也足够了,你可以酌情选择。

2. 字符串缓存(常量池)

如果能避免创建重复字符串,可以有效降低内存消耗和对象创建开销。

String 在 Java 6 以后提供了 intern() 方法,目的是提示 JVM 把相应字符串缓存起来,以备重复使用。在我们创建字符串对象并调用 intern() 方法的时候,如果已经有缓存的字符串,就会返回缓存里的实例,否则将其缓存起来。一般来说,JVM 会将所有的类似“abc”这样的文本字符串,或者字符串常量之类缓存起来。

一般使用 Java 6 这种历史版本,并不推荐大量使用 intern,因为被缓存的字符串是存在所谓 PermGen 里的,也就是臭名昭著的“永久代”,这个空间是很有限的,也基本不会被 FullGC 之外的垃圾收集照顾到。所以,如果使用不当,OOM 就会光顾。在后续版本中,这个缓存被放置在堆中(常量池移到堆中),这样就极大避免了永久代占满的问题,甚至永久代在 JDK 8 中被 MetaSpace(元数据区)替代了。而且,默认缓存大小也在不断地扩大中。

Intern 是一种显式地排重机制,但是它也有一定的副作用,因为需要开发者写代码时明确调用,一是不方便,每一个都显式调用是非常麻烦的;另外就是我们很难保证效率,应用开发阶段很难清楚地预计字符串的重复情况,有人认为这是一种污染代码的实践。


由于String在Java世界中使用过于频繁,Java为了避免在一个系统中产生大量的String对象,引入了字符串常量池。其运行机制是:创建一个字符串时,首先检查池中是否有值相同的字符串对象,如果有则不需要创建直接从常量池中刚查找到的对象引用;如果没有则新建字符串对象,返回对象引用,并且将新创建的对象放入常量池中。但是,通过new方法创建的String对象是不检查字符串常量池的,而是直接在堆区或栈区创建一个新的对象,也不会把对象放入常量池中。上述原则只适用于通过直接量给String对象引用赋值的情况。

举例:String str1 = "123"; //通过直接量赋值方式,放入字符串常量池
String str2 = new String(“123”);//通过new方式赋值方式,不放入字符串常量池,放在堆中

注意:String提供了inter()方法。调用该方法时,如果常量池中包括了一个等于此String对象的字符串(由equals方法确定,内容相同),则返回常量池中的字符串。否则,将此String对象添加到常量池中,并且返回此池中对象的引用。

几张图轻松理解String.intern():

https://blog.csdn.net/soonfly/article/details/70147205

3.String 自身的演化

在历史版本中,String是使用 char 数组来存数据的,这样非常直接。但是 Java 中的 char 是两个 bytes 大小,拉丁语系语言的字符,根本就不需要太宽的 char,这样无区别的实现就造成了一定的浪费。

在 Java 9 中,引入了 Compact Strings 的设计,对字符串进行了大刀阔斧的改进。将数据存储方式从 char 数组,改变为一个 byte 数组加上一个标识编码的所谓 coder,并且将相关字符串操作类都进行了修改。另外,所有相关的 Intrinsic 之类也都进行了重写,以保证没有任何性能损失。虽然底层实现发生了这么大的改变,但是 Java 字符串的行为并没有任何大的变化,所以这个特性对于绝大部分应用来说是透明的,绝大部分情况不需要修改已有代码。在通用的性能测试和产品实验中,我们能非常明显地看到紧凑字符串带来的优势,即更小的内存占用、更快的操作速度。

猜你喜欢

转载自www.cnblogs.com/xuan5301215/p/9072739.html
今日推荐