String详解

String 的声明

String的定义.png

由 JDK 中关于String的声明可以知道:

  • 不同字符串可能共享同一个底层char数组,例如字符串 String s=”abc” 与 s.substring(1) 就共享同一个char数组:char[] c = {‘a’,’b’,’c’}。其中,前者的 offset 和 count 的值分别为0和3,后者的 offset 和 count 的值分别为1和2。
  • offset 和 count 两个成员变量不是多余的,比如,在执行substring操作时。

注意:

  • String不属于八种基本数据类型,String 的实例是一个对象。因为对象的默认值是null,所以String的默认值也是null;但它又是一种特殊的对象,有其它对象没有的一些特性(String 的不可变性导致其像八种基本类型一样,比如,作为方法参数时,像基本类型的传值效果一样)。 例如,以下代码片段:
public class StringTest {

    public static void changeStr(String str) {
        String s = str;
        str += "welcome";
        System.out.println(s);
    }

    public static void main(String[] args) {
        String str = "1234";
        changeStr(str);
        System.out.println(str);
    }
}/* Output: 
        1234
        1234 
*///:~ 
  • new String() 和 new String(“”)都是声明一个新的空字符串,是空串不是null;

String 的不可变性

1. 不可变类

  • 不可变类:所谓的不可变类是指这个类的实例一旦创建完成后,就不能改变其成员变量值。如JDK内部自带的很多不可变类:Interger、Long和String等。
  • 可变类:相对于不可变类,可变类创建实例后可以改变其成员变量值,开发中创建的大部分类都属于可变类。

2. 不可变类的设计方法

  1. 类添加final修饰符,保证类不被继承。
    如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。
  2. 保证所有成员变量必须私有,并且加上final修饰。
    通过这种方式保证成员变量不可改变。但只做到这一步还不够,因为如果是对象成员变量有可能再外部改变其值。所以第4点弥补这个不足。
  3. 不提供改变成员变量的方法,包括setter
    避免通过其他接口改变成员变量的值,破坏不可变特性。
  4. 通过构造器初始化所有成员,进行深拷贝(deep copy)。
    如果构造器传入的对象直接赋值给成员变量,还是可以通过对传入对象的修改进而导致改变内部变量的值。例如:
public final class ImmutableDemo {  
    private final int[] myArray;  
    public ImmutableDemo(int[] array) {  
        this.myArray = array; // wrong  
    }  
}

这种方式不能保证不可变性,myArray和array指向同一块内存地址,用户可以在ImmutableDemo之外通过修改array对象的值来改变myArray内部的值。
为了保证内部的值不被修改,可以采用深度copy来创建一个新内存保存传入的值。正确做法:

public final class MyImmutableDemo {  
    private final int[] myArray;  
    public MyImmutableDemo(int[] array) {  
        this.myArray = array.clone();   
    }   
}
  1. 在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝。
    这种做法也是防止对象外泄,防止通过getter获得内部可变成员对象后对成员变量直接操作,导致成员变量发生改变。

3. String对象的不可变性

string对象在内存创建后就不可改变,不可变对象的创建一般满足以上5个原则,我们看看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
    ....
    public String(char value[]) {
         this.value = Arrays.copyOf(value, value.length); // deep copy操作
     }
    ...
     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类被final修饰,不可继承
  • string内部所有成员都设置为私有变量
  • 不存在value的setter
  • 并将value和offset设置为final。
  • 当传入可变数组value[]时,进行copy而不是直接将value[]复制给内部变量.
  • 获取value时不是直接返回对象引用,而是返回对象的copy.
    这都符合上面总结的不变类型的特性,也保证了String类型是不可变的类。

4. String对象的不可变性的优缺点

从上一节分析,String数据不可变类,那设置这样的特性有什么好处呢?我总结为以下几点:

  1. 字符串常量池的需要.
    字符串常量池可以将一些字符常量放在常量池中重复使用,避免每次都重新创建相同的对象、节省存储空间。但如果字符串是可变的,此时相同内容的String还指向常量池的同一个内存空间,当某个变量改变了该内存的值时,其他遍历的值也会发生改变。所以不符合常量池设计的初衷。
  2. 线程安全考虑。
    同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
  3. 类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。
  4. 支持hash映射和缓存。
    因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
    缺点:
    如果有对String对象值改变的需求,那么会创建大量的String对象。

5. String对象的是否真的不可变

虽然String对象将value设置为final,并且还通过各种机制保证其成员变量不可改变。但是还是可以通过反射机制的手段改变其值。例如:

