String

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

  • 字符串转换是通过方法来实现toString
  • 字符串不变; 它们的值在创建后不能被更改。 字符串缓冲区支持可变字符串。 因为String对象是不可变的,它们可以被共享
  • 除非另有说明,否则传递null参数到此类中的构造函数或方法将导致抛出NullPointerException

 

StringBuffer

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

  • 线程安全,可变的字符序列。 字符串缓冲区就像一个String ,但可以修改。 在任何时间点,它包含一些特定的字符序列,但可以通过某些方法调用来更改序列的长度和内容。
  • 字符串缓冲区可以安全地被多个线程使用。 这些方法在必要时进行同步,以便任何特定实例上的所有操作都按照与所涉及的各个线程所执行的方法调用顺序一致的顺序发生。
  • StringBuffer的主要StringBufferappendinsert方法,它们被重载以便接受任何类型的数据。 每个都有效地将给定的数据转换为字符串,然后将该字符串的字符附加或插入到字符串缓冲区。 append方法总是在缓冲区的末尾添加这些字符; insert方法将insert添加到指定点。sb.append(x)具有相同的效果sb.insert(sb.length(), x)
  • 每当涉及源序列(例如从源序列追加或插入)的操作发生时,该类仅在执行操作的字符串缓冲器上进行同步,而不在源上。 请注意,虽然StringBuffer被设计为可以安全地从多个线程并发使用,但如果构造函数或appendinsert操作被传递通过线程共享的源序列,则调用代码必须确保该操作具有一致且不变的视图在操作期间的源序列。 呼叫者通过使用不可变的源序列,或者不跨线程共享源序列,可以在呼叫期间持有锁来满足这一点。
  • 每个字符串缓冲区都有一个容量。 只要字符串缓冲区中包含的字符序列的长度不超过容量,就不必分配新的内部缓冲区数组。 如果内部缓冲区溢出,则会自动变大。
  • 除非另有说明,否则将null参数传递给null中的构造函数或方法将导致抛出NullPointerException 。

StringBuilder

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

  • StringBuilder的主要StringBuilderappendinsert方法,它们是重载的,以便接受任何类型的数据。 每个都有效地将给定的数据转换为字符串,然后将该字符串的字符附加或插入字符串构建器。 append方法始终在构建器的末尾添加这些字符; insert方法将insert添加到指定点。sb.append(x)具有相同的效果sb.insert(sb.length(), x)
  • 每个字符串构建器都有一个容量。 只要字符串构建器中包含的字符序列的长度不超过容量,则不需要分配新的内部缓冲区。 如果内部缓冲区溢出,则会自动变大。
  • StringBuilder的StringBuilder不能安全使用多线程。 如果需要同步, 那么建议使用StringBuffer 。
  • 除非另有说明,否则将null参数传递给null中的构造函数或方法将导致抛出NullPointerException 。

字符串设计和实现考量

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

StringBuffer 实现的一些细节,它的线程安全是通过把各种修改数据的方法都加上 synchronized 关键字实现的,非常直白。其实,这种简单粗暴的实现方式,非常适合我们常见的线程安全类实现,

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

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

 

在JDK1.8当中执行以下代码:

public class StringConcat {
     public static void main(String[] args) {
           String myStr = "aa" + "bb" + "cc" + "dd";
           System.out.println("My String:" + myStr);
     }
}

利用   javac 类名   进行编译

利用   Java 类名     进行编译

利用   javap -v 类名  进行反编译

Java string区掉后缀匹配 java string stringbuff_字符串

从这里面看出在JDK1.8里面,编译器会自动把String转化为StringBuilder进行字符串的追加,

字符串缓存/常量池

实现:字符串池是使用一个拥有固定容量的 HashMap 每个元素包含具有相同 hash 值的字符串列表,最初的 大小1009

String 在 Java 6 以后提供了 intern() 方法,目的是提示 JVM 把相应字符串缓存起来,以备重复使用。String类维护一个初始为空的字符串的对象池,当我们创建字符串对象并调用 intern() 方法的时候,如果对象池中已经包含这一个相等的字符串对象则返回对象池中的实例,否则添加字符串到对象池并返回该字符串的引用。一般来说,JVM 会将所有的类似“abc”这样的文本字符串,或者字符串常量之类缓存起来。

但是,一般使用 Java 6 这种历史版本,并不推荐大量使用 intern,为什么呢?魔鬼存在于细节中,被缓存的字符串是存在所谓 PermGen 里的,也就是“永久代”,这个空间是很有限的,也基本不会被 FullGC 之外的垃圾收集照顾到。所以,如果使用不当,OOM 就会光顾。

在后续版本中,这个缓存被放置在堆中,这样就极大避免了永久代占满的问题,甚至永久代在JDK 8 中被 MetaSpace(元数据区)替代了。而且,默认缓存大小也在不断地扩大中。字符串常量池的表是保存在原生内存中的,它是一个大小固定的Hashtable。在java7u40之前的版本中,这个表默认有1009个桶,平均而言,在因为链接而出现冲突之前,预计可以保存500个字符串。在64位版本的java7u40及更新的版本中,默认大小为60013。

可以使用下面的参数直接打印具体数字,可以拿自 己的 JDK 立刻试一下

-XX:+PrintStringTableStatistics

在 Java 7 和 8 中使用 -XX:StringTableSize 来设置字符串池 Map 的大小。它是固定的,因为它使用 HashMap 实现。近似于你应用单独的字符串个数(你希望保留的)并且设置池的大小为最接近的质数并乘以 2 (减少碰撞的可能性)。它是的String.intern 可以使用相同(固定)的时间并且在每次插入时消耗更小的内存(同样的任务,使用java WeakHashMap将消耗4-5倍的内存)。

Intern 是一种显式地排重机制,但是它也有一定的副作用,因为需要开发者写代码时明确调用,一是不方便,每一个都显式调用是非常麻烦的;另外就是我们很难保证效率,应用开发阶段很难清楚地预计字符串的重复情况,有人认为这是一种污染代码的实践.幸好在 Oracle JDK 8u20 之后,推出了一个新的特性,也就是 G1 GC 下的字符串排重.它是通过将相同数据的字符串指向同一份数据来做到的,是 JVM底层的改变,并不需要 Java 类库做什么修