解析Java中String、StringBuilder以及StringBuffer

一,String类

1,String类的特点(由源码分析)

public final class String

    implements java.io.Serializable, Comparable<String>, CharSequence

{

    /** The value is used for character storage. */

    private final char value[];

    /** The offset is the first index of the storage that is used. */

    private final int offset;

    /** The count is the number of characters in the String. */

    private final int count;

    /** Cache the hash code for the string */

    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */

    private static final long serialVersionUID = -6849794470754667710L; 

}

由上可知:

(1)String类被final修饰,所以String类不能被继承,也是线程安全的

(2) private final char value[];//用于存储字符串

private int hash; // 缓存String的hash值

(3)String类的方法实现:(源码)

  public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }

 从上面方法可以看出,concat操作不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。

2,String的创建存储以及比较

  字符串的两种创建方式:String str="fuheiuG";(存储在常量池中) String str=new string("fgaueiug");(new对象存储在堆中)

JVM内存机制中,在class文件中有一部分 来存储编译期间生成的 字面常量以及符号引用,这部分叫做class文件常量池,在运行期间对应着方法区的运行时常量池。

  因此在上述代码中,String str1 = "hello world";和String str3 = "hello world"; 都在编译期间生成了 字面常量和符号引用,运行期间字面常量"hello world"被存储在运行时常量池(当然只保存了一份)。通过这种方式来将String对象跟引用绑定的话,JVM执行引擎会先在运行时常量池查找是否存在相同的字面常量,如果存在,则直接将引用指向已经存在的字面常量;否则在运行时常量池开辟一个空间来存储该字面常量,并将引用指向该字面常量。

  总所周知,通过new关键字来生成对象是在堆区进行的,而在堆区进行对象生成的过程是不会去检测该对象是否已经存在的。因此通过new来创建对象,创建出的一定是不同的对象,即使字符串的内容是相同的。