//创建字符串"Hello World", 并赋给引用s
    String s = "Hello World"; 
    System.out.println("s = " + s); //Hello World

    //获取String类中的value字段
    Field valueFieldOfString = String.class.getDeclaredField("value");
    //改变value属性的访问权限
    valueFieldOfString.setAccessible(true);

    //获取s对象上的value属性的值
    char[] value = (char[]) valueFieldOfString.get(s);
    //改变value所引用的数组中的第5个字符
    value[5] = '_';
    System.out.println("s = " + s);  //Hello_World
打印结果为:
s = Hello World
s = Hello_World

发现String的值已经发生了改变。也就是说,通过反射是可以修改所谓的“不可变”对象的。

String 对象创建方式

  1. 字面值形式: JVM会自动根据字符串常量池中字符串的实际情况来决定是否创建新对象 (要么不创建,要么创建一个对象,关键要看常量池中有没有)
    JDK 中明确指出:
    String s = "abc";
    等价于:
char data[] = {'a', 'b', 'c'};
String str = new String(data);

该种方式先在栈中创建一个对String类的对象引用变量s,然后去查找 “abc”是否被保存在字符串常量池中。若”abc”已经被保存在字符串常量池中,则在字符串常量池中找到值为”abc”的对象,然后将s 指向这个对象; 否则,在 堆 中创建char数组 data,然后在 堆 中创建一个String对象object,它由 data 数组支持,紧接着这个String对象 object 被存放进字符串常量池,最后将 s 指向这个对象。
例如:

 private static void test01(){  
    String s0 = "kvill";        // 1
    String s1 = "kvill";        // 2
    String s2 = "kv" + "ill";     // 3

    System.out.println(s0 == s1);       // true  
    System.out.println(s0 == s2);       // true  
}

执行第 1 行代码时,“kvill” 入池并被 s0 指向;执行第 2 行代码时,s1 从常量池查询到” kvill” 对象并直接指向它;所以,s0 和 s1 指向同一对象。 由于 ”kv” 和 ”ill” 都是字符串字面值,所以 s2 在编译期由编译器直接解析为 “kvill”,所以 s2 也是常量池中”kvill”的一个引用。 所以,我们得出 s0==s1==s2;

  1. 通过 new 创建字符串对象 : 一概在堆中创建新对象,无论字符串字面值是否相等 (要么创建一个,要么创建两个对象,关键要看常量池中有没有)
    String s = new String("abc");
    等价于:
String original = "abc"; 
String s = new String(original);

所以,通过 new 操作产生一个字符串(“abc”)时,会先去常量池中查找是否有“abc”对象,如果没有,则创建一个此字符串对象并放入常量池中。然后,在堆中再创建“abc”对象,并返回该对象的地址。所以,对于 String str=new String(“abc”):如果常量池中原来没有”abc”,则会产生两个对象(一个在常量池中,一个在堆中);否则,产生一个对象。
 
  用 new String() 创建的字符串对象位于堆中,而不是常量池中。它们有自己独立的地址空间,例如:

private static void test02(){  
    String s0 = "kvill";  
    String s1 = new String("kvill");  
    String s2 = "kv" + new String("ill");  

    String s = "ill";
    String s3 = "kv" + s;    


    System.out.println(s0 == s1);       // false  
    System.out.println(s0 == s2);       // false  
    System.out.println(s1 == s2);       // false  
    System.out.println(s0 == s3);       // false  
    System.out.println(s1 == s3);       // false  
    System.out.println(s2 == s3);       // false  
}  

例子中,s0 还是常量池中”kvill”的引用,s1 指向运行时创建的新对象”kvill”,二者指向不同的对象。对于s2,因为后半部分是 new String(“ill”),所以无法在编译期确定,在运行期会 new 一个 StringBuilder 对象, 并由 StringBuilder 的 append 方法连接并调用其 toString 方法返回一个新的 “kvill” 对象。此外,s3 的情形与 s2 一样,均含有编译期无法确定的元素。因此,以上四个 “kvill” 对象互不相同。StringBuilder 的 toString 为:

public String toString() {
    return new String(value, 0, count);   // new 的方式创建字符串
    }

构造函数 String(String original) 的源码为:

/**
     * 根据源字符串的底层数组长度与该字符串本身长度是否相等决定是否共用支撑数组
     */
    public String(String original) {
        int size = original.count;
        char[] originalValue = original.value;
        char[] v;
        if (originalValue.length > size) {
            // The array representing the String is bigger than the new
            // String itself. Perhaps this constructor is being called
            // in order to trim the baggage, so make a copy of the array.
            int off = original.offset;
            v = Arrays.copyOfRange(originalValue, off, off + size);  // 创建新数组并赋给 v
        } else {
            // The array representing the String is the same
            // size as the String, so no point in making a copy.
            v = originalValue;
        }

        this.offset = 0;
        this.count = size;
        this.value = v;
    }

