您的位置 首页 java

Java 从底层与接口实现了解String、StringBuffer、StringBuilder

String、StringBuffer 和 StringBuilder的接口实现关系:

String:字符串常量,字符串长度不可变。Java中String 是immutable(不可变)的。用于存放字符的数组被声明为final的,因此只能赋值一次,不可再更改。

StringBuffer:宣称线程安全的字符串变量(Synchronized,即线程安全,multiple threads cannot access it simultaneously,可将字符串缓冲区安全地用于多个线程)。如果要频繁对字符串内容进行修改,出于效率考虑最好使用 StringBuffer,如果想转成 String 类型,可以调用 StringBuffer 的 toString() 方法。

StringBuilder:字符串变量(非线程安全,但效率更高)。在内部 StringBuilder 对象被当作是一个包含字符序列的变长数组。

三类字符串使用建议:

1 关于 String 为啥是不可改变的

字符串实际上就是一个 char 数组,并且内部就是封装了一个 char 数组。

并且这里 char 数组是被 final 修饰的:

 public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */    private final char value[];  

并且 String 中的所有的方法,都是对于 char 数组的改变,只要是对它的改变,方法内部都是返回一个新的 String 实例。

对于字符串的加运算,当编译成 class 文件时,会自动编译为 StringBuffer 来进行字符串的连接操作。

同时对于字符串常量池:

当一个字符串是一个字面量时,它会被放到一个常量池中,等待复用。

String 类的常见面试问题:

面试题一:

 String s1 = "abc"; // 常量池
String s2 = new String("abc"); // 堆内存中
System.out.println(s1==s2); // false,两个对象的地址值不一样。
System.out.println(s1.equals(s2)); // true  

面试题二:

 String s1="a"+"b"+"c";
String s2="abc";
System.out.println(s1==s2);
System.out.println(s1.equals(s2));
// java 中常量优化机制,编译时 s1 已经成为 abc 在常量池中查找创建,s2 不需要再创建。  

面试题三:

 String s1="ab";
String s2="abc";
String s3=s1+"c";
System.out.println(s3==s2); // false
System.out.println(s3.equals(s2)); // true  

先在常量池中创建 ab ,地址指向 s1, 再创建 abc ,指向 s2。对于 s3,先创建StringBuilder(或 StringBuffer)对象,通过 append 连接得到 abc ,再调用 toString() 转换得到的地址指向 s3。故 (s3==s2) 为 false。

2 StringBuffer 类

在使用 StringBuffer 类时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象。

Java.lang.StringBuffer 是宣称线程安全的可变字符序列。在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。

3 StringBuilder 类

StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。

由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。

关于线程安全,即使你真的遇到了这样的场景,很不幸的是,恐怕你仍然有 99.99….99% 的情况下没有必要选择 stringbuffer,因为 stringbuffer 的线程安全,仅仅是保证 jvm 不抛出异常顺利的往下执行而已,它可不保证逻辑正确和调用顺序正确。大多数时候,我们需要的不仅仅是线程安全,而是锁。

最后,为什么会有 stringbuffer 的存在,如果真的没有价值,为什么 jdk 会提供这个类?答案太简单了,因为最早是没有 stringbuilder 的,sun 的人不知处于何种愚蠢的考虑,决定让 stringbuffer 是线程安全的,然后大约 10 年之后,人们终于意识到这是一个多么愚蠢的决定,意识到在这 10 年之中这个愚蠢的决定为 java 运行速度慢这样的流言贡献了多大的力量,于是,在 jdk1.5 的时候,终于决定提供一个非线程安全的 stringbuffer 实现,并命名为 stringbuilder。顺便,javac 好像大概也是从这个版本开始,把所有用加号连接的 string 运算都隐式的改写成 stringbuilder,也就是说,从 jdk1.5 开始,用加号拼接字符串已经没有任何性能损失了。

“用加号拼接字符串已经没有任何性能损失了”这种说法并不严谨,严格的说,如果没有循环的情况下,单行用加号拼接字符串是没有性能损失的,java 编译器会隐式的替换成 stringbuilder,但在有循环的情况下,编译器没法做到足够智能的替换,仍然会有不必要的性能损耗,因此,用循环拼接字符串的时候,还是老老实实的用 stringbuilder 吧。

4 关于字符串字面量

Like many Java objects, all String instances are created on the heap, even literals. When the JVM finds a String literal that has no equivalent reference in the heap, the JVM creates a corresponding String instance on the heap and it also stores a reference to the newly created String instance in the String pool. Any other references to the same String literal are replaced with the previously created String instance in the heap.

与许多Java对象一样,所有字符串实例都是在堆上创建的,甚至是文字。当JVM发现堆中没有等效引用的字符串文本时,JVM会在堆上创建相应的字符串实例,并在字符串池中存储对新创建的字符串实例的引用。对同一字符串文字的任何其他引用都将替换为堆中先前创建的字符串实例。

 class Strings
{
    public static void main (String[] args)
    {
        String a = "alpha";
        String b = "alpha";
        String c = new String("alpha");
        //All three strings are equivalent
        System.out.println(a.equals(b) && b.equals(c));
        //Although only a and b reference the same heap object
        System.out.println(a == b);
        System.out.println(a != c);
        System.out.println(b != c);
    }
}  

图示:

When we use double quotes to create a String, it first looks for String with same value in the String pool, if found it just returns the reference else it creates a new String in the pool and then returns the reference.

当我们使用双引号创建字符串时,它首先在字符串池中查找具有相同值的字符串,如果找到,它只返回引用,否则它在池中创建一个新字符串,然后返回引用。

However using new operator, we force String class to create a new String object in heap space. We can use intern() method to put it into the pool or refer to other String object from string pool having same value.

然而,使用new操作符,我们强制String类在堆空间中创建一个新的String对象。我们可以使用intern()方法将其放入池中,或引用字符串池中具有相同值的其他字符串对象。

The String pool itself is also created on the heap. (Version < Java SE 7)

Java SE 7以前的版本的字符串池本身也在堆上创建。

Before Java 7, String literals were stored in the runtime constant pool in the method area of PermGen, that had a fixed size.

在Java 7之前,字符串文本存储在PermGen方法区域的运行时常量池中,该池的大小是固定的。

The String pool also resided in PermGen. (Version ≥ Java SE 7)

Java SE 7 的字符串池也位于PermGen中。

In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String. intern() method will see more significant differences.

在JDK 7中,内部字符串不再分配给Java堆的永久生成(PermGen),而是与应用程序创建的其他对象一起分配给Java堆的主要部分(称为年轻和老年代)。此更改将导致更多数据驻留在主Java堆中,而永久生成中的数据更少,因此可能需要调整堆大小。由于这种变化,大多数应用程序在堆使用方面只会看到相对较小的差异,但较大的应用程序会加载许多类或大量使用字符串。intern()方法将看到更显著的差异。

绝大部分 Java 程序员应该都见过 “java.lang.OutOfMemoryError: PermGen space “这个异常。这里的 “PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。

其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。

在 JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区间了,取而代之是一个叫做 Metaspace(元空间) 的东西。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小。

去除永久代的原因:(1)为了HotSpot与JRockit的融合;(2)永久代大小不容易确定,PermSize指定太小容易造成永久代OOM,与老年代没关系。字符串存在永久代中,容易出现性能问题和内存溢出。

ref

-End-

文章来源:智云一二三科技

文章标题:Java 从底层与接口实现了解String、StringBuffer、StringBuilder

文章地址:https://www.zhihuclub.com/174682.shtml

关于作者: 智云科技

热门文章

网站地图