例如:public class StringCompare {
  public static void A() {
    String str1 = "java";
    String str2 = "java";
    System.out.println(str1 == str2);  //true  原因很简单,String对象被放进常量池里了,再次出现“java”字符串的时候,JVM很兴奋地把str2的引用也指向了 “java”对象,它认为自己节省了内存开销
  }
  public static void B() {
    String str1 = new String("java");
    String str2 = new String("java");
    System.out.println(str1 == str2);  //false  new是在堆中创建了两个不同的对象。
  }

 public static void D() {
    String s1 = "java";
    String s2 = new String("java");
    System.out.println(s1 == s2.);  //false,  ==比较引用,因为对象在不同的位置创建。所以对象不可能相同。
  }
 public static void D2() {
    String s1 = "java";
    String s2 = new String("java");
    System.out.println(s1 == s2.intern());  //true,  使用intern方法可以通过==比较对象的内容,实质上intern方法运行时会在常量池中查找是否存在内容相同的字符串,如果存在则返回指向该字符串的引用,如果不存在,则会将该字符串入池,并返回一个指向该字符串的引用。       intern()方法主要可以用来避免创建内容重复的String对象,值得我们重视。

  public static void C() {
    String str1 = "java";
    String str2 = "blog";
    String s = str1 + str2;
    System.out.println(s == "javablog");  //false   str1和str2和Javablog在编译时刻就已经放在常量池中了,二s是在运行期间在堆中创建,所以s引用不可能指向常量池中的对象。
  }
  public static void C2() {
    String str1 = "javablog";
    String str2 = "java"+"blog";    //在编译时被优化成String str2 = "javablog";
    System.out.println(str1 == str2);  //true
  }
  public static void E() {
    String str1 = "java";
    String str2 = new String("java");
    System.out.println(str1.equals(str2));  //true     无论在常量池还是堆中的对象,用equals()方法比较的就是内容,就这么简单!
  }

 StringBuffer的insert方法:

1

2

3

4

5

6

public synchronized StringBuffer insert(int index, char str[], int offset,

                                            int len)

    {

        super.insert(index, str, offset, len);

        return this;

    }

二, String vs StringBuilder vs StringBuffer

1,三者之间的区别:
StringBuilder 是可变的,因此可以在创建以后修改内部的值.(适用于单线程下在字符缓冲区进行大量操作的情况)
StringBuffer 是同步的,因此是线程安全的,但效率相对更低.(适用多线程下在字符缓冲区进行大量操作的情况)

String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行           创建和回收的操作,所以速度要比String快很多。

在运行速度方面:StringBuilder > StringBuffer > String,所以在不考虑线程安全的情况下(即单线程的情况下)应使用StringBuilder。。多线程的情况下应使用StringBuffer。

例如:

4

5

6

7

8

9

public class Main {

         

    public static void main(String[] args) {

        String string = "";

        for(int i=0;i<10000;i++){

            string += "hello";

        }

    }

}

  这句 string += "hello";的过程相当于将原有的string变量指向的对象内容取出与"hello"作字符串相加操作再存进另一个新的String对象当中,再让string变量指向新生成的对象。

      string+="hello"的操作事实上会自动被JVM优化成StringBuilder进行操作。

1

2

3

4

5

6

7

8

9

public class Main {

         

    public static void main(String[] args) {

        StringBuilder stringBuilder = new StringBuilder();

        for(int i=0;i<10000;i++){

            stringBuilder.append("hello");

        }

    }

        这里new操作只进行了一次,也就是说只生成了一个对象,append操作是在原有对象的基础上进行的。因此这段代码所占的资源要比上面小得多。

2,字符串拼的实质:

  StringBuilder的append() 工作原理,    StringBuilder是调用了其父类的append方法的,我们来看源码:

public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
  public AbstractStringBuilder append(String str) {
        if (str == null) {
            str = "null";
        }
        int len = str.length();
        if (len == 0) {
            return this;
        }

        int newCount = count + len;//统计拼接后字符串长度
        if (newCount > value.length) {
            expandCapacity(newCount);//如果拼接结果大于所用的char[],则扩容
        }
       //getChars将拼接字符串赋值到char[]中
        str.getChars(0, len, value, count);
        count = newCount;//更新char[]所保存的字符串长度
        return this;
    }

  可知,用StringBuilder拼接字符串时,其实是在底层创建了一个char[]数组,然后通过char[]把要拼接的字符串添加到char[]而已。最后通过toString()生成最终拼接结果时就是通过  return new String(char[]) 实现的。

           从上面(其实应该说是五种,+ 分为字符串常量的拼接和变量的拼接两种)的字符串拼接来看,除了字符串常量的拼接是返回拘留字符串的地址外,其他四种(str1+str2、str1.concat(str2)、builder.append(str1).append(str2)、buffer.append(str1).append(str2))都是使用了StringBuilder,或者说是StringBuilder的父类的拼接方法来做的——创建一个char数组,把需要拼接的内容先存进char数组,最后通过char数组创建新的String对象返回。

            造成三者性能差别的主要原因是:

            用String的 + 累加拼接字符串变量时,每拼接一个就会创建一个StringBuilder,并通过append()方法拼接,然后返回一个新的String对象。然后再继续拼接下一个变量。这样就会导致重复创建StringBuilder对象,性能低下。用 concat() 累计拼接时,则每两个字符串拼接都会创建一个 char[] 进行内容拼接并返回一个新的String对象作为结果,重复调用concat()会导致重复创建char[]和新String对象,性能低下。

            StringBuilder在调用toString()之前都不会创建拼接结果,并且底层的char数组会自动扩容,一直到拼接字符串全部存入char数组后,调用toString()时才创建新的String对象并返回,这样就避免了重复创建,效率提高。

            StringBuffer则因为使用了syncrhoized对append()进行了加锁,所以导致性能稍微低于StringBuilder

注意:

(1)String str = new String("abc")创建了多少个对象?

什么叫“创建了”?什么时候创建了什么? 

人混淆的地方就是这里,这段代码在运行期间确实只创建了一个对象,即在堆上创建了"abc"对象。而为什么大家都在说是2个对象呢,这里面要澄清一个概念  该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。

    因此,这个问题如果换成 String str = new String("abc")涉及到几个String对象?合理的解释是2个。

        解释:一个是字符串字面量"abc"所对应的、驻留(intern)在一个全局共享的字符串常量池中的实例,另一个是通过new String(String)创建并初始化的、内容与"abc"相同的实例

         或者是:在运行时涉及几个String实例?(不涉及编译时期)是一个

(2)StringBuffer常见的方法:,StringBuilder的方法相同。

1,public StringBuffer append(String s)         将指定的字符串追加到此字符序列。

2,public StringBuffer reverse()             将此字符序列用其反转形式取代。

3,public delete(int start, int end)         移除子字符串中的字符(可以删除一个字符,也可以不删除一个字符,也可以是字符串)

4,public  deleteCharAt(int i)           删除某一特定位置的字符,也可以用方法三完成。

5,public insert(int offset,  String str)         将 string参数的字符串表示形式插入此序列中,这里的参数可以是任何类型的。int,double,数组...... ...跟append方法一样后面的参数可以是任何类型的数据。

6,replace(int start, int end, String str)     使用给定 String 中的字符替换字符串中的某段字符。

7,void setCharAt(int index, char ch)       修改某一特定位置的字符。注意参数只能是chat。

8,void setLength(int newLength)             设置字符序列的长度。

猜你喜欢

转载自blog.csdn.net/abingR/article/details/80723540