由源码可以知道,所创建的对象在大多数情形下会与源字符串 original 共享 char数组 。但是,什么情况下不会共享呢?

String s1 = "Abcd";       // s1 的value为Abcd的数组,offset为 0,count为 4
String s2 = a.substring(3);      // s2 的value也为Abcd的数组,offset为 3,count为 1
String c = new String(s2);      // s2.value.length 为 4,而 original.count = size = 1, 即 s2.value.length > size 成立

substring()方法的一些问题

  1. substring() 的作用:
    substring(int beginIndex, int endIndex)方法截取字符串并返回其[beginIndex,endIndex-1]范围内的内容。
String x = "abcdef";
x = x.substring(1,3);
System.out.println(x);
输出内容:
bc
  1. 调用substring()时发生了什么?

    你可能知道,因为x是不可变的,当使用x.substring(1,3)对x赋值的时候,它会指向一个全新的字符串:
    图片1.png

    然而,这个图不是完全正确的表示堆中发生的事情。因为在jdk6 和 jdk7中调用substring时发生的事情并不一样。
JDK 6中的substring

String是通过字符数组实现的。在jdk 6 中,String类包含三个成员变量:char value[], int offset,int count。他们分别用来存储真正的字符数组,数组的第一个位置索引以及字符串中包含的字符个数。

当调用substring方法的时候,会创建一个新的string对象,但是这个string的值仍然指向堆中的同一个字符数组。这两个对象中只有count和offset 的值是不同的。


图片2.png

下面是证明上说观点的Java源码中的关键代码:

//JDK 6
String(int offset, int count, char value[]) {
    this.value = value;
    this.offset = offset;
    this.count = count;
}

public String substring(int beginIndex, int endIndex) {
    //check boundary
    return  new String(offset + beginIndex, endIndex - beginIndex, value);
}
JDK 6中的substring导致的问题

如果你有一个很长很长的字符串,但是当你使用substring进行切割的时候你只需要很短的一段。这可能导致性能问题,因为你需要的只是一小段字符序列,但是你却引用了整个字符串(因为这个非常长的字符数组一直在被引用,所以无法被回收,就可能导致内存泄露)。在JDK 6中,一般用以下方式来解决该问题,原理其实就是生成一个新的字符串并引用他。
x = x.substring(x, y) + ""

JDK 7 中的substring

上面提到的问题,在jdk 7中得到解决。在jdk 7 中,substring方法会在堆内存中创建一个新的数组。
图片3.png

Java源码中关于这部分的主要代码如下:

//JDK 7
public String(char value[], int offset, int count) {
    //check boundary
    this.value = Arrays.copyOfRange(value, offset, offset + count);
}

public String substring(int beginIndex, int endIndex) {
    //check boundary
    int subLen = endIndex - beginIndex;
    return new String(value, beginIndex, subLen);
}

字符串常量池

1、字符串池
  字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价。JVM为了提高性能和减少内存开销,在实例化字符串字面值的时候进行了一些优化。为了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串常量池,每当以字面值形式创建一个字符串时,JVM会首先检查字符串常量池:如果字符串已经存在池中,就返回池中的实例引用;如果字符串不在池中,就会实例化一个字符串并放到池中。Java能够进行这样的优化是因为字符串是不可 变的,可以不用担心数据冲突进行共享。 例如:

public class Program
{
    public static void main(String[] args)
    {
       String str1 = "Hello";  
       String str2 = "Hello"; 
       System.out.print(str1 == str2);   // true
    }
}

一个初始为空的字符串池,它由类 String 私有地维护。当以字面值形式创建一个字符串时,总是先检查字符串池是否含存在该对象,若存在,则直接返回。此外,通过 new 操作符创建的字符串对象不指向字符串池中的任何对象。
2、手动入池
  一个初始为空的字符串池,它由类 String 私有地维护。 当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。特别地,手动入池遵循以下规则:
  对于任意两个字符串 s 和 t ,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true 。

public class TestString{
    public static void main(String args[]){
        String str1 = "abc";
        String str2 = new String("abc");
        String str3 = s2.intern();

        System.out.println( str1 == str2 );   //false
        System.out.println( str1 == str3 );   //true
    }
}

所以,对于 String str1 = “abc”,str1 引用的是 常量池(方法区) 的对象;而 String str2 = new String(“abc”),str2引用的是 堆 中的对象,所以内存地址不一样。但是由于内容一样,所以 str1 和 str3 指向同一对象。

intern方法不同版本的JDK中有何不同?
先看下以下代码:

String str1 = new StringBuilder("Hello").append("World").toString();
System.out.println(str1.intern() == str1);

打印结果:
jdk6 下false
jdk7 下true

Java 6 和Java7 中intern的表现有所不同,导致不同的原因是因为在Java 7中常量池的位置从PermGen区改到了Java堆区中。

