为何String被设计为不可变?是否真的不可变?

1 对象不可变定义 

不可变对象是指对象的状态在被初始化之后,在整个对象的生命周期内,不可改变。 java

2 如何不可变 

一般状况下,在java中经过如下步骤实现不可变数组

  1. 对于属性不提供设值方法
  2. 全部的属性定义为private final
  3. 类声明为final不容许继承
  4. Return deep cloned objects with copied content for all mutable fields in class

注意:不用final关键字也能够实现对象不可变,使用final只是显示的声明,提示开发者和编译器为不可变。缓存

3 Java中典型的不可变类为String类 

为何String被设计为不可变?安全

  • 安全首要缘由是安全,不只仅体如今你的应用中,并且在JDK中,Java的类装载机制经过传递的参数(一般是类名)加载类,这些类名在类路径下,想象一下,假设String是可变的,一些人经过自定义类装载机制分分钟黑掉应用。若是没有了安全,Java不会走到今天
  • 性能 string不可变的设计出于性能考虑,固然背后的原理是string pool,固然string pool不可能使string类不可变,不可变的string更好的提升性能。
  • 线程安全 当多线程访问时,不可变对象是线程安全的,不须要什么高深的逻辑解释,若是对象不可变,线程也不能改变它。 

4 为何String是不可变

区分对象和对象的引用多线程

对于Java初学者, 对于String是不可变对象老是存有疑惑。看下面代码:
 
		String s = "ABCabc";
		System.out.println("s = " + s);
		
		s = "123456";
		System.out.println("s = " + s);

  输出结果:布局

s = ABCabc
s = 123456
首先建立一个String对象s,而后让s的值为“ABCabc”, 而后又让s的值为“123456”。 从打印结果能够看出,s的值确实改变了。那么怎么还说String对象是不可变的呢? 其实这里存在一个误区: s只是一个String对象的引用,并非对象自己。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个4字节的数据,里面存放了它所指向的对象的地址,经过这个地址能够访问对象。
也就是说,s只是一个引用,它指向了一个具体的对象,当s=“123456”; 这句代码执行过以后,又建立了一个新的对象“123456”, 而引用s从新指向了这个心的对象,原来的对象“ABCabc”还在内存中存在,并无改变。内存结构以下图所示:

 

 

为何String对象是不可变的?性能

要理解String的不可变性,首先看一下String类中都有哪些成员变量。 在JDK1.6中,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

  在JDK1.7中,String类作了一些改动,主要是改变了substring方法执行时的行为,这和本文的主题不相关。JDK1.7中String类的主要成员变量就剩下了两个:spa

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

  由以上的代码能够看出, 在Java中String类其实就是对字符数组的封装。JDK6中, value是String封装的数组,offset是String在这个value数组中的起始位置,count是String所占的字符的个数。在JDK7中,只有一个value变量,也就是value中的全部字符都是属于String这个对象的。这个改变不影响本文的讨论。 除此以外还有一个hash成员变量,是该String对象的哈希值的缓存,这个成员变量也和本文的讨论无关。在Java中,数组也是对象。 因此value也只是一个引用,它指向一个真正的数组对象。其实执行了String s = “ABCabc”; 这句代码以后,真正的内存布局应该是这样的:.net

value,offset和count这三个变量都是private的,而且没有提供setValue, setOffset和setCount等公共方法来修改这些值,因此在String类的外部没法修改String。也就是说一旦初始化就不能修改, 而且在String类的外部不能访问这三个成员。此外,value,offset和count这三个变量都是final的, 也就是说在String类内部,一旦这三个值初始化了, 也不能被改变。因此能够认为String对象是不可变的了。
 
那么在String中,明明存在一些方法,调用他们能够获得改变后的值。这些方法包括substring, replace, replaceAll, toLowerCase等。例如以下代码:
		
		String a = "ABCabc";
		System.out.println("a = " + a);
		a = a.replace('A', 'a');
		System.out.println("a = " + a);
		

  输出结果:线程

a = ABCabc
a = aBCabc
那么a的值看似改变了,其实也是一样的误区。再次说明, a只是一个引用, 不是真正的字符串对象,在调用a.replace('A', 'a')时, 方法内部建立了一个新的String对象,并把这个心的对象从新赋给了引用a。
		String ss = "123456";
		
		System.out.println("ss = " + ss);
		
		ss.replace('1', '0');
		
		System.out.println("ss = " + ss);

  

打印结果:
ss = 123456
ss = 123456

String对象真的不可变吗?

从上文可知String的成员变量是private final 的,也就是初始化以后不可改变。那么在这几个成员中, value比较特殊,由于他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其余数组对象,那么我能改变value指向的数组吗? 好比将数组中的某个位置上的字符变为下划线“_”。 至少在咱们本身写的普通代码中不可以作到,由于咱们根本不可以访问到这个value引用,更不能经过这个引用去修改数组。
那么用什么方式能够访问私有成员呢? 没错,用反射, 能够反射出String对象中的value属性, 进而改变经过得到的value引用改变数组的结构。下面是实例代码:
	public static 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对象不可变。
 
连接:https://blog.csdn.net/zhangjg_blog/article/details/18319521
相关文章
相关标签/搜索