String 的不可变性

什么是不可变对象?

如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变

定义一个字符串

String s = "abcd";
String 的不可变性_第1张图片

s 中保存了 String 对象的引用。

使用变量来赋值变量

String s2 = s;
String 的不可变性_第2张图片

s2 保存了相同的引用值,因为他们代表同一个对象。

字符串连接

s = s.concat("ef");
String 的不可变性_第3张图片

s 中保存的是一个重新创建出来的 String 对象的引用。

String 总结

一旦一个 String 对象在内存(堆)中被创建出来,它就无法被修改(因为 String 类的所有成员变量都是 private,并且没有提供 public 的 set 方法来修改这些值。此外成员变量都是 final 的,这就意味着一旦初始化就无法修改)。特别要注意的是,String 类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象

如果需要一个可修改的字符串,应该使用 StringBuffer 或者 StringBuilder。否则会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的 String 对象被创建出来

String 类不可变性的好处

  1. 只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多 heap 空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么将不能实现 String interning(指对不同的字符串仅仅只保存一个,即不会保存多个相同的字符串),因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变

  2. 如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在 socket 编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞

  3. 因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的

  4. 类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载 java.sql.Connection 类,而这个值被改成了 myhacked.Connection,那么会对你的数据库造成不可知的破坏

  5. 因为字符串是不可变的,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算。这就使得字符串很适合作为 Map 中的键,字符串的处理速度要快过其它的键对象。这就是为什么 HashMap 中的键往往都使用字符串

String 对象真的不可变吗

JDK 6 String 源码:

public final class String  
    implements java.io.Serializable, Comparable, 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  

JDK 7 String 源码:

public final class String  
    implements java.io.Serializable, Comparable, CharSequence {  
    /** The value is used for character storage. */  
    private final char value[];  
  
    /** Cache the hash code for the string */  
    private int hash; // Default to 0 

String 的成员变量是 private final 的,也就是初始化之后不可改变。但是在这几个成员中, value 比较特殊,因为他是一个引用变量,而不是真正的对象。value 是 final 修饰的,也就是说 final 不能再指向其他数组对象,那么能改变 value 指向的数组吗? 比如将数组中的某个位置上的字符变为下划线 “_”。至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个 value 引用,更不能通过这个引用去修改数组

那么用什么方式可以访问私有成员呢? 没错,用反射,可以得到 String 对象中的 value 属性, 进而改变通过获得的 value 引用改变数组的结构

public void testReflection() throws Exception {  
      
    //创建字符串"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

在这个过程中,s 始终引用的同一个 String 对象,但是在反射前后,这个 String 对象发生了变化,也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,它组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个 Car 对象,它组合了一个 Wheel 对象,虽然这个 Wheel 对象声明成了 private final 的,但是这个 Wheel 对象内部的状态可以改变, 那么就不能很好的保证 Car 对象不可变

你可能感兴趣的:(String 的不可变性)