jdk1.6中 intern 方法会把首次遇到的字符串实例复制到永久待(常量池)中,并返回此引用;但在jdk1.7中,只是会把首次遇到的字符串实例的引用添加到常量池中(没有复制),并返回此引用。

对于以上代码中的str1.intern() ,在jdk1.6中,会把“HollisChuang”这个字符串复制到常量池中,并返回他的引用。所以str1.intern()的值和str1的值,即两个对象的地址是不一样的。

对于以上代码中的str1.intern() ,在jdk1.7中,会把str1的引用保存到常量池中,并把这个引用返回。所以str1.intern()的值和str1的值是相等的。

扩展另外一个例子:

String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);

以上代码,无论在1.6还是1.7中都会返回false。
原因是"ja" + "va" = "java",这个常量在常量池中已经有了,因为这个字符串在Java中随处可见,曾经被初始化过。所以,在1.7中str2.intern()返回的内容是很久之前初始化的时候的那个引用,自然和刚刚创建的字符串的引用不相等。

  1. 实例
      看下面几个场景来深入理解 String。
    1). 情景一:字符串常量池
      Java虚拟机(JVM)中存在着一个字符串常量池,其中保存着很多String对象,并且这些String对象可以被共享使用,因此提高了效率。之所以字符串具有字符串常量池,是因为String对象是不可变的,因此可以被共享。字符串常量池由String类维护,我们可以通过intern()方法使字符串池手动入池。
String s1 = "abc";     
    //↑ 在字符串池创建了一个对象  
    String s2 = "abc";     
    //↑ 字符串pool已经存在对象“abc”(共享),所以创建0个对象,累计创建一个对象  
    System.out.println("s1 == s2 : "+(s1==s2));    
    //↑ true 指向同一个对象,  
    System.out.println("s1.equals(s2) : " + (s1.equals(s2)));    
    //↑ true  值相等  

2). 情景二:关于new String(“…”)

String s3 = new String("abc");  
    //↑ 创建了两个对象,一个存放在字符串池中,一个存在与堆区中;  
    //↑ 还有一个对象引用s3存放在栈中  
    String s4 = new String("abc");  
    //↑ 字符串池中已经存在“abc”对象,所以只在堆中创建了一个对象  
    System.out.println("s3 == s4 : "+(s3==s4));  
    //↑false   s3和s4栈区的地址不同,指向堆区的不同地址;  
    System.out.println("s3.equals(s4) : "+(s3.equals(s4)));  
    //↑true  s3和s4的值相同  
    System.out.println("s1 == s3 : "+(s1==s3));  
    //↑false 存放的地区都不同,一个方法区,一个堆区  
    System.out.println("s1.equals(s3) : "+(s1.equals(s3)));  
    //↑true  值相同 

通过 new String(“…”) 来创建字符串时,在该构造函数的参数值为字符串字面值的前提下,若该字面值不在字符串常量池中,那么会创建两个对象:一个在字符串常量池中,一个在堆中;否则,只会在堆中创建一个对象。对于不在同一区域的两个对象,二者的内存地址必定不同。
3). 情景三:字符串连接符“+”

    String str2 = "ab";  //1个对象  
    String str3 = "cd";  //1个对象                                         
    String str4 = str2+str3;                                        
    String str5 = "abcd";    
    System.out.println("str4 = str5 : " + (str4==str5)); // false 

我们看这个例子,局部变量 str2,str3 指向字符串常量池中的两个对象。在运行时,第三行代码(str2+str3)实质上会被分解成五个步骤,分别是:
 (1). 调用 String 类的静态方法 String.valueOf() 将 str2 转换为字符串表示;
 (2). JVM 在堆中创建一个 StringBuilder对象,同时用str2指向转换后的字符串对象进行初始化; 
 (3). 调用StringBuilder对象的append方法完成与str3所指向的字符串对象的合并;
 (4). 调用 StringBuilder 的 toString() 方法在堆中创建一个 String对象;
 (5). 将刚刚生成的String对象的堆地址存赋给局部变量引用str4。
  而引用str5指向的是字符串常量池中字面值”abcd”所对应的字符串对象。由上面的内容我们可以知道,引用str4和str5指向的对象的地址必定不一样。这时,内存中实际上会存在五个字符串对象: 三个在字符串常量池中的String对象、一个在堆中的String对象和一个在堆中的StringBuilder对象。
4). 情景四:字符串的编译期优化

String str1 = "ab" + "cd";  //1个对象  
    String str11 = "abcd";   
    System.out.println("str1 = str11 : "+ (str1 == str11));   // true

    final String str8 = "cd";  
    String str9 = "ab" + str8;  
    String str89 = "abcd";  
    System.out.println("str9 = str89 : "+ (str9 == str89));     // true
    //↑str8为常量变量,编译期会被优化  

    String str6 = "b";  
    String str7 = "a" + str6;  
    String str67 = "ab";  
    System.out.println("str7 = str67 : "+ (str7 == str67));     // false
    //↑str6为变量,在运行期才会被解析。

Java 编译器对于类似“常量+字面值”的组合,其值在编译的时候就能够被确定了。在这里,str1 和 str9 的值在编译时就可以被确定,因此它们分别等价于: String str1 = “abcd”; 和 String str9 = “abcd”;
  Java 编译器对于含有 “String引用”的组合,则在运行期会产生新的对象 (通过调用StringBuilder类的toString()方法),因此这个对象存储在堆中。
4、小结

  • 使用字面值形式创建的字符串与通过 new 创建的字符串一定是不同的,因为二者的存储位置不同:前者在方法区,后者在堆;

  • 我们在使用诸如String str = “abc”;的格式创建字符串对象时,总是想当然地认为,我们创建了String类的对象str。但是事实上, 对象可能并没有被创建。唯一可以肯定的是,指向 String 对象 的引用被创建了。至于这个引用到底是否指向了一个新的对象,必须根据上下文来考虑;

  • 字符串常量池的理念是享元模式

  • Java 编译器对 “常量+字面值” 的组合 是当成常量表达式直接求值来优化的;对于含有“String引用”的组合,其在编译期不能被确定,会在运行期创建新对象。

三大字符串类 : String、StringBuilder 和 StringBuffer

1. String 与 StringBuilder
  简要的说, String 类型 和 StringBuilder 类型的主要性能区别在于 String 是不可变的对象。 事实上,在对 String 类型进行“改变”时,实质上等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象。由于频繁的生成对象会对系统性能产生影响,特别是当内存中没有引用指向的对象多了以后,JVM 的垃圾回收器就会开始工作,继而会影响到程序的执行效率。所以,对于经常改变内容的字符串,最好不要声明为 String 类型。但如果我们使用的是 StringBuilder 类,那么情形就不一样了。因为,我们的每次修改都是针对 StringBuilder 对象本身的,而不会像对String操作那样去生成新的对象并重新给变量引用赋值。所以,在一般情况下,推荐使用 StringBuilder ,特别是字符串对象经常改变的情况下。
  在某些特别情况下,String 对象的字符串拼接可以直接被JVM 在编译期确定下来,这时,StringBuilder 在速度上就不占任何优势了。
  因此,在绝大部分情况下, 在效率方面:StringBuilder > String 。
2.StringBuffer 与 StringBuilder
  首先需要明确的是,StringBuffer 始于 JDK 1.0,而 StringBuilder 始于 JDK 5.0;此外,从 JDK 1.5 开始,对含有字符串变量 (非字符串字面值) 的连接操作(+),JVM 内部是采用 StringBuilder 来实现的,而在这之前,这个操作是采用 StringBuffer 实现的。
  JDK的实现中 StringBuffer 与 StringBuilder 都继承自 AbstractStringBuilder。AbstractStringBuilder的实现原理为:AbstractStringBuilder中采用一个 char数组 来保存需要append的字符串,char数组有一个初始大小,当append的字符串长度超过当前char数组容量时,则对char数组进行动态扩展,即重新申请一段更大的内存空间,然后将当前char数组拷贝到新的位置,因为重新分配内存并拷贝的开销比较大,所以每次重新申请内存空间都是采用申请大于当前需要的内存空间的方式,这里是 2 倍。
  StringBuffer 和 StringBuilder 都是可变的字符序列,但是二者最大的一个不同点是:StringBuffer 是线程安全的,而 StringBuilder 则不是。StringBuilder 提供的API与StringBuffer的API是完全兼容的,即,StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,但是后者一般要比前者快。因此,可以这么说,StringBuilder 的提出就是为了在单线程环境下替换 StringBuffer 。
  在单线程环境下,优先使用 StringBuilder。
3.实例
1). 编译时优化与字符串连接符的本质
  我们先来看下面这个例子:

public class Test2 {
    public static void main(String[] args) {
        String s = "a" + "b" + "c";
        String s1 = "a";
        String s2 = "b";
        String s3 = "c";
        String s4 = s1 + s2 + s3;

        System.out.println(s);
        System.out.println(s4);
    }
}

由上面的叙述,我们可以知道,变量s的创建等价于 String s = “abc”; 而变量s4的创建相当于:

StringBuilder temp = new StringBuilder(s1);
    temp.append(s2).append(s3);
    String s4 = temp.toString();

但事实上,是不是这样子呢?我们将其反编译一下,来看看Java编译器究竟做了什么:

//将上述 Test2 的 class 文件反编译
public class Test2
{
    public Test2(){}
    public static void main(String args[])
    {
        String s = "abc";            // 编译期优化
        String s1 = "a";
        String s2 = "b";
        String s3 = "c";

        //底层使用 StringBuilder 进行字符串的拼接
        String s4 = (new StringBuilder(String.valueOf(s1))).append(s2).append(s3).toString();   
        System.out.println(s);
        System.out.println(s4);
    }
}

根据上面的反编译结果,很好的印证了我们在上面提出的字符串连接符的本质。
2). 另一个例子:字符串连接符的本质
  由上面的分析结果,我们不难推断出 String 采用连接运算符(+)效率低下原因分析,形如这样的代码:

public class Test { 
    public static void main(String args[]) { 
        String s = null; 
            for(int i = 0; i < 100; i++) { 
                s += "a"; 
            } 
    }
}

会被编译器编译为:

public class Test
{
    public Test(){}
    public static void main(String args[])
    {
        String s = null;
        for (int i = 0; i < 100; i++)
            s = (new StringBuilder(String.valueOf(s))).append("a").toString();
    }
}

也就是说,每做一次 字符串连接操作 “+” 就产生一个 StringBuilder 对象,然后 append 后就扔掉。下次循环再到达时,再重新 new 一个 StringBuilder 对象,然后 append 字符串,如此循环直至结束。事实上,如果我们直接采用 StringBuilder 对象进行 append 的话,我们可以节省 N - 1 次创建和销毁对象的时间。所以,对于在循环中要进行字符串连接的应用,一般都是用StringBulider对象来进行append操作。

String 与 (深)克隆

1、克隆的定义与意义
  顾名思义,克隆就是制造一个对象的副本。一般地,根据所要克隆的对象的成员变量中是否含有引用类型,可以将克隆分为两种:浅克隆(Shallow Clone) 和 深克隆(Deep Clone),默认情况下使用Object中的clone方法进行克隆就是浅克隆,即完成对象域对域的拷贝。
(1). Object 中的 clone() 方法

20170313093548412.png

在使用clone()方法时,若该类未实现 Cloneable 接口,则抛出 java.lang.CloneNotSupportedException 异常。下面我们以Employee这个例子进行说明:

public class Employee {
    private String name;
    private double salary;
    private Date hireDay;

    ...
    public static void main(String[] args) throws CloneNotSupportedException {
        Employee employee = new Employee();
        employee.clone();
        System.out.println("克隆完成...");
    } 
}/* Output: 
        ~Exception in thread "main" java.lang.CloneNotSupportedException: P1_1.Employee
 *///:

(2). Cloneable 接口
  Cloneable 接口是一个标识性接口,即该接口不包含任何方法(甚至没有clone()方法),但是如果一个类想合法的进行克隆,那么就必须实现这个接口。下面我们看JDK对它的描述:

  • A class implements the Cloneable interface to indicate to the java.lang.Object.clone() method that it is legal for that method to make a field-for-field copy of instances of that class.
  • Invoking Object’s clone method on an instance that does not implement the Cloneable interface results in the exception CloneNotSupportedException being thrown.
  • By convention, classes that implement this interface should override Object.clone (which is protected) with a public method.
  • Note that this interface does not contain the clone() method. Therefore, it is not possible to clone an object merely by virtue of the fact that it implements this interface. Even if the clone method is invoked reflectively, there is no guarantee that it will succeed.
/**
 * @author  unascribed
 * @see     java.lang.CloneNotSupportedException
 * @see     java.lang.Object#clone()
 * @since   JDK1.0
 */
public interface Cloneable {
}

2、Clone & Copy
  假设现在有一个Employee对象,Employee tobby = new Employee(“CMTobby”,5000),通常, 我们会有这样的赋值Employee tom=tobby,这个时候只是简单了copy了一下reference,tom 和 tobby 都指向内存中同一个object,这样tom或者tobby对对象的修改都会影响到对方。打个比方,如果我们通过tom.raiseSalary()方法改变了salary域的值,那么tobby通过getSalary()方法得到的就是修改之后的salary域的值,显然这不是我们愿意看到的。如果我们希望得到tobby所指向的对象的一个精确拷贝,同时两者互不影响,那么我们就可以使用Clone来满足我们的需求。Employee cindy=tobby.clone(),这时会生成一个新的Employee对象,并且和tobby具有相同的属性值和方法。
3、Shallow Clone & Deep Clone

  Clone是如何完成的呢?Object中的clone()方法在对某个对象实施克隆时对其是一无所知的,它仅仅是简单地执行域对域的copy,这就是Shallow Clone。这样,问题就来了,以Employee为例,它里面有一个域hireDay不是基本类型的变量,而是一个reference变量,经过Clone之后克隆类只会产生一个新的Date类型的引用,它和原始引用都指向同一个 Date 对象,这样克隆类就和原始类共享了一部分信息,显然这种情况不是我们愿意看到的,过程下图所示:
clone.png

这个时候,我们就需要进行 Deep Clone 了,以便对那些引用类型的域进行特殊的处理,例如本例中的hireDay。我们可以重新定义 clone方法,对hireDay做特殊处理,如下代码所示:
class Employee implements Cloneable  
{  
    private String name;
    private int id;
    private Date hireDay;
    ...

    @Override
    public Object clone() throws CloneNotSupportedException {
       Employee cloned = (Employee) super.clone();  
       // Date 支持克隆且重写了clone()方法,Date 的定义是:
       // public class Date implements java.io.Serializable, Cloneable, Comparable<Date>
       cloned.hireDay = (Date) hireDay.clone() ;   
       return cloned;  
    }
}  

因此,Object 在对某个对象实施 Clone 时,对其是一无所知的,它仅仅是简单执行域对域的Copy。 其中,对八种基本类型的克隆是没有问题的,但当对一个引用类型进行克隆时,只是克隆了它的引用。因此,克隆对象和原始对象共享了同一个对象成员变量,故而提出了深克隆 : 在对整个对象浅克隆后,还需对其引用变量进行克隆,并将其更新到浅克隆对象中去。
4、一个克隆的示例
  在这里,我们通过一个简单的例子来说明克隆在Java中的使用,如下所示:

// 父类 Employee 
public class Employee implements Cloneable{

    private String name;
    private double salary;
    private Date hireDay;

    public Employee(String name, double salary, Date hireDay) {
        this.name = name;
        this.salary = salary;
        this.hireDay = hireDay;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    public Date getHireDay() {
        return hireDay;
    }

    public void setHireDay(Date hireDay) {
        this.hireDay = hireDay;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        Employee cloned = (Employee) super.clone();
        cloned.hireDay = (Date) hireDay.clone();
        return cloned;
    }


    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((hireDay == null) ? 0 : hireDay.hashCode());
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        long temp;
        temp = Double.doubleToLongBits(salary);
        result = prime * result + (int) (temp ^ (temp >>> 32));
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Employee other = (Employee) obj;
        if (hireDay == null) {
            if (other.hireDay != null)
                return false;
        } else if (!hireDay.equals(other.hireDay))
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        if (Double.doubleToLongBits(salary) != Double
                .doubleToLongBits(other.salary))
            return false;
        return true;
    }

    @Override
    public String toString() {
        return name + " : " + String.valueOf(salary) + " : " + hireDay.toString();
    }
}
// 子类 Manger 
public class Manger extends Employee implements Cloneable {
    private String edu;

    public Manger(String name, double salary, Date hireDay, String edu) {
        super(name, salary, hireDay);
        this.edu = edu;
    }

    public String getEdu() {
        return edu;
    }

    public void setEdu(String edu) {
        this.edu = edu;
    }

    @Override
    public String toString() {
        return this.getName() + " : " + this.getSalary() + " : "
                + this.getHireDay() + " : " + this.getEdu();
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = super.hashCode();
        result = prime * result + ((edu == null) ? 0 : edu.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (!super.equals(obj))
            return false;
        if (getClass() != obj.getClass())
            return false;
        Manger other = (Manger) obj;
        if (edu == null) {
            if (other.edu != null)
                return false;
        } else if (!edu.equals(other.edu))
            return false;
        return true;
    }

    public static void main(String[] args) throws CloneNotSupportedException {

        Manger manger = new Manger("Rico", 20000.0, new Date(), "NEU");
        // 输出manger
        System.out.println("Manger对象 = " + manger.toString());

        Manger clonedManger = (Manger) manger.clone();
        // 输出克隆的manger
        System.out.println("Manger对象的克隆对象 = " + clonedManger.toString());
        System.out.println("Manger对象和其克隆对象是否相等:  "
                + manger.equals(clonedManger) + "\r\n");

        // 修改、输出manger
        manger.setEdu("TJU");
        System.out.println("修改后的Manger对象 = " + manger.toString());

        // 再次输出manger
        System.out.println("原克隆对象= " + clonedManger.toString());
        System.out.println("修改后的Manger对象和原克隆对象是否相等:  "
                + manger.equals(clonedManger));
    }
}
/* Output: 
        Manger对象 = Rico : 20000.0 : Mon Mar 13 15:36:03 CST 2017 : NEU
        Manger对象的克隆对象 = Rico : 20000.0 : Mon Mar 13 15:36:03 CST 2017 : NEU
        Manger对象和其克隆对象是否相等:  true

        修改后的Manger对象 = Rico : 20000.0 : Mon Mar 13 15:36:03 CST 2017 : TJU
        原克隆对象= Rico : 20000.0 : Mon Mar 13 15:36:03 CST 2017 : NEU
        修改后的Manger对象和原克隆对象是否相等:  false
 *///:

5、Clone()方法的保护机制
  在Object中clone()是被申明为 protected 的,这样做是有一定的道理的。以 Employee 类为例,如果我们在Employee中重写了protected Object clone()方法, ,就大大限制了可以“克隆”Employee对象的范围,即可以保证只有在和Employee类在同一包中类及Employee类的子类里面才能“克隆”Employee对象。进一步地,如果我们没有在Employee类重写clone()方法,则只有Employee类及其子类才能够“克隆”Employee对象。
  这里面涉及到一个大家可能都会忽略的一个知识点,那就是关于protected的用法。实际上,很多的有关介绍Java语言的书籍,都对protected介绍的比较的简单,就是:被protected修饰的成员或方法对于本包和其子类可见。这种说法有点太过含糊,常常会对大家造成误解。
6、注意事项
Clone()方法的使用比较简单,注意如下几点即可:

  • 什么时候使用shallow Clone,什么时候使用deep Clone?
    这个主要看具体对象的域是什么性质的,基本类型还是引用类型。

  • 调用Clone()方法的对象所属的类(Class)必须实现 Clonable 接口,否则在调用Clone方法的时候会抛出CloneNotSupportedException;

  • 所有数组对象都实现了 Clonable 接口,默认支持克隆;

  • 如果我们实现了 Clonable 接口,但没有重写Object类的clone方法,那么执行域对域的拷贝;

  • 明白 String 在克隆中的特殊性
      String 在克隆时只是克隆了它的引用。
      奇怪的是,在修改克隆后的 String 对象时,其原来的对象并未改变。原因是:String是在内存中不可以被改变的对象。虽然在克隆时,源对象和克隆对象都指向了同一个String对象,但当其中一个对象修改这个String对象的时候,会新分配一块内存用来保存修改后的String对象并将其引用指向新的String对象,而原来的String对象因为还存在指向它的引用,所以不会被回收。这样,对于String而言,虽然是复制的引用,但是当修改值的时候,并不会改变被复制对象的值。所以在使用克隆时,我们可以将 String类型 视为与基本类型,只需浅克隆即可。

String 总结

(1). 使用字面值形式创建字符串时,不一定会创建对象,但其引用一定指向位于字符串常量池的某个对象;
(2). 使用 new String(“…”)方式创建字符串时,一定会创建对象,甚至可能会同时创建两个对象(一个位于字符串常量池中,一个位于堆中);
(3). String 对象是不可变的,对String 对象的任何改变都会导致一个新的 String 对象的产生,而不会影响到原String 对象;
(4). StringBuilder 与 StringBuffer 具有共同的父类,具有相同的API,分别适用于单线程和多线程环境下。特别地,在单线程环境下,StringBuilder 是 StringBuffer 的替代品,前者效率相对较高;

(5). 字符串比较时用的什么方法,内部实现如何?
  使用equals方法 : 先比较引用是否相同(是否是同一对象),再检查是否为同一类型(str instanceof String), 最后比较内容是否一致(String 的各个成员变量的值或内容是否相同)。这也同样适用于诸如 Integer 等的八种包装器类。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 155,457评论 4 357
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 65,943评论 1 285
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 105,327评论 0 236
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,307评论 0 201
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,630评论 3 283
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,114评论 1 202
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,514评论 2 305
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,227评论 0 193
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,859评论 1 234
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,186评论 2 238
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,741评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,087评论 2 248
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,614评论 3 228
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,926评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,630评论 0 190
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,117评论 2 261
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,076评论 2 257

推荐阅读更多精彩内容

  • 使用Java语言进行编程,我们每天都要用到String类,但是以前只是拿来就用,并不知道String类的实现原理和...
    lunabird阅读 412评论 0 1
  • 从网上复制的,看别人的比较全面,自己搬过来,方便以后查找。原链接:https://www.cnblogs.com/...
    lxtyp阅读 1,305评论 0 9
  • 一 : String内存解析 案例1 内存分析 : str1->存放在字符串常量池中 假设地址为 0x12138 ...
    TianTianBaby223阅读 1,646评论 4 1
  • 【姓名】吴从严 【导师】袁文魁 【分舵】文魁派第一分舵 【舵主】刘丽琼 【导图解说】 这堂课的主题是如何运用双值分...
    希波克拉底先生阅读 414评论 3 0
  • 序章 人这一生都会有一些你不敢做的事,比如说:第一,当你想追你喜欢的那个男孩子时,你会错过;第二是喜欢他,但却...
    仲夜梦阅读 138评论 0 0